TalkList Views

Our TalkLists need a few different views, so let’s look at creating those.

TalkListListView

We should have a view to show all of our lists. In views.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[...]
from braces import views

from . import models

class TalkListListView(views.LoginRequiredMixin, generic.ListView):
    model = models.TalkList

    def get_queryset(self):
        return self.request.user.lists.all()

There’s not really anything fancy about this view other than overriding get_queryset. We want users to only be able to view lists that they own, so this does that for us. We didn’t specify a template, so Django will look for the default one at talks/talklist_list.html.

Template

Since this is an app and the templates are only for this app, I think it’s best to put them in the app. This makes it easier to focus on the files for a specific app and it also makes it easier to make an app reusable elsewhere.

mkdir -p talks/templates/talks
touch talks/templates/talks/talklist_list.html

We need to namespace the template inside of a directory named the same as our app. Open up the template file and add in:

{% extends '_layouts/base.html' %}

{% block title %}Lists | {{ block.super }}{% endblock title %}

{% block headline %}<h1>Your Lists</h1>{% endblock headline %}

{% block content %}
<div class="row">
    <div class="col-sm-6">
        <ul class="list-group">
            {% for object in object_list %}
            <li class="list-group-item">
                <a href="{{ object.get_absolute_url }}">{{ object }}</a>
            </li>
            {% empty %}
            <li class="list-group-item">You have no lists</li>
            {% endfor %}
        </ul>
    </div>
    <div class="col-sm-6">
        <p><a href="#" class="btn">Create a new list</a></p>
    </div>
</div>
{% endblock %}

URL

Now in our urls.py file, we need to update our list_patterns set of patterns.

[...]
url(r'^$', views.TalkListListView.as_view(), name='list'),
url(r'^d/(?P<slug>[-\w]+)/$', views.TalkListDetailView.as_view(),
    name='detail'),
[...]

You’ll notice that we replaced our old default URL (r'^$') with our TalkListListView and put the TalkListDetailView under a new regex that captures a slug. Our model’s get_absolute_url should still work fine.

TalkListDetailView

Let’s build out our actual detail view now. Back in views.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class TalkListDetailView(
    views.LoginRequiredMixin,
    views.PrefetchRelatedMixin,
    generic.DetailView
):
    model = models.TalkList
    prefetch_related = ('talks',)

    def get_queryset(self):
        queryset = super(TalkListDetailView, self).get_queryset()
        queryset = queryset.filter(user=self.request.user)
        return queryset

This mixin from django-braces lets us do a prefetch_related on our queryset to, hopefully, save ourselves some time in the database. Again, we didn’t specify a template so we’ll make one where Django expects.

Template

Create the file talks/templates/talks/talklist_detail.html and add in:

{% extends '_layouts/base.html' %}

{% block title %}{{ object.name }} | Lists | {{ block.super }}{% endblock title %}

{% block headline %}
<h1>{{ object.name }}</h1>
<h2>Your Lists</h2>
{% endblock headline %}

{% block content %}
<div class="row">
    <div class="col-sm-6">
        <p>Talks go here</p>
    </div>

    <div class="col-sm-6">
        <p><a href="{% url 'talks:lists:list' %}">Back to lists</a></p>
    </div>
</div>
{% endblock %}

A pretty standard Django template. We already have the URL so this should be completely wired up now.

RestrictToUserMixin

We had to override get_queryset in both of our views, which is kind of annoying. It would be nice to not have to do that, especially two different ways both times. Let’s write a custom mixin to do this work for us.

class RestrictToUserMixin(views.LoginRequiredMixin):
    def get_queryset(self):
        queryset = super(RestrictToOwnerMixin, self).get_queryset()
        queryset = queryset.filter(user=self.request.user)
        return queryset

This does the same work as our get_queryset in TalkListDetailView. Let’s use it. In both views, add RestrictToUserMixin and take out the views.LoginRequiredMixin from django-braces since our new mixin provides that functionality too. Also remove the overrides of get_queryset from both views.

TalkListCreateView

