Django 1.8 Tutorial – 3. Adding Account Logins

The previous article cleaned up the UI and made the comment system work more like a comment system, but it has a glaring flaw: you could choose to post as any user. LOLz.

This small modification adds login and logout features. It does it the raw way rather than use the built-in classes, or the django-registration-redux library. This is just a temporary feature, an example to learn authentication.

That said, it does something a little different from what seems to be provided by Django: the login is embedded right in the page, where the form would have been.

You start off by adding two urls, one to log in, and one to log out. Add this to the urlpatterns in urls.py:

    url(r'^login/$',
        views.LoginView.as_view(),
        name='login'),
    url(r'^logout/$',
        views.LogoutView.as_view(),
        name='logout'),

Then you create the views and a login form. We also need to remove the user selector from the comment form. (Now the comment for has just one field.)

class CommentForm(forms.Form):
    text = forms.CharField(
        label="Comment",
        max_length=1024,
        widget=forms.Textarea)

class LoginForm(forms.Form):
    username = forms.CharField(label="User")
    password = forms.CharField(widget=forms.PasswordInput, label="Password")

Since we have the comment form on the CommentList view, we need to add the login form there, as well. It’ll be switched in the template’s logic.

class CommentList(ListView):
    model = Comment

    # adding a form to a listview
    def get_context_data(self, **kwargs):
        form = CommentForm
        loginform = LoginForm
        context = super(CommentList, self).get_context_data(**kwargs)
        context['form'] = form
        context['loginform'] = loginform
        context['is_authenticated'] = self.request.user.is_authenticated()
        return context

The CreateView needs to be modified because we no longer specify the author in the form. Instead, we need to get the user from the request. This is kind of tricky. Here’s the code:

class CommentCreate(CreateView):
    model = Comment
    fields = ['text']

    def form_valid(self, form):
        comment = form.save(commit=False)
        comment.author = User.objects.get(pk=self.request.user.pk)
        comment.save()
        return http.HttpResponseRedirect(reverse('comment_list'))

form_valid(self, form) is called after the form is validated. note that the form is a ModelForm based on Comment, but includes only the ‘text’ field. So, when the input contains only text, with no author, it’s considered valid.

The form validates, and form_valid is called.

The first line, form.save(commit=False), saves the form, creating a Comment object, but doesn’t persist the object to storage. This gives us time to alter the object before it’s written to the database.

We alter it by setting the author property. We can’t just set it with the ID (the PK) of the user. We need a User object, so we do a lookup based on the PK.

Then, we save() again, this time, allowing it to save to the database.

Lastly, we redirect back to the comment list.

And, finally, the login and logout views.

class LoginView(View):
    def post(self, request, *args, **kwargs):
        user = auth.authenticate(
            username=request.POST['username'],
            password=request.POST['password']
        )
        if user is not None:
            if user.is_active:
                auth.login(request, user)
            else:
                messages.error(request, 'Account not available.')
        else:
            messages.error(request,
                'Password incorrect or account not available.')
        return http.HttpResponseRedirect(reverse('comment_list'))


    def get(request, *args, **kwargs):
        # we should never get to this codepath
        return http.HttpResponseRedirect(reverse('comment_list'))


class LogoutView(View):
    def get(self, request, *args, **kwargs):
        auth.logout(request)
        return http.HttpResponseRedirect(reverse('comment_list'))

The logout is self-explanatory, but login is not. It’s based on the boilerplate from the Django docs.

Authentication and login is a two-step process. You can authenticate a user who is not active – that is, the user account exists, but they haven’t verified, or a moderator hasn’t verified the user.

The template for the list must also be revised. I’ll paste then entire comment_list.html file, since it’s changed a bit:

{% extends "base.html" %}

{% block content %}
    {% for comment in object_list %}
    <div class="comment">
        <p>{{ comment.text }}</p>
        <p>
            <a href="{% url 'comment_edit' comment.pk %}">edit</a>
            |
            <a href="{% url 'comment_delete' comment.pk %}">delete</a></p>
    </div>
    {% endfor %}

    {% if messages %}
    <ul class="messages">
        {% for message in messages %}
        <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
        {% endfor %}
    </ul>
    {% endif %}

    {% if is_authenticated %}
        User: {{user}} | <a href="{% url "logout" %}">logout</a>
        <form method="post" action="{% url "comment_create" %}">
            {% csrf_token %}
            {{ form.as_p }}
            <input type="submit" />
        </form>
    {% else %}
        <form method="post" action="{% url "login" %}">
            {% csrf_token %}
            {{ loginform.as_p }}
            <input type="submit" />
        </form>
    {% endif %}
{% endblock %}

All the changes are at the bottom. There’s a test for is_authenticated that switches between the login form and comment form. There’s an added logout link.

Above this is boilerplate that displays error messages.

Conclusion

This application is better, but still incomplete. Also, we’re doing our own login form, when we should be using Django’s provided authentication views.

The built-in auth, plus the add-on django-registration-redux, are really confusing pieces of software, but I’m figuring it’s a good investment to learn it, and use it, and extend it, because right off the bat, it saves you from writing several screens of user interaction.

Attachment Size
minimal3.tgz 15.97 KB