Goal for this part
Give every user a public profile with an avatar, bio, and neighborhood. Let signed-in people follow and unfollow from that page with HTMX — the button swaps in place.
Concepts
The profile URL is /n/<username>/ (“n” for neighbor). Follow is a POST that inserts or deletes a Follow row. HTMX targets the button fragment so the rest of the profile does not reload. Authorization still runs in Django: you cannot follow yourself, and guests are sent to login.
Walkthrough
# social/views.py
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404, render
from django.views.decorators.http import require_POST
from .models import Follow
@login_required
@require_POST
def toggle_follow(request, username):
target = get_object_or_404(User, username=username)
if target == request.user:
return render(request, "social/_follow_button.html", {"profile_user": target, "is_following": False})
follow, created = Follow.objects.get_or_create(follower=request.user, following=target)
if not created:
follow.delete()
is_following = False
else:
is_following = True
return render(
request,
"social/_follow_button.html",
{"profile_user": target, "is_following": is_following},
)<form hx-post="{% url 'social:toggle_follow' profile_user.username %}"
hx-target="this"
hx-swap="outerHTML">
{% csrf_token %}
<button type="submit" class="rounded px-3 py-1 {% if is_following %}bg-stone-200{% else %}bg-emerald-700 text-white{% endif %}">
{% if is_following %}Following{% else %}Follow{% endif %}
</button>
</form>Avatar uploads go to MEDIA_ROOT. In development Django can serve them; on the VPS, Nginx will. Validate content type and size in the profile form — a 20 MB “avatar” is a denial of service.
How to run it and what you should see
Sign in as Ada, open Grace’s profile, click Follow. The button should flip to Following without a full reload. Refresh: the state sticks. Click again to unfollow. Ada’s own profile must not show a Follow button.
Common mistakes
Missing MEDIA_URL routes in urls.py makes avatars 404 locally. A GET follow URL will be crawled by prefetchers and create follows by accident — keep it POST. Forgetting hx-headers on <body> yields 403.
Try this
Upload a square JPEG as Ada, then a huge PNG. The second should fail validation. Count Ada’s following and Grace’s followers on each profile header.