Profiles and Follows

Show public profiles, upload avatars, and follow or unfollow neighbors with HTMX.

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.