We want to be able to create a new TalkList, of course, so let’s create a CreateView for that. In views.py, still, add a new class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 [...]
 import .forms

 class TalkListCreateView(
     views.LoginRequiredMixin,
     views.SetHeadlineMixin,
     generic.CreateView
 ):
     form_class = forms.TalkListForm
     headline = 'Create'
     model = models.TalkList

     def form_valid(self, form):
         self.object = form.save(commit=False)
         self.object.user = self.request.user
         self.object.save()
         return super(TalkListCreateView, self).form_valid(form)

This view has a form_class that we haven’t created yet, so we’ll need to do that soon. Also, we override form_valid, which is called when the submitted form passes validation, and in there we create an instance in memory, assign the current user to the model instance, and then save for real and call super() on the method.

This view also brings in the SetHeadlineMixin and provides the headline attribute. We do this because we’ll be using the same template for both create and update views and we don’t want them to both have the same title and headline. This way we can control that from the view instead of having to create new templates all the time.

Let’s create the form now.

TalkListForm

We don’t yet have a talks/forms.py so go ahead and create it with the following content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 from __future__ import absolute_import

 from django import forms

 from crispy_forms.helper import FormHelper
 from crispy_forms.layout import Layout, ButtonHolder, Submit

 from . import models


 class TalkListForm(forms.ModelForm):
     class Meta:
         fields = ('name',)
         model = models.TalkList

     def __init__(self, *args, **kwargs):
         super(TalkListForm, self).__init__(*args, **kwargs)
         self.helper = FormHelper()
         self.helper.layout = Layout(
             'name',
             ButtonHolder(
                 Submit('create', 'Create', css_class='btn-primary')
             )
         )

Nothing really different here from our earlier forms except for line 13 which restricts the fields that the form cares about to just the name field.

URL

As with all of our other views, we need to make an URL for creating lists. In talks/urls.py, add the following line:

url(r'^create/$', views.TalkListCreateView.as_view(), name='create'),

Template

And, again, we didn’t specify a specific template name so Django expects talks/talklist_form.html to exist. Django will use this form for both CreateView and UpdateView views that use the TalkList model unless we tell it otherwise.

{% extends '_layouts/base.html' %}
{% load crispy_forms_tags %}

{% block title %}{{ headline }} | Lists | {{ block.super }}{% endblock title %}

{% block headline %}
<h1>{{ headline }}</h1>
<h2>Your Lists</h2>
{% endblock headline %}

{% block content %}
{% crispy form %}
{% endblock content %}

You can see here were we use the {{ headline }} context item provided by the SetHeadlineMixin. Now users should be able to create new lists.

TalkListUpdateView

Anything we can create, we should be able to update so let’s create a TalkListUpdateView in views.py.

class TalkListUpdateView(
    RestrictToOwnerMixin,
    views.LoginRequiredMixin,
    views.SetHeadlineMixin,
    generic.UpdateView
):
    form_class = forms.TalkListForm
    headline = 'Update'
    model = models.TalkList

There isn’t anything in this view that we haven’t covered already. All that’s left for it is to create the URL pattern.

URL

You should be getting the hang of this by now, so let’s just add this line to our urls.py:

url(r'^update/(?P<slug>[-\w]+)/$', views.TalkListUpdateView.as_view(),
    name='update'),

Global template changes

It’s great that we’ve created all of these views but now there’s no easy way to get to your views. Let’s fix that by adding the following into _layouts/base.html next to our other navigation items inside the {% else %} clause:

<a href="{% url 'talks:lists:list' %}" class="btn btn-primary navbar-btn">Talk lists</a>

App template changes

Our talks/talklist_list.html template should have a link to the TalkListCreateView so let’s add that into the sidebar:

<p><a href="{% url 'talks:lists:create' %}" class="btn">Create a new list</a></p>

DeleteView?

So what about a DeleteView for TalkList? I didn’t make one for this example but it shouldn’t be too hard of an exercise for the reader. Be sure to restrict the queryset to the logged-in user.