This assignment is designed for you to practice writing forms and adding authentication to an existing app.
Scoring is as follows:
Criteria | Possible |
---|---|
Add Login & Logout links | 5 |
Add Accounts URLConf | 5 |
Add Registration App & Login Page | 10 |
Add Message for Logged-in Users | 10 |
Sign Up Page | 10 |
New Question Creation Form | 10 |
Save the Question's Author | 10 |
New Choice Creation Form | 10 |
TOTAL | 70 |
To complete this assignment, you MUST have a working version of the Polls tutorial, completed through at least Part 4.
To begin, make a copy of your Polls Tutorial folder. If this folder is called django-tutorial
, you can do this as follows in the command line:
$ cp -r django-tutorial polls-redux
Then, set up a new repository for your Polls application on GitHub.com and execute the following to switch the remote:
$ git remote remove origin
$ git remote add origin git@github.com:USERNAME/REPOSITORY.git
Alternatively, if you'd prefer, you can complete this project on a separate branch of the same repository you used for your Polls tutorial.
In this part, we'll be adding login & logout functionality to our Polls app.
In the polls/templates/polls
folder, create a file called base.html
. This will hold our HTML code for displaying the login & logout links. In this file, put the following contents:
<p align="right">
<a href="/accounts/login">Login</a>
<a href="/accounts/logout">Logout</a>
</p>
{% block content %}
<p>
This is a placeholder block.
</p>
{% endblock %}
Then, refactor your templates for detail.html
, index.html
, and results.html
by extending base.html
and putting the existing content in between {% block content %}
and {% endblock content %}
blocks, as follows:
{% extends 'polls/base.html' %}
{% block content %}
<!-- ... EXISTING CONTENT GOES HERE ... -->
{% endblock content}
Run your server, and verify that all of your pages now have "Login" and "Logout" links. If you click on one, you will see a "Page not found" error - don't worry, we'll fix that shortly!
In your project's root URLConf (most likely called mysite/urls.py
), add the following to your urlpatterns
list:
path('accounts/', include('django.contrib.auth.urls')),
This single line of code tells Django to include the URLs from the django.contrib.auth
package, which gives us URLs and views for login, logout, password change, password reset, and more! So cool!
If you want to learn more about precisely which URLs are included there, click here.
Now, run your server and refresh the page at /polls/
. Click on one of the links again. What do you see?
You should see the following error: TemplateDoesNotExist at /accounts/login/: registration/login.html
. This means that we need a template called registration/login.html
to render our login page!
In your root folder, run the following command to create a "registration" app:
$ python3 manage.py startapp registration
Make sure to add the registration
app to the INSTALLED_APPS
list in settings.py
.
Now, let's add a template for our login page! In the registration
folder, create a folder called templates
, and inside of that one create another folder called registration
. Inside of that folder, create a file called login.html
and give it the following contents:
{% extends 'polls/base.html' %}
{% block content %}
<h3>Log In!</h3>
{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<p>
<label for="id_username">Username</label>
<input type="text" name="username" id="id_username" required>
</p>
<p>
<label for="id_password">Password</label>
<input type="password" name="password" id="id_password" required>
</p>
<input type="submit" value="login">
</form>
{% endblock %}
Save your work, and try loading the login page again. You should see a form to enter your username and password. Log in with your superuser credentials. (If you've forgotten the password, you can always run python3 manage.py createsuperuser
to create a new login.) What do you see?
That's right! You should see... (dun dun dun) another error message! Let's fix that.
Go to your settings.py
file and enter the following two lines:
LOGIN_REDIRECT_URL = '/polls/'
LOGOUT_REDIRECT_URL = '/polls/'
This will tell Django to take us back to the polls homepage whenever we log in or log out. Once you're done, re-load the login page and try logging in again. Success!
Okay, so that's cool and all, but... How do we actually know we've logged in?! If you've followed the instructions so far, you should find yourself back at the Polls homepage. Let's add a message in base.html
to greet the logged-in user (and to prove to ourselves that we're now logged into the site).
In polls/templates/polls/base.html
, add the following to the very top of the file:
<p>
Hello, {{ request.user.username }}!
</p>
Now, re-load the page and behold: the website now knows your name! WHOA SO COOL! So, why does this work?!
It turns out that whenever we load a template in Django, there are some extra variables that always get passed to the template, even if we don't explicitly include them in the context. One of these is the request
object, which has a property user
that contains the logged-in user object. If there is no logged in user, then request.user
will contain an instance of the AnonymousUser
class instead of the User
class. Pretty cool!
Click here to learn more about the User
and AnonymousUser
models.
Let's make one more change to polls/templates/polls/base.html
. We only want to show this message if the user is logged in. So, let's change the previous lines to the following:
{% if request.user.is_authenticated %}
<p>
Hello, {{ request.user.username }}!
</p>
{% endif %}
This ensures that we will only see the message if we are logged into the site.
As a final challenge, modify the code for the "Login" and "Logout" links to show the "Logout" link only if the user is logged in, and the "Login" link only if they are logged out.
Woo-hoo, we've got the login flow working! But there's still an issue: None of our users can sign up for the site unless they are site admin users. Let's make a sign-up page!
In polls/templates/polls/base.html
, add the following link (to be shown only if the user is not logged in):
<a href="/registration/signup">Sign Up</a>
Now, let's add the code to make this link work. In mysite/urls.py
, add the following to your URLConf:
path('registration/', include('registration.urls')),
Then, in the registration
app, create a file called urls.py
and put the following URL into its urlpatterns
list (and make sure to import the views.py
file):
path('signup/', views.SignUpView.as_view(), name='signup'),
Next, add the SignUpView
class to registration/views.py
:
from django.contrib.auth.forms import UserCreationForm
from django.urls import reverse_lazy
from django.views.generic import CreateView
class SignUpView(CreateView):
form_class = UserCreationForm
success_url = reverse_lazy('login')
template_name = 'registration/signup.html'
Finally, we'll need to make the template file to display to the user. In registration/templates/registration
, create a new template called signup.html
and give it the following contents:
{% extends 'polls/base.html' %}
{% block content %}
<h3>Sign Up!</h3>
<form method="post">
{% csrf_token %}
{% for field in form %}
{% for error in field.errors %}
<p class="text-danger">{{ error }}</p>
{% endfor %}
<p>
<label for="id_{{ field.name }}">{{ field.label|title }}:</label>
<input type="{% if "password" in field.name %}password{% else %}text{% endif %}" name="{{ field.name }}" id="id_{{ field.name }}" required>
</p>
{% endfor %}
<button type="submit">Sign up</button>
</form>
{% endblock %}
Run your server, and verify that you are able to sign up with a new username and password. Once you sign up, you will still need to log in with your new credentials.
In polls/templates/polls/base.html
, add the following link. Make sure it shows only for logged-in users - we don't want just anyone to create a new poll!
<a href="/polls/create">New Poll</a>
Now, let's create the URL for the poll creation page. In polls/urls.py
, add the following URL to your URLConf:
path('create/', views.QuestionCreateView.as_view(), name='create'),
Next, add the corresponding QuestionCreateView
to polls/views.py
:
from .forms import QuestionCreateForm
class QuestionCreateView(generic.edit.CreateView):
def get(self, request, *args, **kwargs):
context = {
'form': QuestionCreateForm()
}
return render(request, 'polls/create.html', context)
def post(self, request, *args, **kwargs):
form = QuestionCreateForm(request.POST)
if form.is_valid():
question = form.save()
question.save()
return HttpResponseRedirect(
reverse('polls:detail', args=[question.id]))
# else if form is not valid
return render(request, 'polls/create.html', { 'form': form })
Read over this code to make sure that you understand what it is doing:
- When the user loads the "Create" page, show the
polls/create.html
template and pass in a form object. - When the user submits the form, save the question to the database, using the data passed in through the
POST
request. Then, redirect the user to the detail page for their newly created question. - If the form is not valid (e.g. the user entered an invalid publish date), show the form again with any error messages.
For our code to work, we'll have to also write the QuestionCreateForm
class. In the polls
app folder, create a file called forms.py
and in it, add the following code:
from django import forms
from .models import Question
class QuestionCreateForm(forms.ModelForm):
class Meta:
model = Question
fields = ['question_text', 'pub_date']
This indicates that the QuestionCreateForm
should use the 'question_text' and 'pub_date' fields from the Question
model class. This is preferred to writing our own fields here, so that if they were to ever change in the Question
class, they would be updated in the QuestionCreateForm
class as well.
Finally, we'll need a template to display our form. In polls/templates/polls
, create a file called create.html
and give it the following contents:
{% extends 'polls/base.html' %}
{% block content %}
<h3>New Poll</h3>
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">SUBMIT</button>
</form>
{% endblock %}
At this point, we should finally be ready to test our code! Run your server, and see if you can create a few new Questions this way.
Before moving on, let's add one more feature to add some polish to our application: I want to know who the author was of any given question. Since we now have a signup/login flow, this should be pretty straightforward!
First, let's restrict access to the QuestionCreateView
to only users who are logged in. That way, every new Question
object will have a valid author.
In polls/views.py
, modify the QuestionCreateView
class using the LoginRequiredMixin
as follows:
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
class QuestionCreateView(LoginRequiredMixin, generic.edit.CreateView):
login_url = reverse_lazy('login')
def get(self, request, *args, **kwargs):
# ...
def post(self, request, *args, **kwargs):
# ...
To verify that that worked, try going to /polls/create
while logged out. You should be redirected to the login page!
In polls/models.py
, modify the Question
class by adding a ForeignKey field for the question's author:
from django.contrib.auth.models import User
class Question(models.Model):
author = models.ForeignKey(User, on_delete=models.PROTECT, blank=True, null=True, help_text='The user who posted this question.')
After doing this step, make sure to migrate your models so that the app will pick up on the new author
field.
Next, let's modify the QuestionCreateView
's post()
method so that it will save the question's author, in addition to its text and publish date:
class QuestionCreateView(LoginRequiredMixin, generic.edit.CreateView):
# ...
def post(self, request, *args, **kwargs):
form = QuestionCreateForm(request.POST)
if form.is_valid():
question = form.save(commit=False) # don't save the question yet
question.author = request.user
question.save()
return HttpResponseRedirect(
reverse('polls:detail', args=[question.id]))
# else if form is not valid
return render(request, 'polls/create.html', { 'form': form })
Finally, update polls/detail.html
to show the author of a particular question:
{% extends 'polls/base.html' %}
{% block content %}
<h1>{{ question.question_text }}</h1>
{% if question.author %}
<p>Author: {{ question.author.username }}</p>
{% endif %}
<!-- ... voting form goes here ... -->
{% endblock %}
Make sure you test out creating a new Question, and make sure your username shows up!
By now, you're probably wondering: How do we create new choices for our brand-new polls?! We'll cover that in this next part!
In polls/forms.py
, create a new form class for the Choice
model and call it ChoiceCreateForm
. It should be very similar to the QuestionCreateForm
we made previously. Specify that it contains only the field choice_text
.
In polls/views.py
, modify the existing DetailView
class as follows:
from .forms import ChoiceCreateForm
class DetailView(generic.DetailView):
model = Question
template_name = 'polls/detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['choice_form'] = ChoiceCreateForm()
return context
def post(self, request, pk):
form = ChoiceCreateForm(request.POST)
if form.is_valid():
choice = form.save(commit=False)
choice.question = Question.objects.get(pk=pk)
choice.save()
return HttpResponseRedirect(reverse('polls:detail', args=[pk]))
# else if form is not valid
context = {
'choice_form': form,
'question': Question.objects.get(pk=pk)
}
return render(request, 'polls/detail.html', context)
This code does the following:
- If the user loads the question detail page, an extra entry for the
choice_form
is added to the context variables and sent to the template. - When the user submits a new choice, save the choice to the database, using the data passed in as well as the question corresponding to the
pk
from the URL. Then, redirect the user back to the detail page. - If the form is not valid (e.g. the user entered an invalid publish date), show the form again with any error messages.
Finally, modify polls/templates/polls/detail.html
by adding the choice_form
right after the voting form. We only want to show this extra form if the logged-in user is the same as the question's author.
{% if request.user == question.author %}
<!-- choice creation form -->
<h3>Create a new Choice!</h3>
<form method='POST'>
{% csrf_token %}
{{ choice_form.as_p }}
<input type="submit" value="Submit new choice!">
</form>
{% endif %}
Verify that your code is correct by adding some Choice
s! Woo-hoo, you're done!!
Your completed detail page should look like the following:
Submit your finished homework using our class submission site.