~/TechPurAI
~/tutorials/django-from-scratch/permissions-and-login-required
intermediate·part 17 of 22·2 min read

Restricting access: login_required and LoginRequiredMixin

Updated Aug 15, 2026Python · Django

Every view built since part 9 is reachable by anyone who knows the URL, logged in or not. This part closes that: requiring login for anything that changes data, and restricting editing further to a post's own author.

LoginRequiredMixin

python
# blog/views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import CreateView, UpdateView, DeleteView

class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    form_class = PostForm
    template_name = "blog/post_form.html"
    success_url = reverse_lazy("post_list")

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)

LoginRequiredMixin has to come before CreateView in the class's parent list — Python resolves methods left to right through multiple inheritance, and the mixin needs to intercept the request before CreateView's own logic runs. Add the same mixin, first in line, to PostUpdateView and PostDeleteView.

An anonymous request to any of these now redirects to the login page instead of running the view — by default, to whatever LOGIN_URL is set to in settings.py (it defaults to /accounts/login/, which already matches the URL from part 16).

The function-based equivalent

python
from django.contrib.auth.decorators import login_required

@login_required
def some_function_view(request):
    ...

@login_required is the same protection as LoginRequiredMixin, for a function-based view instead of a class-based one — this project doesn't need it, since every view left needing protection is already class-based, but it's the name to reach for in a project with function views.

Restricting edits to a post's own author

Login alone isn't enough — right now, any logged-in user can edit or delete any post, not just their own. UpdateView and DeleteView both call get_queryset() to find the object to work with; overriding it is how to scope that lookup:

python
class PostUpdateView(LoginRequiredMixin, UpdateView):
    model = Post
    form_class = PostForm
    template_name = "blog/post_form.html"
    slug_field = "slug"
    slug_url_kwarg = "slug"

    def get_queryset(self):
        return Post.objects.filter(author=self.request.user)

    def get_success_url(self):
        return reverse_lazy("post_detail", kwargs={"slug": self.object.slug})

Narrowing the queryset to author=self.request.user means a logged-in user editing someone else's post slug gets a 404, not the other user's post — the lookup genuinely finds nothing, since it was never looking outside their own posts to begin with. Apply the same get_queryset() override to PostDeleteView.

Common mistake

Only hiding the edit/delete links from users who shouldn't see them (the {% if user == post.author %} template check from part 15) and treating that as the actual protection. A template check controls what's shown — it does nothing to stop a direct request to the URL. The get_queryset() override above is the real enforcement; the template check is just a courtesy that keeps a logged-in user from seeing links that would 404 anyway.

Next: the pieces that make the site look and feel finished — starting with CSS and JavaScript, served correctly.

VK

Vijay Kumar

Founder of TechPurAI — writing hands-on tutorials and honest tool breakdowns.

LinkedIn ↗
← previous16. User authentication: login, logout, and signupnext →18. Static files: serving CSS and JavaScript correctly