Writing your first Fastview app, part 2
Digging into the ViewGroup
Views are added to a ViewGroup by adding a class-based view to the class with a name in
the format <action>_view
, where the action part of the name is how the viewgroup and
other views will refer to it.
For example, a ViewGroup with detail_view = generics.DetailView
will give it the
action name detail
, it can be linked to as {% url "mygroup:detail" ... %}
etc.
Fastview provides generic class-based views (CBVs) which wrap Django’s generic CBVs with a bit of extra logic for permission handling and other functionality.
The ModelViewGroup defines 5 actions using Fastview’s generic views:
from fastview.viewgroups import ViewGroup
from fastview.views import generic
class ModelViewGroup(ViewGroup):
index_view = generic.ListView
detail_view = generic.DetailView
create_view = generic.CreateView
update_view = generic.UpdateView
delete_view = generic.DeleteView
This makes it easy to completely change the functionality of these views, or add or remove them.
Configure the list view
Fastview’s ListView provides a lot more functionality than a standard CBV ListView. At the moment we’re just showing each quiz question - lets show some extra columns and add some filtering.
Lets add a ListView
and use it for the index view on the PollViewGroup
:
# Remove::
# from django.views import generic
from fastview.views import generic
class ListView(generic.ListView):
fields = ["question_text", "pub_date", "was_published_recently"]
filters = ["pub_date"]
paginate_by = 25
class PollViewGroup(ModelViewGroup):
# ...
index_view = ListView
Note how we didn’t mention the model or template name in our View - it will get the model from the ViewGroup, and will be using Fastview’s default index templates (although you can override them both).
We’ve done five things here:
We’ve swapped
django.views.generic
forfastview.views.generic
. These are subclasses of Django’s, so will be compatible.We’ve added
paginate_by
to activate the Django CBV’s pagination. Fastview will detect this and add a set of pagination links.The
fields
attribute defines the list of fields to use when displaying each object. We’re showing a mix of model fields and model methods. You will notice that Fastview has picked up the Django admin attributes set onwas_published_recently
by@admin.display
.The
filters
attribute defines the list of filters to present to the user. Again, how the filter is shown to the user has a functional default but is easy to override.We’ve replaced the old IndexView overriden the default
index_view
with our own CBV.
Lets look at the list view again and see what has changed:
We’ve now got a new filter panel on the right-hand side, and a list of pagination links
at the bottom. You’ll see that the Fastview has detected that pub_date
is a date
field, so it has picked a date-based filter.
Again, Tagulous’s default styles has some practical values to make it function, but lets add some basic styles:
Note
TODO: Move this into fastview.css
/* Fastview list filter (simplified) */
div.fastview-list-filter {
margin: 0 1rem;
padding: 0;
}
div.fastview-list-filter ul {
list-style: none;
margin: 0;
padding: 0;
}
/* Fastview pagination */
div.fastview-pagination ul {
list-style: none;
margin: 1rem;
padding: 0;
}
div.fastview-pagination li {
display: inline-block;
margin: 0 0.5rem;
}
Configure with a dict
Creating a view class can be useful if you want to override its methods, but it bloats your views.py if you just want to customise an attribute or two.
Fastview has a shortcut - instead of setting the view on index_view
, lets delete the
ListView
we defined and redefine the view with a dict:
from fastview.views import generic
class PollViewGroup(ModelViewGroup):
model = Question
permission = permissions.Public()
index_view = {
"fields": ["question_text", "pub_date", "was_published_recently"],
"filters": ["pub_date"],
"paginate_by": 25,
}
This will configure the view class as if you had subclassed it yourself with those attributes.
Adding a new view
Our list index is nice and functional now, but it doesn’t look that interesting for
users. Lets convert our old IndexView
from the Django tutorial. Originally it was:
class IndexView(generic.ListView):
template_name = "polls/index.html"
context_object_name = "latest_question_list"
def get_queryset(self):
"""Return the last five published questions."""
return Question.objects.filter(pub_date__lte=timezone.now()).order_by(
"-pub_date"
)[:5]
Lets change it to work more closely with Fastview. Instead of querying the model
directly, we’ll call super().get_queryset()
so we can benefit from Fastview’s
standard permission checks and filtering. We’ll then move our list view and add our
index back in its place:
class IndexView(generic.ListView):
template_name = "polls/index.html"
context_annotated_name = "latest_question_list"
def get_queryset(self):
"""Return the last five published questions."""
qs = super().get_queryset()
return qs.filter(pub_date__lte=timezone.now()).order_by(
"-pub_date"
)[:5]
class PollViewGroup(ModelViewGroup):
# ...
index_view = IndexView
list_view = generic.ListView.config(
fields=["question_text", "pub_date", "was_published_recently"],
filters=["pub_date"],
paginate_by=25,
)
You’ll see we have also replaced the dict view configuration with a call to
ListView.config(..)
. The dict is actually a shortcut to calling .config(...)
on
a Fastview generic view, so you can define and customise new views on the fly without
the need to explicitly subclass it yourself.
We also changed context_object_name
to context_annotated_name
- we’ll come
back to that when we write the template.
Custom ListView Template
A Fastview ListView
provides the same context to a template that a Django
ListView
does, but it also adds several variables for Fastview to do its work.
The main addition is an annotated object list (annotated_object_list
by default,
controlled by the context_annotated_name
property), which wraps each object so you
can iterate over the values in the order set by fields
, with additional attributes
for action permission checks and URLs to related views.
You can take a look at the default list template to see how to iterate over the fields, but we want something more designed, so we’ll use the annotated objects for permission checks, and then access the original object directly:
{% extends "base.html" %}
{% block content %}
{% if latest_question_list %}
{# We'll iterate over the annotated object list #}
{% for annotated_question in latest_question_list %}
<div class="question">
{# Access the Question instance as annotated_question.object #}
{% with question=annotated_question.object %}
<h2>{{ question.question_text }}</h2>
<p>{{ question.pub_date|date:"D d M Y" }}</p>
{% endwith %}
{# Use the permission annotations to check access #}
{% if annotation.can_vote %}
<a href="{{ annotation.get_vote_url }}">Vote</A>
{% else %}
<a href="{{ annotation.get_detail_url }}">See the question</a>
{% endif %}
</div>
{% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}
{% endblock %}
Every view defined in the viewgroup which operates on an object gets its own can_X
permission check, and get_X_url
to return the URL to the view.
Here we call get_detail_url
, which links to the detail view, but we also check the
can_vote
and call get_vote_url
, for a view which doesn’t exist yet. We’ll add
that and the other views in `part3`_.
Going further
This covers the basics of working with the ListView
and its options, but for a
deeper dive into the functionality available and how to customise the defaults, you may
like to look at:
listview
generic_views/annotated
When you’re ready, lets move on to part 3 of this tutorial where we’ll add the missing views and see how to work with forms and inlines.