Django + Wagtail Custom Users

Tags: user django authentication custom wagtail

The Problem

As I said in my last post, I'm aiming to add the ability for users to log in to the website using django-registration. It should be pretty simple, especially considering I got it working just fine on my nonprofit's website. Unfortunately, this website is essentially all about creating silly problems for myself so I can learn by fixing them. And thus, I have run into a problem that I did not encounter on my other domain:

AttributeError at /accounts/register/
Manager isn't available; 'auth.User' has been swapped for 'custom_user.CustomUser'

Argh!

I had created a custom wagtail user model pretty much just because Wagtail showed me how to in their documentation. And now Django doesn't know how to create users like normal.

There are of course a number of ways to solve this. I will be using the one I can figure out first.

The Struggle

Google has led me to a SO answer that makes it seem like an easy fix. We shall see.

Essentially all I have to do is instruct Django to use my model instead of the one provided in django.contrib.auth:

from django.contrib.auth import get_user_model

User = get_user_model()

So now I just have to figure out exactly where to place that code. And... after some more googling I found that the Django docs themselves describe how to address this issue. And this is why I love Django.

from django.contrib.auth.forms import UserCreationForm
from myapp.models import CustomUser

class CustomUserCreationForm(UserCreationForm):

    class Meta(UserCreationForm.Meta):
        model = CustomUser
        fields = UserCreationForm.Meta.fields + ('custom_field',)

But since we're using django-registration, we should defer to their docs first. The process looks similar to the official docs:

from registration.forms import RegistrationForm

from mycustomuserapp.models import MyCustomUser

class MyCustomUserForm(RegistrationForm):
    class Meta:
        model = MyCustomUser

After making the appropriate changes in my custom user app's forms.py file, I get the following error:

django.core.exceptions.ImproperlyConfigured: Creating a ModelForm without either the 'fields'   attribute or the 'exclude' attribute is prohibited;

It instructs me to edit my custom user creation form. For now I add the most basic implementation by specifying the fields from the parent class:

fields = RegistrationForm.Meta.fields

Now when attempting to create a new user using the registration form I get a timeout error with the following (truncated) traceback:

Traceback:

File "/home/ave/venv/averyuslaner.com/lib/python3.5/site-packages/registration/backends/hmac/views.py" in create_inactive_user
  55.         self.send_activation_email(new_user)

File "/home/ave/venv/averyuslaner.com/lib/python3.5/site-packages/registration/backends/hmac/views.py" in send_activation_email
  98.         user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)

File "/home/ave/venv/averyuslaner.com/lib/python3.5/site-packages/django/contrib/auth/models.py" in email_user
  366.         send_mail(subject, message, from_email, [self.email], **kwargs)

File "/home/ave/venv/averyuslaner.com/lib/python3.5/site-packages/django/core/mail/__init__.py" in send_mail
  62.     return mail.send()

File "/home/ave/venv/averyuslaner.com/lib/python3.5/site-packages/django/core/mail/message.py" in send
  348.         return self.get_connection(fail_silently).send_messages([self])

Exception Type: TimeoutError at /accounts/register/
Exception Value: [Errno 110] Connection timed out

So now it's just a matter of configuring the connection back to the mail server which also happens to be the same server running this website. I just needed to remove the Digital Ocean firewall since I already had UFW configured to handle all the appropriate firewall rules.

Since the error timed out while trying to send the activation email, the user was actually created and saved to the database. I'm just going to delete that user and try again from the beginning.

I'm also going to start matching up the django-registration form to match my custom user model fields. So I need to add some name fields which is as simple as editing the fields attribute in my CustomUserForm:

fields = ['first_name', 'last_name'] + RegistrationForm.Meta.fields

Sweet, and now we successfully get an activation email sent to the email address supplied when submitting the registration form.

But we also get a new error when trying to access the activation link sent to our new user:

django.urls.exceptions.NoReverseMatch: Reverse for 'login' not found. 'login' is not a valid view function or pattern name.
[14/Sep/2017 12:04:24] "GET /accounts/activate/complete/ HTTP/1.1" 500 158304

So I just need to throw in the default urls from django.contrib.auth:

url(r'', include('django.contrib.auth.urls'))

At this point everything seems to work properly but when we login, we get a 404 error because the default redirect page for a django login is /accounts/profile which doesn't point anywhere yet. Were going to specify where that redirection should occur and create a template for it:

# Django auth settings added to base settings file
LOGIN_REDIRECT_URL = 'edit_profile'

