Talks model¶
We need a model for our actual talks which will belong to a list. Eventually we’ll want ratings and notes and such, but let’s start with a simple model.
Model, take one¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | from django.core.urlresolvers import reverse
from django.db import models
from django.template.defaultfilters import slugify
from django.utils.timezone import utc
class Talk(models.Model):
ROOM_CHOICES = (
('517D', '517D'),
('517C', '517C'),
('517AB', '517AB'),
('520', '520'),
('710A', '710A')
)
talk_list = models.ForeignKey(TalkList, related_name='talks')
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, blank=True)
when = models.DateTimeField()
room = models.CharField(max_length=10, choices=ROOM_CHOICES)
host = models.CharField(max_length=255)
class Meta:
ordering = ('when', 'room')
unique_together = ('talk_list', 'name')
def __unicode__(self):
return self.name
def save(self, *args, **kwargs):
self.slug = slugify(self.name)
super(Talk, self).save(*args, **kwargs)
|
Like with our TalkList
model, we want to slugify
the name whenever we save an instance. We also provide a tuple of two-tuples of choices for our room
field, which makes sure that whatever talks get entered all have a valid room and saves users the trouble of having to type the room number in every time.
Also, we want default ordering of the model to be by when the talk happens, in ascending order, and then by room number.
Migration¶
Since we’ve added a model, we need to create and apply a migration.
python manage.py schemamigration --auto talks
python manage.py migrate talks
Form¶
We should create a form for creating talks. In forms.py
, let’s add TalkForm
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | import datetime
from django.core.exceptions import ValidationError
from django.utils.timezone import utc
[...]
class TalkForm(forms.ModelForm):
class Meta:
fields = ('name', 'host', 'when', 'room')
model = models.Talk
def __init__(self, *args, **kwargs):
super(TalkForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
'name',
'host',
'when',
'room',
ButtonHolder(
Submit('add', 'Add', css_class='btn-primary')
)
)
def clean_when(self):
when = self.cleaned_data.get('when')
pycon_start = datetime.datetime(2014, 4, 11).replace(tzinfo=utc)
pycon_end = datetime.datetime(2014, 4, 13, 17).replace(tzinfo=utc)
if not pycon_start < when < pycon_end:
raise ValidationError("'when' is outside of PyCon.")
return when
|
This ModelForm
should look pretty similar to the other ones we’ve created so far, but it adds a new method, clean_when
, which is called during the validation process and only on the when
field.
We get the current value of when
, then check it against two datetime
objects that represent the start and end dates of PyCon. So long as our submitted date is between those two datetime
s, we’re happy.
Update TalkListDetailView
¶
So now we need to be able to add a Talk
to a TalkList
. If you noticed on the TalkForm
, we don’t pass through the talk_list
field because we’ll do this in the view. But we aren’t going to create a custom view for this, even though we could. We’ll just extend the TalkListDetailView
to handle this new bit of functionality.
So, back in views.py
, let’s update TalkListDetailView
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | [...]
from django.shortcuts import redirect
[...]
class TalkListDetailView(
RestrictToUserMixin,
views.PrefetchRelatedMixin,
generic.DetailView
):
form_class = forms.TalkForm
http_method_names = ['get', 'post']
model = models.TalkList
prefetch_related = ('talks',)
def get_context_data(self, **kwargs):
context = super(TalkListDetailView, self).get_context_data(**kwargs)
context.update({'form': self.form_class(self.request.POST or None)})
return context
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
obj = self.get_object()
talk = form.save(commit=False)
talk.talk_list = obj
talk.save()
else:
return self.get(request, *args, **kwargs)
return redirect(obj)
|
So, what are we doing here? We set a form_class
attribute on the view, and, if this was a FormView
derivative, it would know what to do with that, but it’s not so we’re really just providing this for our own convenience.
Then, in get_context_data
, we set up the normal context dictionary before adding a self.request.POST or None
-seeded instance of the form to the dict.
And, finally, in post
, which is now allowed by the http_method_names
attribute, we build a new instance of the form, check to see if it’s valid, and save it if it is, first adding the TalkList
to the Talk
.
Template¶
Now we need to update the template for the TalkListDetailView
, so open up talks/templates/talks/talklist_detail.html
and add the following:
{% load crispy_forms_tags %}
[...]
<div class="panel panel-default">
<div class="panel-heading">
<h1 class="panel-title">Add a new talk</h1>
</div>
<div class="panel-body">
{% crispy form %}
</div>
</div>
The .panel
div goes in the sidebar near the “Back to lists” and “Edit this list” links.
We’re not doing anything interesting in this new snippet, just having django-crispy-forms
render the form for us.
TalkListListView
¶
Now that we can add talks to lists, we should probably show a count of the talks that a list has.
Pop open talks/templates/talks/talklist_list.html
and, where we have a link to each TalkList
, add:
<span class="badge">{{ object.talks.count }}</span>
Now, while this works, this adds an extra query for every TalkList
our user has. If someone has a ton of lists, this could get very expensive.
Note
This is normally where I’d add in django-debug-toolbar
and suggest you do the same. Install it with pip
and follow the instructions online.
In views.py
, let’s fix this extra query.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | [...]
from django.db.models import Count
[...]
class TalkListListView(
RestrictToUserMixin,
generic.ListView
):
model = models.TalkList
def get_queryset(self):
queryset = super(TalkListListView, self).get_queryset()
queryset = queryset.annotate(talk_count=Count('talks'))
return queryset
|
We’re using Django’s Count
annotation to add a talk_count
attribute to each instance in the queryset, which means all of the counting is done by our database and we don’t ever have to touch the Talk
related items.
Go back to the template and change {{ object.talks.count }}
to {{ object.talk_count }}
.
Show the talks on a list¶
We aren’t currently showing the talks that belong to a list, so let’s fix that.
In talks/templates/talks/talklist_detail.html
, the leftside column should contain:
<div class="col-sm-6">
{% for talk in object.talks.all %}
{% include 'talks/_talk.html' %}
{% endfor %}
</div>
This includes a new template, talks/templates/talks/_talk.html
for every talk on a list. Here’s that new template:
1 2 3 4 5 6 7 8 9 10 | <div class="panel panel-info">
<div class="panel-heading">
<a class="close" aria-hidden="true" class="pull-right" href="#">×</a>
<h1 class="panel-title"><a href="{{ talk.get_absolute_url }}">{{ talk.name }}</a></h1>
</div>
<div class="panel-body">
<p class="bg-primary" style="padding: 15px"><strong>{{ talk.when }}</strong> in <strong>{{ talk.room }}</strong></p>
<p>by <strong>{{ talk.host }}</strong>.</p>
</div>
</div>
|
TalkListRemoveTalkView
¶
Since we can add talks to a list, we should be able to remove them. Let’s make a new view in views.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | from django.contrib import messages
class TalkListRemoveTalkView(
views.LoginRequiredView,
generic.RedirectView
):
model = models.Talk
def get_redirect_url(self, *args, **kwargs):
return self.talklist.get_absolute_url()
def get_object(self, pk, talklist_pk):
try:
talk = self.model.objects.get(
pk=pk,
talk_list_id=talklist_pk,
talk_list__user=self.request.user
)
except models.Talk.DoesNotExist:
raise Http404
else:
return talk
def get(self, request, *args, **kwargs):
self.object = self.get_object(kwargs.get('pk'),
kwargs.get('talklist_pk'))
self.talklist = self.object.talk_list
messages.success(
request,
u'{0.name} was removed from {1.name}'.format(
self.object, self.talklist))
self.object.delete()
return super(TalkListRemoveTalkView, self).get(request, *args,
**kwargs)
|
Since we’re using a RedirectView
, we need to supply a redirect_url
for the view to send requests to once the view is finished, and since we need it to be based off of a related object that we won’t know until the view is resolved, we supply this through the get_redirect_url
method.
Normally RedirectView
s don’t care about models or querysets, but we provide get_object
on our view which expects the pk
and talklist_pk
that will come in through our URL (when we build it in a moment). We, again, check to make sure the current user owns the list and that the talk belongs to the list.
And, we’ve overridden get
completely to make this all work. get
gets our object with the URL kwargs
, grabs the TalkList
instance for later use, gives the user a message, and then actually deletes the Talk
.
URL¶
Like all views, our new one needs a URL.
url(r'^remove/(?P<talklist_pk>\d+)/(?P<pk>\d+)/$',
views.TalkListRemoveTalkView.as_view(),
name='remove_talk'),
We add this to list_patterns
, still, and then update talks/templates/talks/_talk.html
, replacing the '#'
in the .close
link with {% url 'talks:lists:remove_talk` talk.talk_list_id talk.id %}
. We can now remove talks from a list.
TalkListScheduleView
¶
The views we’ve been creating are handy but aren’t necessarily the cleanest for looking at, printing off, or keeping up on a phone, so let’s make a new view that expressly aimed at those purposes.
In views.py
, we’re going to add:
class TalkListScheduleView(
RestrictToUserMixin,
views.PrefetchRelatedMixin,
generic.DetailView
):
model = models.TalkList
prefetch_related = ('talks',)
template_name = 'talks/schedule.html'
This view is very similar to our TalkListDetailView
but has a specific template, no added form, and no post
method. To round it out, let’s set up the URL and the template.
URL¶
url(r'^s/(?P<slug>[-\w]+)/$', views.TalkListScheduleView.as_view(),
name='schedule'),
Almost identical to the URL for our TalkListDetailView
, it just changes the d
to an s
.
Note
This could be done entirely through arguments to the view from the url or querystring, but that would required more conditional logic in our view and/or our template, which I think is, in this case, a completely unnecessary complication.
Template¶
Our new template file is, of course, talks/templates/talks/schedule.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | {% 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 %}
{% regroup object.talks.all by when|date:"Y/m/d" as day_list %}
{% for day in day_list %}
<div class="panel panel-default">
<div class="panel-heading">
<h1 class="panel-title">{{ day.grouper }}</h1>
</div>
<table class="table">
<thead>
<tr>
<th>Room</th>
<th>Time</th>
<th>Talk</th>
<th>Presenter(s)</th>
</tr>
</thead>
<tbody>
{% for talk in day.list %}
<tr>
<td>{{ talk.room }}</td>
<td>{{ talk.when|date:"h:i A" }}</td>
<td>{{ talk.name }}</td>
<td>{{ talk.host }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
{% endblock %}
|
The special thing about this template is how we regroup the talks. We want them grouped and sorted by their dates. Using {% regroup %}
gives us this ability and a new object that is a list of dictionaries with two keys, grouper
which holds our day; and list
, which is a list of the instances in that group.