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 CuriousUntill next time!