The template is pretty basic:

{% extends "base.html" %}
{% load home_extras %}

{% block title %}Edit your profile{% endblock %}

{% block heading_text %}Edit Your Profile{% endblock %}

{% block content %}
    <div>
        <p>
            Use the form below to edit your profile.
        </p>
        <p>
            Use whatever name you'd like to be identified with while on this website. If
            you leave it blank, we'll identify you as {{ user.username }}, your username.
        </p>

        <form method="POST" action="." class="form-horizontal">
            {% csrf_token %}
            {% for error in form.non_field_errors %}
                <div class="form-group has-error text-danger">
                    {{ error }}
                </div>
            {% endfor %}
            {% for field in form %}
                <div class="form-group has-error text-danger">
                    {{ field.errors }}
                </div>
                <div class="form-group">
                    {{ field.label_tag }}
                    {{ field|add_css:"form-control" }}
                </div>
            {% endfor %}
            <button class="btn btn-primary form-group">Save
        </form>
    </div>
{% endblock %}

But we still need to create the view to serve it up:

@login_required
def edit_profile(request):
    user = CustomUser.objects.get(username=request.user.username)
    form = ProfileForm(request.POST or None, instance=user)
    if form.is_valid():
        form.save()
        return redirect('user_profile', request.user.username)
    return render(request, "accounts/edit_profile.html", {'form': form})

And create the form referenced in the above view:

class ProfileForm(forms.ModelForm):
    first_name = forms.CharField(
        required=True,
        widget=forms.TextInput(attrs={'placeholder': 'First Name'})
    )
    last_name = forms.CharField(
        required=False,
        widget=forms.TextInput(attrs={'placeholder': 'Last Name'})
    )
    email = forms.EmailField(
        required=True,
        widget=forms.TextInput(attrs={'placeholder': 'Email'})
    )

    class Meta(object):
        model = CustomUser
        fields = ['first_name', 'last_name', 'email']

    def __init__(self, *args, **kwargs):
        # Instance is a CustomUser object
        instance = kwargs.get('instance', None)
        if instance:
            kwargs.setdefault('initial', {}).update({'first_name': instance.first_name,
                                                     'last_name': instance.last_name,
                                                     'email': instance.email})
        super().__init__(*args, **kwargs)

    def save(self, commit=True):
        # Instance is a CustomUser object
        instance = super().save(commit=commit)
        instance.first_name = self.cleaned_data['first_name']
        instance.last_name = self.cleaned_data['last_name']
        instance.email = self.cleaned_data['email']
        if commit:
            instance.save()
        return instance

Upon saving the edit form, our view redirects to the normal profile view which we also need to create under templates/accounts/user_profile.html:

{% extends "base.html" %}
{% load humanize %}

{% block title %}{% firstof user_obj.profile.name user_obj.username %}{% endblock %}

{% block heading_text %}User Profile{% endblock %}

{% block content %}
    <div>
        <h3>
            {% firstof user_obj.profile.name user_obj.username %}
        </h3>

        <p>
            Hey internet person.
        </p>

        <p>
            You can edit your profile <a href="{% url 'edit_profile' %}">here. If you want. No pressure.
        </p>
    </div>
{% endblock content %}

Cool, now we have everything working properly so we just need to make some improvements to the nav page. We need to hook up the login button to the actual login function:

{% if user.is_authenticated %}
    <ul class="nav navbar-nav navbar-right">
        <li class="dropdown">
            <a href="#" class="dropdown-toggle" data-toggle="dropdown">My Account <b class="caret">
                <ul class="dropdown-menu">
                    <li><a href="{% url 'user_profile' user %}">Profile
                    <li><a href="{% url 'logout' %}">Logout
                </ul>
        </li>
    </ul>
{% else %}
    <a class="navbar-text navbar-right" href="{% url 'registration_register' %}">Register
    <form method="POST"
                    action="{% url 'login' %}?next={% if request.path == '/logout/' %}/{% else %}{{request.path}}{% endif %}"
                    class="navbar-form navbar-right">
        {% csrf_token %}
        <div class="form-group">
            <input type="text" name="username" placeholder="Username" id="id_username" class="form-control">
        </div>
        <div class="form-group">
            <input type="password" name="password" placeholder="Password" id="id_password"
                class="form-control">
        </div>
        <button type="submit" class="btn btn-success">Sign in
    </form>
{% endif %}

And voilà! We now have a working registration process!