Embedding a login form in a Wagtail page

When a registered user wants to access restricted functionality on a specific page, it would be more user-friendly to enable him/her to sign in on that page instead of being redirected to a login page.

July 12, 2020, 6 p.m.
Themes: Authentication

In a previous tutorial we have enabled registered users to comment easily on articles. When a registered user is not logged in however, he/she would have to go to the login page and then return to the specific article. There a many ways to solve this, one being a pop-up login form accessible via the menu. In this tutorial we will embed a simple login form in the specific article page.

The steps are:

  1. add a login form to the page by overriding the serve() method of the page,
  2. add a login form template to the page template and initially hide it,
  3. add a link to unhide the form via jQuery,
  4. submit the filled-in form via jQuery,
  5. return to the page to allow the user to submit his comment.

To override the serve() method add the following to the page model (in our case ArticlePage):

from allauth.account.forms import LoginForm

def serve(self, request, *args, **kwargs):
    response = super().serve(request, 'cms/article_page.html')
    response.context_data['login_form'] = LoginForm()
    return response

We use the standard LoginForm from the package allauth; use your own if you have a different one. First we call the super() method to get the default response. Wagtail returns a TemplateResponse object which has a context_data field to which we can add the form.

The html-code to add to the template just has a login field (in our case email), a password field and a submit button:

{% if not request.user.is_authenticated %}
    <div class="container mt-4 ml-0">
        <form method="POST" id="id_login_form" action="{% url 'account_login' %}" style="display: none">
            {% csrf_token %}
            <div class="row align-items-end">
                <div class="col-6 form-group">
                    {% with field=login_form.login %}{% include "account/form_field.html" %}{% endwith %}
                <div class="col-4 form-group">
                    {% with field=login_form.password %}{% include "account/form_field.html" %}{% endwith %}
                <div class="col-2 form-group">
                    <button id="id_submit_login_form" class="btn btn-outline-primary">{% trans "Sign in" %}</button>
{% endif %}

We initially hide the form by setting style="display: none" in the form label. A link to unhide it is added by:

{% if not request.user.is_authenticated %}
    <div id="id_comment_invite" class="container-fluid mt-4">
        <p>{% trans "Comment on this article ("%}<a href="#" id="id_login_first_link">{% trans "sign in first" %}</a>{% trans " or confirm by name and email below)" %}</p>
{% endif %}

Adapt the text to your own needs. Wagtail by default has a block extra_js in our base template; add it if necessary. Since we are going to use ajax to send our form, make sure that your jQuery can handle it. When you have previously used a jQuery link for Bootstrap it might be a slim version, which cannot handle ajax. Copy the link from https://code.jquery.com/ and replace the existing jQuery link in base.html at the bottom:

<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>

On the article_page.html template add:

{% block extra_js %}

            $("#id_login_first_link").click(function(event) {

{% endblock %}

This simple Javascript code unhides the login form when the user clicks the link. The event.preventDefault() command prevents scrolling of the page. The code for the login form to be added above is one ajax command:


        url: "{% url 'account_login' %}",
        data: $('#id_login_form').serialize(),
        success: function(response, status){
        error: function(xhr, status, error){

The event.preventDefault() prevents submitting the form, which would bring the user to another page. The url to which the POST form should be sent is account_login, declared in our authentication app. The data to be sent is the serialized form. On success we hide the form and the link and reload the page. Reloading could be prevented, but then we would have to regenerate a csrf token, to prevent an error when a comment is subsequently sent. Additionally we would have to hide the comment fields that we don't need, and change some more things, such as enabling a user can reply on a given comment (which also requires being logged in). So here we choose the lazy option and reload the page. When the login is not successful, e.g. because the user did not fill in the right password, we do submit the login form. This redirects the user to whatever page a failed login generates. For our purposes here we don't want to handle all sign in and sign up eventualities on the article page.

Migrating the database should not be necessary. Try out if it works as expected.

An additional note on Django Comments Xtd. The package also has a Javascript option which submits comments via ajax as well, without reloading the page. It also polls for new comments. This really works beautifully out-of-the-box. A drawback is that it is not trivial to change the styling of the html-elements; this requires diving into the React code. Integrating the login functionality requires some more work. So we leave it for now and maybe revisit some other time.

If you would like to add editable forms to your Wagtail pages, read on.

Comment on this article (sign in first or confirm by name and email below)