models.py — designed the Skill, Review, and BookingRequest tables before writing a single view. Got the ForeignKey relationships right (especially unique_together on Review) before running migrations.Portfolio
Portfolio Assistant
Ask questions about the work, projects, or how to get in touch.
A full-stack student marketplace built from the ground up in Django — no tutorials followed, no scaffolds cloned. Students post skills they can teach, browse what others offer, leave star reviews, and send booking requests. Complete auth, CRUD, search, and a personal dashboard, all wired together with Django's MVT pattern.
Browse view with live search + category filter. Every card links to a full detail page with reviews and booking form.
Three models, two foreign key relationships. Django handles the SQL — you write the Python class, it creates the table.
Click a feature to see how it's built — from the URL pattern to the view logic.
The browse page supports simultaneous keyword search (title OR description) and category filter. Both are GET parameters so the URL stays shareable and the browser back button works perfectly.
Django's Q objects let you chain OR conditions on the queryset — no raw SQL needed. The annotate() call computes each skill's average rating in a single database query.
# skills/views.py — skill_list() from django.db.models import Q, Avg def skill_list(request): skills = Skill.objects.filter( status='available' ).annotate( avg_rating=Avg('reviews__rating') ) query = request.GET.get('query', '').strip() category = request.GET.get('category', '') if query: skills = skills.filter( Q(title__icontains=query) | Q(description__icontains=query) ) if category: skills = skills.filter(category=category) return render(request, 'skills/skill_list.html', {'skills': skills, 'query': query} )
Django's built-in User model handles password hashing and session management. The custom RegisterForm wraps UserCreationForm for consistent styling.
The @login_required decorator protects any view that modifies data — post a skill, edit, delete, or send a booking. Unauthenticated users get redirected to login and bounced back to their original destination after signing in.
# users/views.py from django.contrib.auth import login from .forms import RegisterForm def register(request): if request.method == 'POST': form = RegisterForm(request.POST) if form.is_valid(): user = form.save() login(request, user) return redirect('skills:dashboard') else: form = RegisterForm() return render(request, 'registration/register.html', {'form': form} ) # Protect a view with one decorator: @login_required def skill_create(request): ... # settings.py LOGIN_REDIRECT_URL = 'skills:dashboard' LOGIN_URL = 'users:login'
The skill detail page handles two separate form submissions in one view — a review form and a booking form — distinguished by a hidden field name in the POST body. Both forms are validated server-side with ModelForms.
Owners get an inbox of incoming requests on their dashboard and can accept, decline, or mark bookings complete via a single booking_update view that takes an action URL parameter.
# Dual-form POST handling if request.method == 'POST': if 'submit_review' in request.POST: # handle star review review_form = ReviewForm(request.POST) if review_form.is_valid(): review = review_form.save(commit=False) review.skill = skill review.reviewer = request.user review.save() elif 'submit_booking' in request.POST: # handle booking request booking_form = BookingRequestForm(request.POST) if booking_form.is_valid(): booking = booking_form.save(commit=False) booking.skill = skill booking.requester = request.user booking.save() # Owner action URL: /skills/booking/<pk>/accept/ def booking_update(request, pk, action): booking = get_object_or_404(BookingRequest, pk=pk) if action == 'accept': booking.status = 'accepted' booking.save()
Each user gets a dashboard showing their posted skills, incoming booking requests from other students, and their own outgoing requests — all pulled from the database in a single view with three querysets.
Stats (skill count, inbox count, sent count) are computed with Django's .count() and passed as a list of tuples so the template can loop over them cleanly.
# skills/views.py — dashboard() @login_required def dashboard(request): my_skills = Skill.objects.filter( owner=request.user ) incoming = BookingRequest.objects.filter( skill__owner=request.user ).order_by('-created_at') outgoing = BookingRequest.objects.filter( requester=request.user ).order_by('-created_at') stats = [ ('My Skills', my_skills.count(), '📋'), ('Incoming Requests', incoming.count(), '📬'), ('Requests Sent', outgoing.count(), '📤'), ] return render(request, 'skills/dashboard.html', {'my_skills': my_skills, 'incoming': incoming, 'outgoing': outgoing, 'stats': stats} )
models.py — designed the Skill, Review, and BookingRequest tables before writing a single view. Got the ForeignKey relationships right (especially unique_together on Review) before running migrations.app_name = 'skills' so templates could use {% url 'skills:skill_detail' pk=skill.pk %}. Built all seven FBVs — list, detail, create, update, delete, dashboard, booking_update.clean() validation to SkillForm: if is_free=False and price is blank, raise a ValidationError before the form ever hits the database.@login_required decorator handles redirect logic automatically. The hardest part was correctly setting LOGIN_URL, LOGIN_REDIRECT_URL, and LOGOUT_REDIRECT_URL in settings so the flow felt seamless.base.html with Bootstrap 5 navbar and flash messages. Every other template extends it with {% block content %}. The Django messages framework showed success/error alerts across form submissions automatically.skill.owner == request.user before proceeding. Tried reviewing my own skill — got blocked. Tried booking my own skill — blocked. Django's ORM makes these checks trivially easy to add.Avg('reviews__rating').
Booking lifecycle management (pending → accepted → completed). Flash messages on every action.
Django Admin registration for all models — Skill, Review, and BookingRequest are registered
in admin.py so the
built-in admin panel provides full data management without extra code.
'submit_review' in request.POST vs
'submit_booking' in request.POST
as the branch condition was the key insight.
models.py,
views.py,
templates, and migration history.