Ryan's Chipy Blog

From Hatchling to Slightly Experienced Hatchling The Trilogy, Part 3

Hello Earth!

It's the time of year again where I let all my invisible internet friends know about my Python adventures! So let's get down to it!

Loggin' In!

Over the past few days Evan and I have wrangled the Django system into letting us place the signup and login forms on the same page and I think we've really got something nice here. There's always room for more tests but it's such a careful line to walk when working with a codebase that you didn't write. Do I test to see if Django is still working like it was when I wrote the login? Is that a waste of time? What if Django radically rewrites their built-in login functions? In the end, I settled on a few simple tests, so let's dig in!

URLs

Here's the relevant parts of the url.py file we settled on:

from django.conf.urls import url
from django.contrib import admin
from soundboard import views

urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^login/$', views.login_user, name='login_user'),
url(r'^signup/$', views.signup, name='signup'),
url(r'^login_or_signup/$', views.login_or_signup, name='login_or_signup'),
]
              

The login_or_signup/ URL is the landing page that the nav bar will point to. The specific signup and login routes will handle the given action of each flow, signing up and logging respectively. login_user will send you down to the login route, and signup to the signup route.

Template

    {% extends 'soundboard/base.html' %}
    {% block content %}

  <form method="post" action="/login/">
    <h4 class="card-title">
      <a href="#">Log In!</a>
    </h4>
    {% csrf_token %}
    {{ authentication_form.as_p }}
    <button type="submit">Login</button>
  </form>

  <form method="post" action="/signup/">
    <h4 class="card-title">
      <a href="#">Sign Up!</a>
    </h4>
    <p class="card-text">To sign up, please provide an email, desired username, and password.</p>
    {% csrf_token %}
    {{ user_creation_form.as_p }}
    <button type="submit">Sign up</button>
  </form>

  {% endblock %}
            

Here we're etablishing the login form from our views with the variable authentication_form, and letting Django populate that sucker with as_p which generates said form with <p> tags. We do the same for the signup form with the variable user_creation_form. This view might evolve over time with ever increased fancyness but this gets the job done for now.

Views

Here are the relevant parts of the views.py file needed to use Django's built-in user authentication tools.

from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.views.decorators.csrf import csrf_protect
from django.contrib.auth import login, authenticate
from django.contrib.auth.models import User
from django import forms
               

We opted to use decorators for the CSRF tokens due to the ease of use, and the rest were imported in order to use some built-in authentication features of Django.

class SignUpForm(UserCreationForm):
    email = forms.EmailField(max_length=254, help_text='Required. Submit a valid email address.')

    class Meta:
        model = User
        fields = ('username', 'email', 'password1', 'password2', )
                

Here we're establishing the user model and adding in the email field to the UserCreationForm.

@csrf_protect
def login_user(request):
    if request.method != 'POST':
        return redirect('/login_or_signup/')
    authentication_form = AuthenticationForm(data=request.POST)
    if not authentication_form.is_valid():
        return login_or_signup(request, authentication_form=authentication_form)
    username = authentication_form.cleaned_data.get('username')
    password = authentication_form.cleaned_data.get('password')
    user = authenticate(username=username, password=password)
    login(request, user)
    return redirect('your_boards')
                

Alright so here comes some pure, unadaultered magic. So first thing's first, the form was decorated with CSRF tokens. Next some logic to prevent some sneaky users or an eager search index that tries to let a user try to pull a fast one and just GET themselves to a login_user route. They'll be appropriately redirected back to the login_or_signup route to POST a login attempt. Following this, if the authentication form isn't valid we'll send the user back to the form and let Django throw some validation messages on the form.

If the form submits with valid inputs, we'll redirect the user to the your_boards URL, which is the logical place for soundboard users to be after a successful login.

