ModelView¶
We’ll start with simple object list and object detail pages, explaining many provided tools along the way. Next, this guide covers the CRUD part of Towel, talk about batch processing a bit and end up with explaining a few components in more detail.
Warning
Please note that Towel’s ModelView could be considered similar to Django’s own generic views. However, they do not have the same purpose and software design: Django’s generic views use one class per view, and every instance only processes one request. Towel’s ModelView is more similar to Django’s admin site in that one instance is responsible for many URLs and handles many requests. You have to take care not to modify ModelView itself during request processing, because this will not be thread-safe.
Preparing your models, views and URLconfs for ModelView¶
ModelView has a strong way of how Django-based web applications should be written. The rigid structure is necessary to build a well-integrated toolset which will bring you a long way towards successful completion of your project. If you do not like the design decisions made, ModelView offers hooks to customize the behavior, but that’s not covered in this guide.
For this guide, we assume the following model structure and relationships:
from django.db import models
class Publisher(models.Model):
name = models.CharField(max_length=100)
address = models.TextField()
class Author(models.Model):
name = models.CharField(max_length=100)
date_of_birth = models.DateField(blank=True, null=True)
class Book(models.Model):
title = models.CharField(max_length=100)
topic = models.CharField(max_length=100)
authors = models.ManyToManyField(Author)
published_on = models.DateField()
publisher = models.ForeignKey(Publisher)
ModelView works with an URL structure similar to the following:
/books/
/books/add/
/books/<pk>/
/books/<pk>/edit/
/books/<pk>/delete/
The regular expression used to match the detail page (here <pk>) can be
customized. If you’d rather match on the slug, on a combination of
several fields (separated by dashes or slashes, whatever you want) or on
something else, you can do this by modifying
urlconf_detail_re
. You only have to make sure that
get_object()
will know what to do with the extracted
parameters.
If you want to use the primary key-based URL configuration, you do not
need to add a get_absolute_url()
method to
your model, because ModelView
will add one itself. It isn’t
considered good practice to put primary keys on the web for everyone to see
but it might be okay for your use case.
The main ModelView
class¶
- class towel.modelview.ModelView(model[, ...])¶
The first and only required argument when instantiating a model view is the Django model. Additional keyword arguments may be used to override attribute values of the model view class. It is not allowed to pass keyword arguments which do not exist as attributes on the class already.
- urlconf_detail_re¶
The regular expression used for detail pages. Defaults to a regular expression which only accepts a numeric primary key.
- paginate_by¶
Objects per page for list views. Defaults to
None
which means that all objects are shown on one page (usually a bad idea).
- pagination_all_allowed¶
Pagination can be deactivated by passing
?all=1
in the URL. If you expect having lots of objects in the table showing all on one page can lead to a very slow and big page being shown. Set this attribute toFalse
to disallow this behavior.
- paginator_class¶
Paginator class which should have the same interface as
django.core.paginator.Paginator
. Defaults totowel.paginator.Paginator
which is almost the same as Django’s, but offers additional methods for outputting Digg-style pagination links.
- template_object_name¶
The name used for the instance in detail and edit views. Defaults to
object
.
- template_object_list_name¶
The name used for instances in list views. Defaults to
object_list
.
- base_template¶
The template which all standard modelview templates extend. Defaults to
base.html
.
- form_class¶
The form class used to create and update models. The method
get_form()
returns this value instead of invokingmodelform_factory()
if it is set. Defaults toNone
.
- search_form¶
The search form class to use in list views. Should be a subclass of
towel.forms.SearchForm
. Defaults toNone
, which deactivates search form handling.
- search_form_everywhere¶
Whether a search form instance should be added to all views, not only to list views. Useful if the search form is shown on detail pages as well.
- batch_form¶
The batch form class used for batch editing in list views. Should be a subclass of
towel.forms.BatchForm
. Defaults toNone
.
- default_messages¶
A set of default messages for various success and error conditions. You should not modify this dictionary, but instead override messages by adding them to
custom_messages
below. The current set of messages is:object_created
adding_denied
object_updated
editing_denied
object_deleted
deletion_denied
deletion_denied_related
Note that by modifying this dictionary you are modifying it for all model view instances!
- custom_messages¶
A set of custom messages for custom actions or for overriding messages from
custom_messages
.Note that by modifying this dictionary you are modifying it for all model view instances! If you want to override a few messages only for a particular model view instance, you have to set this attribute to a new dictionary instance, not update the existing dictionary.
- view_decorator(self, func)¶
- crud_view_decorator(self, func)¶
The default implementation of
get_urls()
uses those two methods to decorate all views, the former for list and detail views, the latter for add, edit and delete views.
Models and querysets¶
- towel.modelview.ModelView.get_query_set(self, request, \*args, \*\*kwargs)¶
This method should return a queryset with all objects this modelview is allowed to see. If a certain user should only ever see a subset of all objects, add the permission checking here. Example:
class UserModelView(ModelView): def get_query_set(self, request, *args, **kwargs): return self.model.objects.filter(created_by=request.user)
- towel.modelview.ModelView.get_object(self, request, \*args, \*\*kwargs)¶
Returns a single object for the query parameters passed as
args
andkwargs
or raises aObjectDoesNotExist
exception. The default implementation passes all args and kwargs to aget()
call, which means that all parameters extracted by theurlconf_detail_re
regular expression should uniquely identify the object in the queryset returned byget_query_set()
above.
- towel.modelview.ModelView.get_object_or_404(self, request, \*args, \*\*kwargs)¶
Wraps
get_object()
, but raises aHttp404
instead of aObjectDoesNotExist
.
Object lists¶
Towel`s object lists are handled by list_view()
. By default,
all objects are shown on one page but this can be modified through
paginate_by
. The following code puts a paginated list of
books at /books/
:
from myapp.models import Book
from towel.modelview import ModelView
class BookModelView(ModelView):
paginate_by = 20
book_views = BookModelView(Book)
urlpatterns = patterns('',
url(r'^books/', include(book_views.urls)),
)
This can even be written shorter if you do not want to override any ModelView methods:
from myapp.models import Book
from towel.modelview import ModelView
urlpatterns = patterns('',
url(r'^books/', include(ModelView(Book, paginate_by=20).urls)),
)
The model instances are passed as object_list
into the template by default.
This can be customized by setting template_object_list_name
to a different value.
The list_view()
method does not contain much code, and simply defers to
other methods who do most of the grunt-work. Those methods are shortly explained
here.
- towel.modelview.ModelView.list_view(self, request)¶
Main entry point for object lists, calls all other methods.
- towel.modelview.ModelView.handle_search_form(self, request, ctx, queryset=None)¶
- towel.modelview.ModelView.handle_batch_form(self, request, ctx, queryset)¶
These methods are discussed later, under List Searchable and Batch processing.
- towel.modelview.ModelView.paginate_object_list(self, request, queryset, paginate_by=10)¶
If
paginate_by``is given paginates the object list using the ``page
GET parameter. Pagination can be switched off by passingall=1
in the GET request. If you have lots of objects and want to disable theall=1
parameter, setpagination_all_allowed
toFalse
.
- towel.modelview.ModelView.render_list(self, request, context)¶
The rendering of object lists is done inside
render_list
. This method callsget_template
to assemble a list of templates to try, andget_context
to build the context for rendering the final template. The templates tried are as follows:<app_label>/<model_name>_list.html
(in our case,myapp/book_list.html
)modelview/object_list.html
The additional variables passed into the context are documented in Standard context variables.
List Searchable¶
Please refer to the Search and Filter page for information about filtering lists.
Object detail pages¶
Object detail pages are handled by detail_view()
. All parameters
captured in the urlconf_detail_re
regex are passed on to
get_object_or_404()
, which passes them to get_object()
.
get_object()
first calls get_query_set()
, and tries finding
a model thereafter.
The rendering is handled by render_detail()
; the templates tried are
<app_label>/<model_name>_detail.html
(in our case,myapp/book_detail.html
)modelview/object_detail.html
The model instance is passed as object
into the template by default. This
can be customized by setting template_object_name
to a different value.
Adding and updating objects¶
Towel offers several facilities to make it easier to build and process complex forms composed of forms and formsets. The code paths for adding and updating objects are shared for a big part.
add_view
and edit_view
are called first. They defer most of their work
to helper methods.
- towel.modelview.ModelView.add_view(self, request)¶
add_view
does not accept any arguments.
- towel.modelview.ModelView.edit_view(self, request, \*args, \*\*kwargs)¶
args
andkwargs
are passed as they are directly intoget_object()
.
- towel.modelview.ModelView.process_form(self, request, intance=None, change=None)¶
These are the common bits of
add_view()
andedit_view()
.
- towel.modelview.ModelView.get_form(self, request, instance=None, change=None, \*\*kwargs)¶
Return a Django form class. The default implementation returns the result of calling
modelform_factory()
. Keyword arguments are forwarded to the factory invocation.
- towel.modelview.ModelView.get_form_instance(self, request, form_class, instance=None, change=None, \*\*kwargs)¶
Instantiate the form, for the given instance in the editing case.
The arguments passed to the form class when instantiating are determined by
extend_args_if_post
and**kwargs
.
- towel.modelview.ModelView.extend_args_if_post(self, request, args)¶
Inserts
request.POST
andrequest.FILES
at the beginning ofargs
ifrequest.method
isPOST
.
- towel.modelview.ModelView.get_formset_instances(self, request, instance=None, change=None, \*\*kwargs)¶
Returns an empty
dict
by default. Construct your formsets if you want any in this method:BookFormSet = inlineformset_factory(Publisher, Book) class PublisherModelView(ModelView): def get_formset_instances(self, request, instance=None, change=None, **kwargs): args = self.extend_args_if_post(request, []) kwargs.setdefault('instance', instance) return { 'books': BookFormSet(prefix='books', *args, **kwargs), }
- towel.modelview.ModelView.save_form(self, request, form, change)¶
Return an unsaved instance when editing an object.
change
isTrue
if editing an object.
- towel.modelview.ModelView.save_model(self, request, instance, form, change)¶
Save the instance to the database.
change
isTrue
if editing an object.
- towel.modelview.ModelView.save_formsets(self, request, form, formsets, change)¶
Iterates through the
formsets
dict
, callingsave_formset
on each.
- towel.modelview.ModelView.save_formset(self, request, form, formset, change)¶
Actually saves the formset instances.
- towel.modelview.ModelView.post_save(self, request, form, formsets, change)¶
Hook for adding custom processing after forms, formsets and m2m relations have been saved. Does nothing by default.
- towel.modelview.ModelView.render_form(self, request, context, change)¶
Offloads work to
get_template
,get_context
andrender_to_response
. The templates tried when rendering are:<app_label>/<model_name>_form.html
modelview/object_form.html
- towel.modelview.ModelView.response_add()¶
- towel.modelview.ModelView.response_edit()¶
They add a message using the
django.contrib.messages
framework and redirect the user to the appropriate place, being the detail page of the edited object or the editing form if_continue
is contained in the POST request.
Object deletion¶
Object deletion through ModelView is forbidden by default as a safety measure. However, it is very easy to allow deletion globally:
class AuthorModelView(ModelView):
def deletion_allowed(self, request, instance):
return True
If you wanted to allow deletion only for the creator, you could use something like this:
class AuthorModelView(ModelView):
def deletion_allowed(self, request, instance):
# Our author model does not have a created_by field, therefore this
# does not work.
return request.user == instance.created_by
Often, you want to allow deletion, but only if no related objects are affected by the deletion. ModelView offers a helper to do that:
class PublisherModelView(ModelView):
def deletion_allowed(self, request, instance):
return self.deletion_allowed_if_only(request, instance, [Publisher])
If there are any books in our system published by the given publisher instance, the deletion would not be allowed. If there are no related objects for this instance, the user is asked whether he really wants to delete the object. If he confirms, the instance is or the instances are deleted for good, depending on whether there are related objects or not.
Deletion of inline formset instances¶
Django’s inline formsets are very convenient to edit a set of related objects on one page. When deletion of inline objects is enabled, it’s much too easy to lose related data because of Django’s cascaded deletion behavior. Towel offers helpers to allow circumventing Django’s inline formset deletion behavior.
Note
The problem is that formset.save(commit=False)
deletes objects marked
for deletion right away even though commit=False
might be interpreted
as not touching the database yet.
The models edited through inline formsets have to be changed a bit:
from django.db import models
from towel import deletion
class MyModel(deletion.Model):
field = models.CharField(...) # whatever
deletion.Model
only consists of a customized Model.delete
method
which does not delete the model under certain circumstances. See the
Deletion API documentation if you need to know more.
Next, you have to override save_formsets
:
class MyModelView(modelview.ModelView):
def get_formset_instances(self, request, instance=None, change=None, **kwargs):
args = self.extend_args_if_post(request, [])
kwargs['instance'] = instance
return {
'mymodels': InlineFormSet(*args, **kwargs),
}
def save_formsets(self, request, form, formsets, change):
# Only delete MyModel instances if there are no related objects
# attached to them
self.save_formset_deletion_allowed_if_only(
request, form, formsets['mymodels'], change, [MyModel])
Warning
save_formset_deletion_allowed_if_only
calls save_formset
do actually
save the formset. If you need this customized behavior, you must not call
save_formset_deletion_allowed_if_only
in save_formset
or you’ll get
infinite recursion.
Standard context variables¶
The following variables are always added to the context:
verbose_name
verbose_name_plural
list_url
add_url
base_template
search_form
ifsearch_form_everywhere
isTrue
RequestContext
is used, therefore all configured
context processors are executed too.
Permissions¶
get_urls()
assumes that there are two groups of users with
potentially differing permissions: Those who are only allowed to view and those
who may add, change or update objects.
To restrict viewing to authenticated users and editing to managers, you could do the following:
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import login_required
book_views = BookModelView(Book,
search_form=BookSearchForm,
paginate_by=20,
view_decorator=login_required,
crud_view_decorator=staff_member_required,
)
If crud_view_decorator()
is not provided, it defaults to
view_decorator()
, which defaults to returning the function as-is.
This means that by default, you do not get any view decorators.
Additionally, ModelView offers the following hooks for customizing permissions:
- towel.modelview.ModelView.adding_allowed(self, request)¶
- towel.modelview.ModelView.editing_allowed(self, request, instance)¶
Return
True
by default.
- towel.modelview.ModelView.deletion_allowed(self, request, instance)¶
Was already discussed under Object deletion. Returns
False
by default.
Batch processing¶
Suppose you want to change the publisher for a selection of books. You could do this by editing each of them by hand, or by thinking earlier and doing this:
from django import forms
from django.contrib import messages
from towel import forms as towel_forms
from myapp.models import Book, Publisher
class BookBatchForm(towel_forms.BatchForm):
publisher = forms.ModelChoiceField(Publisher.objects.all(), required=False)
formfield_callback = towel_forms.towel_formfield_callback
def _context(self, batch_queryset):
data = self.cleaned_data
if data.get('publisher'):
messages.success(request, 'Updated %s books.' % (
batch_queryset.update(publisher=data.get('publisher')),
))
return {
'batch_items': batch_queryset,
}
Activate the batch form like this:
book_views = BookModelView(Book,
batch_form=BookBatchForm,
search_form=BookSearchForm,
paginate_by=20,
)
If you have to return a response from the batch form (f.e. because you want to
generate sales reports for a selection of books), you can return a response in
_context
using the special-cased key response
:
def _context(self, batch_queryset):
# [...]
return {
'response': HttpResponse(your_report,
content_type='application/pdf'),
}