@csrf_protect
def signup(request):
    if request.method != 'POST':
        return redirect('/login_or_signup/')
    user_creation_form = SignUpForm(data=request.POST)
    if not user_creation_form.is_valid():
        return login_or_signup(request, user_creation_form=user_creation_form)
    user_creation_form.save()
    username = user_creation_form.cleaned_data.get('username')
    password = user_creation_form.cleaned_data.get('password1')
    user = authenticate(username=username, password=password)
    login(request, user)
    return redirect('your_boards')
                

The signup form follows more or less the same logic as the login. CSRF protected, no sneaky GETs, and we'll let Django handle the validations to the user models.

def login_or_signup(request, authentication_form=None, user_creation_form=None):
    if authentication_form is None:
        authentication_form = AuthenticationForm()
    if user_creation_form is None:
        user_creation_form = SignUpForm()
    return render(request, "soundboard/login_or_signup.html", {'authentication_form': authentication_form,
        'user_creation_form': user_creation_form})
              

Finally, we set up some logic for login_or_signup to display each form without any settings in place each time one form is submitted and potentially redirected back to the page.

Tests!

While writing some functional that gets one to your next step is all fine and good, you'll want to add some tests in place that affirm your code does what you set out for it to do. Moreover, it's a good means for future developers (myself included) of this open source soundboard generator to ensure their updates don't break key functionatly. Here's some basic tests we settled on to make sure things run 👌.

These tests should be very self explanitory (otherwise they don't really work well as tests) so I'll keep the words to a minimum.

                from django.test import TestCase, Client
from django.contrib.auth.models import User

class AuthenticationTestCase(TestCase):
    def test_signup_creates_user(self):
        client = Client()

        client.post('/signup/', {
            'username': 'ryguy',
            'email': 'ryguy@example.com',
            'password1': 'mtxMAPC6ch1EP',
            'password2': 'mtxMAPC6ch1EP',
        })

        new_user = User.objects.get(username='ryguy')
        self.assertEqual(new_user.email, 'ryguy@example.com')
              

POSTing a valid form to the signup route makes a new user with the data from that form.

    def test_signup_redirects_to_your_boards(self):
        client = Client()

        response = client.post('/signup/', {
            'username': 'ryguy',
            'email': 'ryguy@example.com',
            'password1': 'mtxMAPC6ch1EP',
            'password2': 'mtxMAPC6ch1EP',
        })

        self.assertEqual(response.url, '/your_boards/')
              

After a successful signup, we redirect the user to your_boards.

    def test_login_redirects_to_your_boards(self):
        User.objects.create_user('ryguy', 'ryguy@example.com', 'mtxMAPC6ch1EP')
        client = Client()

        response = client.post('/login/', {
            'username': 'ryguy',
            'password': 'mtxMAPC6ch1EP',
        })

        self.assertEqual(response.url, '/your_boards/')
              

Just like with a signup, a successful login gets redirected to your_boards

    def test_get_call_to_login_redirects_to_login_or_signup(self):
    client = Client()

    response = client.get('/login/')

    self.assertEqual(response.url, '/login_or_signup/')
              
              
def test_get_call_to_signup_redirects_to_login_or_signup(self): client = Client() response = client.get('/signup/') self.assertEqual(response.url, '/login_or_signup/')

A sneaky GET is met with a redirect back to signup_or_login.

And that's just about it! The site now lets you sign up and log in, and we didn't compromise on separating those two view onto descrete URL's. I'm super happy about how all this went and learned a LOT about testing and general Django wranglin'.

Conclusions

Since this is the final sanctioned blog, it'll probably be a good while before I update you all with anymore progress. However I have good faith I'll be able to at least pull off a basic sound upload UI and some basic soundboard templates before the big day in December.

Oh and before I forget, Evan is amazing and I legitimately look forward to every opprotunity I have left to work with him on this project. Chipy rules, Python rules and I am one lucky duck to have been admitted into this program. Someday I hope to be able to give back so I'll keep at it untill then!

The Development Soundboard Link For The Curious

Untill next time!

Fin