Towel - Keeping you DRY since 2010¶
Towel is a collection of tools which make your life easier if you are building a web application using Django. It contains helpers and templates for creating paginated, searchable object lists, CRUD functionality helping you safely and easily create and update objects, and using Django’s own proofed machinery to see what happens when you want to safely delete objects.
Contents¶
Installation instructions¶
This document describes the steps needed to get Towel up and running.
Towel is based on Django, so you need a working Django installation first. Towel is mainly developed using the newest release of Django, but should work with Django 1.4 up to the upcoming 1.7 and with Python 2.7 and 3.3. Towel does not currently support Python 3.2 but patches adding support are welcome.
Towel can be installed using the following command:
$ pip install Towel
Towel has no dependencies apart from Django.
You should add towel
to INSTALLED_APPS
if you want to use
the bundled templates and template tags. This isn’t strictly
required though.
Towel API¶
towel.api
is a set of classes which facilitate building a RESTful
API. In contrast to other, well known projects such as
django-piston and
tastypie it does not try to cover all HTTP
verbs out of the box, and does not come with as many configuration knobs
and classes for everything, and tries staying small and simple instead.
The API consists of the following classes and methods, which are explained in more depth further down this page:
API
: A collection of resources.Resource
: A single resource which exposes a Django model instance.Serializer
: The API response serializer, responsible for content type negotiation and creation ofHttpResponse
instances.RequestParser
: Understands requests in various formats (JSON, urlencoded, etc.) and handles the differences.APIException
: An exception which can be raised deep down in the API / resource machinery and will be converted into a nicely formatted response in the requested content format.Objects
andPage
: Containers for objects related to a particular resource and / or URI. They are returned by the methodResource.objects()
.api_reverse()
: Helper for reversing URLs inside a particular API instance.serialize_model_instance()
: The default Django model serializer.querystring()
: Helper for constructing querystrings.
The API
class¶
-
class
towel.api.
API
(name, decorators=[csrf_exempt])¶ This class acts as a collection of resources. The arguments are:
name
: The name of this API. If you don’t know what to use here, simply use'v1'
.decorators
: A list of decorators which should be applied to the root API view and to all resources (if you don’t override it upon resource registration). The list of decorators is applied in reverse, which means that you should follow the same order as if you were using the@decorator
notation. It’s recommended to always usecsrf_exempt()
here, otherwise API requests other than GET, HEAD, OPTIONS and TRACE (the HTTP verbs defined as safe by RFC2616) will have to include a valid CSRF middleware token.
Example:
api_v1 = API('v1')
-
name
¶ The name of this API.
-
decorators
¶ The decorators passed upon initialization.
-
resources
¶ A list of dictionaries holding resource configuration.
-
serializers
¶ A dictionary mapping models to serialization functions. If a model does not exist inside this dictionary, the default serialization function
serialize_model_instance()
is used.
-
urls
¶ This property returns a URL pattern instance suitable for including inside your main URLconf:
from .views import api_v1 urlpatterns = patterns('', url(r'^api/v1/', include(api_v1.urls)), )
-
register
(self, model, view_class=None, canonical=True, decorators=None, prefix=None, view_init=None, serializer=None)¶ Resources are normally not created by hand. This method should be used instead. The arguments are:
model
: The Django model used in this resource.view_class
: The resource view class used, defaults toResource
.canonical
: Whether this resource is the canonical location of the model in this API. Allows registering the same model several times in the API (only one location should be the canonical location!)decorators
: A list of decorators which should be applied to the view. Function decorators only, method decorators aren’t supported. The list is applied in reverse, the order is therefore the same as with the@decorator
notation. If unset, the set of decorators is determined from the API initialization. Pass an empty list if you want no decorators at all.prefix
: The prefix for this model, defaults to the model name in lowercase. You should include a caret and a trailing slash if you specify this yourself (prefix=r'^library/'
).view_init
: Python dictionary which contains keyword arguments used during the instantiation of theview_class
.serializer
: Function which takes a model instance, the API instance and additional keyword arguments (accept**kwargs
for forward compatibility) and returns the serialized representation as a Python dictionary.
-
serialize_instance
(self, instance, **kwargs)¶ Returns a serialized version of the passed model instance.
This method should always be used for serialization, because it knows about custom serializers specified when registering resources with this API.
-
root
(self, request)¶ Main API view, returns a list of all available resources
Resources¶
-
class
towel.api.
Resource
(self, **kwargs)¶ This is a
View
subclass with additional goodies for exposing a Django model in a RESTful way. You should not instantiate this class yourself, but useAPI.register()
instead.-
model
¶ The model exposed by this resource.
-
queryset
¶ Prefiltered queryset for this resource or
None
if all objects accessible through the first defined manager on the model should be exposed (or if you do the limiting yourself inResource.get_query_set()
)
-
limit_per_page
¶ Standard count of items in a single request. Defaults to 20. This can be overridden by sending a different value with the
limit
querystring parameter.
-
max_limit_per_page
¶ Maximal count of items in a single request.
limit
query values higher than this are not allowed. Defaults to 1000.
-
A typical request-response cycle¶
-
Resource.
dispatch
(self, request, *args, **kwargs)¶ This method is the primary entry point for requests. It is similar to the base class implementation but has a few important differences:
- It uses
self.request
,self.args
andself.kwargs
in all places. - It calls
unserialize_request()
after assigning the aforementioned variables onself
which may modify all aspects and all variables (f.e. deserialize a JSON request and serialize it again to look like a standard POST request) and only then determines whether the request should be handled by this view at all. - The return value of the
get()
,post()
etc. methods is passed toserialize_response()
and only then returned to the client. The processing methods should return a dictionary which is then serialized into the requested format. If the format is unknown or unsupported, a 406 Not acceptable HTTP error is returned instead. APIException
andHttp404
exceptions are caught and transformed into appropriate responses according to the content type requested.
- It uses
-
Resource.
unserialize_request
(self)¶ This method’s intent is to standardize various aspects of the incoming request so that the following code does not have to care about the format of the incoming data. It might decode incoming JSON data and reformat it as a standard HTTP POST.
Currently, this method does nothing, and because of that, content is only accepted in two forms:
- urlencoded in the request body
- multipart in the request body
-
Resource.
get
(self, request, *args, **kwargs)¶
-
Resource.
head
(self, request, *args, **kwargs)¶ These methods return serialized lists, sets or details depending upon the request URI.
All of the following are valid URIs for a fictional resource for books:
/api/v1/book/
: Returns 20 books./api/v1/book/?offset=20&limit=20
: Returns books 21-40./api/v1/book/42/
: Returns the book with the primary key of 42./api/v1/book/1;3;15/
: Returns a set of three books.
The
get()
method offloads processing into three distinct methods depending upon the URI:-
get_single
(self, request, objects, *args, **kwargs)¶
-
Resource.
get_set
(self, request, objects, *args, **kwargs)¶
-
Resource.
get_page
(self, request, objects, *args, **kwargs)¶
These methods receive an
Objects
instance containing all instances they have to process. The default implementation of all these methods useAPI.serialize_instance()
to do the serialization work (using theAPI
instance atResource.api
).If any of the referenced objects do not exist for the single and the set case, a HTTP 404 is returned instead of returning a partial response.
The list URI does not only return a list of objects, but another mapping containing metadata about the response such as URIs for the previous and next page (if they exist) and the total object count.
-
Resource.
options
(self, request, *args, **kwargs)¶ Returns a list of allowed HTTP verbs in the
Allow
response header. The response is otherwise empty.Note
URIs inside the resource might still return 405 Method not allowed erorrs if a particular HTTP verb is only implemented for a subset of URIs, for example only for single instances.
-
Resource.
post
(self, request, *args, **kwargs)¶
-
Resource.
put
(self, request, *args, **kwargs)¶
-
Resource.
delete
(self, request, *args, **kwargs)¶
-
Resource.
patch
(self, request, *args, **kwargs)¶
-
Resource.
trace
(self, request, *args, **kwargs)¶ Default implementations do not exist, that means that if you do not provide your own, the only answer will ever be a HTTP 405 Method not allowed error.
-
Resource.
serialize_response
(self, response, status=httplib.OK, headers={})¶ This method is a thin wrapper around
Serializer.serialize()
. Ifresponse
is already aHttpResponse
instance, it is returned directly.The content types supported by
Serializer
are JSON, but more on that later.
The serializer¶
-
class
towel.api.
Serializer
¶
The API supports output as JSON. The format is determined
by looking at the HTTP Accept
header first. If no acceptable encoding
is found, a HTTP 406 Not acceptable error is returned to the client.
The detection of supported content types can be circumvented by adding
a querystring parameter naemd format
. The supported values are as
follows:
?format=json
or?format=application/json
for JSON output
The request parser¶
-
class
towel.api.
RequestParser
¶ Parses the request body into a format independent of its content type.
Does nothing for the following HTTP methods because they are not supposed to have a request body:
GET
HEAD
OPTIONS
TRACE
DELETE
Otherwise, the code tries determining a parser for the request. The following content types are supported:
application/x-www-form-urlencoded
(the default)multipart/form-data
application/json
The two former content types are supported directly by Django, all capabilities and restrictions are inherited directly. When using JSON, file uploads are not supported.
The parsed data is available as
request.POST
andrequest.FILES
.request.POST
is used instead of something else even forPUT
andPATCH
requests (among others), because most code written for Django expects data to be provided under that name.-
parse
(self, request)¶ Decides whether the request body should be parsed, and if yes, decides which parser to use. Returns a HTTP 415 Unsupported media type if the request isn’t understood.
-
parse_form
(self, request)¶
-
parse_json
(self, request)¶ The actual work horses.
Additional classes and exceptions¶
-
exception
towel.api.
APIException
(error_message=None, status=None, data={})¶ Custom exception which signals a problem detected somewhere inside the API machinery.
Usage:
# Use official W3C error names from ``httplib.responses`` raise ClientError(status=httplib.NOT_ACCEPTABLE)
or:
raise ServerError('Not implemented, go away', status=httplib.NOT_IMPLEMENTED)
Additional information can be passed through by setting the
data
argument to a dict instance. TheAPIException
handler will merge the dict into the default error data and return everything to the client:raise APIException('Validation failed', data={ 'form': form.errors, })
-
class
towel.api.
Objects
(queryset, page, set, single)¶ A
namedtuple
holding the return value ofResource.objects()
.
Utility functions¶
-
towel.api.
api_reverse
(model, ident, api_name='api', fail_silently=False, **kwargs)¶ Determines the canonical URL of API endpoints for arbitrary models.
model
is the Django model you want to use,ident
should be one oflist
,set
ordetail
at the moment- Additional keyword arguments are forwarded to the
reverse()
call.
Usage:
api_reverse(Product, 'detail', pk=42)
Passing an instance works too:
api_reverse(instance, 'detail', pk=instance.pk)
-
towel.api.
serialize_model_instance
(instance, api, inline_depth=0, exclude=(), only_registered=True, build_absolute_uri=lambda uri: uri, **kwargs)¶ Serializes a single model instance.
If
inline_depth
is a positive number,inline_depth
levels of related objects are inlined. The performance implications of this feature might be severe! Note: Additional arguments specified when callingserialize_model_instance
such asexclude
,only_registered
and further keyword arguments are currently not forwarded to inlined objects. Those parameters should be set upon resource registration time as documented in theAPI
docstring above.The
exclude
parameter is especially helpful when used together withfunctools.partial
.Set
only_registered=False
if you want to serialize models which do not have a canonical URI inside this API.build_absolute_uri
should be a callable which transforms any passed URI fragment into an absolute URI including the protocol and the hostname, for examplerequest.build_absolute_uri
.This implementation has a few characteristics you should be aware of:
- Only objects which have a canonical URI inside this particular API are
serialized; if no such URI exists, this method returns
None
. This behavior can be overridden by passingonly_registered=False
. - Many to many relations are only processed if
inline_depth
has a positive value. The reason for this design decision is that the database has to be queried for showing the URIs of related objects anyway and because of that we either show the full objects or nothing at all. - Some fields (currently only fields with choices) have a machine readable
and a prettified value. The prettified values are delivered inside the
__pretty__
dictionary for your convenience. - The primary key of the model instance is always available as
__pk__
.
- Only objects which have a canonical URI inside this particular API are
serialized; if no such URI exists, this method returns
-
towel.api.
querystring
(data, exclude=(), **kwargs)¶ Returns a properly encoded querystring
The supported arguments are as follows:
data
should be aMultiValueDict
instance (i.e.request.GET
)exclude
is a list of keys fromdata
which should be skipped- Additional key-value pairs are accepted as keyword arguments
Usage:
next_page_url = querystring( request.GET, exclude=('page',), page=current + 1, )
API behavior¶
Resource list¶
The available resources can be determined by sending a request to the root
URI of this API, /api/v1/
. Resources can either be canonical or not.
All resources are returned in a list, the canonical URIs for objects are additionally returned as a hash.
The individual resources are described by a hash containing two values (as do most objects returned by the API):
__uri__
: The URI of the particular object__str__
: A string containing the ‘name’ of the object, whatever that would be (it’s the return value of the__str__
method for Django models, and the lowercased class name of the model registered with the resource).
In the list of resources, a particular __str__
value will exist
several times if a model is exposed through more than one resource;
__uri__
values will always be unique.
Listing endpoints¶
All API endpoints currently support GET, HEAD and OPTIONS requests.
All listing endpoints support the following parameters:
?limit=<integer>
: Determines how many objects will be shown on a single page. The default value is 20. The lower limit is zero, the upper limit is determined by the variablemax_limit_per_page
which defaults to 1000.?offset=<integer>
: Can be used for retrieving a different page of objects. Passing?offset=20
with a limit of 20 will return the next page. The offset is zero-indexed.
Note
You won’t have to construct query strings containing these parameters
yourself in most cases. All list views return a mapping with additional
information about the current request and next
and previous
links for your convenience as well.
List views return two data structures, objects
and meta
. The
former is a list of all objects for the current request, the latter
a mapping of additional information about the current set of objects:
offset
: The offset value as described above.limit
: The limit value as described above.total
: The total count of objects.previous
: A link to the previous page ornull
.next
: A link to the next page ornull
.
Object representation¶
The following fields should always be available on objects returned:
__uri__
: The URI.__pk__
: The primary key of this object.__str__
: The return value of the__str__
or__unicode__
method.
A few fields’ values have to be treated specially, because their values do not have an obvious representation in an JSON document. The fields and their representations are as follows:
date
anddatetime
objects are converted into strings usingstr()
.Decimal
is converted into a string without (lossy) conversion tofloat
first.FileField
andImageField
are shown as the URL of the file.ForeignKey
fields are shown as their canonical URI (if there exists such a URI inside this API) or even inlined if?full=1
is passed when requesting the details of an object.
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'),
}
Search and Filter¶
Towel does not distinguish between searching and filtering. There are different layers of filtering applied during a request and depending on your need you have to hook in your filter at the right place.
Making lists searchable using the search form¶
Pagination is not enough for many use cases, we need more! Luckily, Towel has a pre-made solution for searching object lists too.
towel.forms.SearchForm
can be used together with
towel.managers.SearchManager
to build a low-cost implementation of
full text search and filtering by model attributes.
The method used to implement full text search is a bit stupid and cannot replace mature full text search solutions such as Apache Solr. It might just solve 80% of the problems with 20% of the effort though.
Code talks. First, we extend our models definition with a
Manager
subclass with a simple search
implementation:
from django.db import models
from towel.managers import SearchManager
class BookManager(SearchManager):
search_fields = ('title', 'topic', 'authors__name',
'publisher__name', 'publisher__address')
class Book(models.Model):
# [...]
objects = BookManager()
SearchManager
supports queries with multiple clauses;
terms may be grouped using apostrophes, plus and minus signs may be optionally
prepended to the terms to determine whether the given term should be included
or not. Example:
+Django "Shop software" -Satchmo
Please note that you can search fields from other models too. You should
be careful when traversing many-to-many or reverse foreign key relations
however, because you will get duplicated results if you do not call
distinct()
on the resulting queryset.
The method _search()
does the heavy
lifting when constructing a queryset. You should not need to override this
method. If you want to customize the results further, f.e. apply a site-wide
limit for the objects a certain logged in user may see, you should override
search()
.
Next, we have to create a SearchForm
subclass:
from django import forms
from towel import forms as towel_forms
from myapp.models import Author, Book, Publisher
class BookSearchForm(towel_forms.SearchForm):
publisher = forms.ModelChoiceField(Publisher.objects.all(), required=False)
authors = forms.ModelMultipleChoiceField(Author.objects.all(), required=False)
published_on__lte = forms.DateField(required=False)
published_on__gte = forms.DateField(required=False)
formfield_callback = towel_forms.towel_formfield_callback
You have to add required=False
to every field if you do not want validation
errors on the first visit to the form (which would not make a lot of sense, but
isn’t actively harmful).
As long as you only use search form fields whose names correspond to the keywords
used in Django’s .filter()
calls or Q()
objects you do not have to do
anything else.
The formfield_callback
simply substitutes a few fields with whitespace-stripping
equivalents, and adds CSS classes to DateInput
and DateTimeInput
so that
they can be easily augmented by javascript code.
Warning
If you want to be able to filter by multiple items, i.e. publishers 1 and 2,
you have to define the publisher
field in the SearchForm
as
ModelMultipleChoiceField
. Even if the model itself only
has a simple ForeignKey Field. Otherwise only the last element of a series
is used for filtering.
To activate a search form, all you have to do is add an additional parameter when you instantiate a ModelView subclass:
from myapp.forms import BookSearchForm
from myapp.models import Book
from towel.modelview import ModelView
urlpatterns = patterns('',
url(r'^books/', include(ModelView(Book,
search_form=BookSearchForm,
paginate_by=20,
).urls)),
)
You can now filter the list by providing the search keys as GET parameters:
localhost:8000/books/?author=2
localhost:8000/books/?publisher=4&o=authors
localhost:8000/books/?authors=4&authors=5&authors=6
Advanced SearchForm features¶
The SearchForm
has a post_init
method,
which receives the request and is useful if you have to further modify
the queryset i.e. depending on the current user:
def post_init(self, request):
self.access = getattr(request.user, 'access', None)
self.fields['publisher'].queryset = Publisher.objects.for_user(request.user)
The ordering is also defined in the SearchForm
.
You have to specify a dict called orderings
which has the ordering key
as first parameter. The second parameter can be a field name, an iterable of
field names or a callable. The ordering keys are what is used in the URL:
class AddressSearchForm(SearchForm):
orderings = {
'': ('last_name', 'first_name'), # Default
'dob': 'dob', # Sort by date of birth
'random': lambda queryset: queryset.order_by('?'),
}
Persistent queries¶
When you pass the parameter s
, the search is stored in the session for
that path. If the user returns to the object list, the filtering is applied again.
The field is included in the SearchForm by default, but don’t forget to add it to your template if you are using a custom form render method.
To reset the filters, you have to pass ?clear=1
or ?n
.
Quick Rules¶
Another option for filtering are Quick rules.
This allows for field-independent filtering like is:cool
.
Quick rules are mapped to filter attributes using regular expressions.
They go into the search form and are parsed automatically
(as long as query_data
is used inside the queryset
method:
class BookSearchForm(towel_forms.SearchForm):
quick_rules = [
(re.compile(r'has:publisher'), quick.static(publisher__isnull=False)),
(re.compile(r'is:published'), quick.static(published_on__lt=timezone.now)),
]
Template tags¶
ModelView detail tags¶
Yields a list of
(verbose_name, value)
tuples for all local model fields:{% load modelview_detail %} <table> {% for title, value in object|model_details %} <tr> <th>{{ title }}</th> <td>{{ value }}<td> </tr> {% endfor %} </table>
ModelView list tags¶
Requires a list of fields which should be shown in columns on a list page. The fields may also be callables. ForeignKey fields are automatically converted into links:
{% load modelview_list %} <table> {% for object in object_list %} <tr> {% for title, value in object|model_row:"__unicode__,author %} <td>{{ value }}</td> {% endfor %} </tr> {% endfor %} </table>
Uses
towel/_pagination.html
to display a nicely formatted pagination section. An additional parameter may be provided if the pagination should behave differently depending on where it is shown; it is passed totowel/_pagination.html
aswhere
:{% load modelview_list %} {% if paginator %}{% pagination page paginator "top" %}{% endif %} {# list / table code ... #} {% if paginator %}{% pagination page paginator "bottom" %}{% endif %}
As long as
paginate_by
is set on the ModelView, a paginator object is always provided. The{% if paginator %}
is used because you cannot be sure that pagination is used at all in a generic list template.This template tag needs the
django.core.context_processors.request
context processor.
URL-encodes the passed
dict
in a format suitable for pagination.page
andall
are excluded by default:{% load modelview_list %} <a href="?{{ request.GET|querystring }}&page=1">Back to first page</a> {# equivalent, but longer: #} <a href="?{{ request.GET|querystring:"page,all" }}&page=1">Back to first page</a>
Shows a table column header suitable for use as a link to change the ordering of objects in a list:
{% ordering_link "" request title=_("Edition") %} {# default order #} {% ordering_link "customer" request title=_("Customer") %} {% ordering_link "state" request title=_("State") %}
Required arguments are the field and the request. It is very much recommended to add a title too of course.
ordering_link
has an optional argument,base_url
which is useful if you need to customize the link part before the question mark. The default behavior is to only add the query string, and nothing else to thehref
attribute.It is possible to specify a set of CSS classes too. The CSS classes
'asc'
and'desc'
are added automatically by the code depending upon the ordering which would be selected if the ordering link were clicked (NOT the current ordering):{% ordering_link "state" request title=_("State") classes="btn" %}
The
classes
argument defaults to'ordering'
.
Batch tags¶
Returns the checkbox for batch processing:
{% load towel_batch_tags %} {% for object in object_list %} {# ... #} {% batch_checkbox batch_form object.id %} {# ... #} {% endfor %}
Form tags¶
Returns the concatenated result of running
{% form_item field %}
on every form field.
Uses
towel/_form_item.html
to render a form field. The default template renders a table row, and includes:help_text
after the form field in ap.help
invalid
andrequired
classes on the row
Uses
towel/_form_item_plain.html
to render a form field, f.e. inside a table cell. The default template puts the form field inside a<span>
tag with various classes depending on the state of the form field such asinvalid
andrequired
.
Shows form and formset errors using
towel/_form_errors.html
. You can pass a list of forms, formsets, lists containing forms and formsets and dicts containing forms and formsets as values.Variables which do not exist are silently ignored:
{% load towel_form_tags %} {% form_errors publisher_form books_formset %}
Shows form and formset warnings using
towel/_form_warnings.html
. You can pass a list of forms, formsets, lists containing forms and formsets and dicts containing forms and formsets as values. Also shows a checkbox which can be used to ignore warnings. This template tag does not work with Django’s standard forms because they have do not have support for warnings. UseWarningsForm
instead.Variables which do not exist are silently ignored:
{% load towel_form_tags %} {% form_warnings publisher_form books_formset %}
This is a very convenient block tag which can be used to build dynamic formsets, which means formsets where new forms can be added with javascript (jQuery):
{% load towel_form_tags %} <script type="text/javascript" src="PATH_TO_JQUERY.JS"></script> <script type="text/javascript" src="{{ STATIC_URL }}towel/towel.js"></script> <style type="text/css">.empty { display: none; }</style> <form method="post" action=".">{% csrf_token %} {% form_errors form formset %} <table> {% for field in form %}{% form_item field %}{% endfor %} </table> <h2>Formset</h2> <table> <thead><tr> <th>Field 1</th> <th>Field 2</th> <th></th> </tr></thead> <tbody> {% dynamic_formset formset "formset-prefix" %} <tr id="{{ form_id }}" {% if empty %}class="empty"{% endif %}> <td> {{ form.id }} {% form_item_plain form.field1 %} </td> <td>{% form_item_plain form.field2 %}</td> <td>{{ form.DELETE }}</td> </tr> {% enddynamic_formset %} </tbody> </table> <button type="button" onclick="towel_add_subform('formset-prefix')"> Add row to formset</button> <button type="submit">Save</button> </form>
The formset-prefix must correspond to the prefix used when initializing the FormSet in your Python code. You should pass
extra=0
when creating the FormSet class; any additional forms are better created usingtowel_add_subform
.
Autogenerated API Documentation¶
API programming¶
Deletion¶
Forms¶
-
class
towel.forms.
BatchForm
(request, queryset, *args, **kwargs) This form class can be used to provide batch editing functionality in list views, similar to Django’s admin actions.
You have to implement your batch processing in the
_context()
method. This method only receives one parameter, a queryset which is already filtered according to the selected items on the list view. Additionally, the current request is available as an attribute of the form instance,self.request
.The method
process(self)
may have the following return values:- A
dict
instance: Will be merged into the template context. - A
HttpResponse
instance: Will be returned directly to the client. - An iterable: The handler assumes successful processing of all objects contained in the iterable.
- Nothing: Nothing happens.
Usage example:
class AddressBatchForm(BatchForm): subject = forms.CharField() body = forms.TextField() def process(self): # Form validation has already been taken care of subject = self.cleaned_data.get('subject') body = self.cleaned_data.get('body') if not (subject and body): return {} sent = 0 for item in self.batch_queryset: send_mail(subject, body, settings.DEFAULT_SENDER, [item.email]) sent += 1 if sent: messages.success(self.request, 'Sent %s emails.' % sent) return self.batch_queryset def addresses(request): queryset = Address.objects.all() batch_form = AddressBatchForm(request, queryset) ctx = {'addresses': queryset} if batch_form.should_process(): result = form.process() if isinstance(result, HttpResponse): return result elif isinstance(result, dict): ctx.update(result) elif hasattr(result, '__iter__'): messages.success(request, _('Processed the following items: %s') % ( ', '.join(force_text(item) for item in result))) return HttpResponseRedirect('.') return render(request, 'addresses.html', ctx)
Template code:
{% load towel_batch_tags %} <form method="post" action="."> <ul> {% for address in addresses %} <li> {% batch_checkbox address.id batch_form %} {{ address }} </li> {% endfor %} </ul> {# Required! Otherwise, ``BatchForm.process`` does nothing. #} <input type="hidden" name="batchform" value="1" /> <table> {{ batch_form }} </table> <button type="submit">Send mail to selected</button> </form>
-
batch_queryset
Returns the queryset containing only items that have been selected for batch processing.
-
clean
() Cleans the batch form fields and checks whether at least one item had been selected.
-
process
() Actually processes the batch form submission. Override this with your own behavior.
Batch forms may return the following types here (they are handled by
ModelView.handle_batch_form
:- A
HttpResponse
: Will be returned directly to the user. - An iterable: A success message will be generated containing all items in the iterable.
- A
-
should_process
() Returns true when the submitted form was the batch form, and the batch form is valid.
- A
-
class
towel.forms.
ModelAutocompleteWidget
(attrs=None, url=None, queryset=None) Model autocompletion widget using jQuery UI Autocomplete
Supports both querysets and JSON-returning AJAX handlers as data sources. Use as follows:
class MyForm(forms.ModelForm): customer = forms.ModelChoiceField(Customer.objects.all(), widget=ModelAutocompleteWidget(url='/customers/search_ajax/'), ) type = forms.ModelChoiceField(Type.objects.all(), widget=ModelAutocompleteWidget(queryset=Type.objects.all()), )
You need to make sure that the jQuery UI files are loaded correctly yourself.
-
class
towel.forms.
MultipleAutocompletionWidget
(attrs=None, queryset=None) You should probably use harvest chosen instead.
-
class
towel.forms.
SearchForm
(data, *args, **kwargs) Supports persistence of searches (stores search in the session). Requires not only the GET parameters but the request object itself to work correctly.
Usage example:
class AddressManager(SearchManager): search_fields = ('first_name', 'last_name', 'address', 'email', 'city', 'zip_code', 'created_by__email') class Address(models.Model): ... objects = AddressManager() class AddressSearchForm(SearchForm): orderings = { '': ('last_name', 'first_name'), # Default 'dob': 'dob', # Sort by date of birth 'random': lambda queryset: queryset.order_by('?'), } is_person = forms.NullBooleanField() def addresses(request): search_form = AddressSearchForm(request.GET, request=request) queryset = search_form.queryset(Address) ctx = { 'addresses': queryset, 'search_form': search_form, } return render(request, 'addresses.html', ctx)
Warning
All fields in the form need to have
required=False
set. Otherwise, form validation would already fail on the first visit on the list page (which would kind of defeat the purpose of a search form).Template code:
<form method="get" action="."> <input type="hidden" name="s" value="1"> <!-- SearchForm search --> <table> {{ search_form }} </table> <button type="submit">Search</button> </form> {% for address in addresses %} ... {% endfor %}
-
always_exclude
= (u's', u'query', u'o') Fields which are always excluded from automatic filtering in
apply_filters
-
apply_filters
(queryset, data, exclude=()) Automatically apply filters
Uses form field names for
filter()
argument construction.
-
apply_ordering
(queryset, ordering=None) Applies ordering if the value in
o
matches a key inself.orderings
. The ordering may also be reversed, in which case theo
value should be prefixed with a minus sign.
-
default
= {} Default field values - used if not overridden by the user
-
fields_iterator
() Yield all additional search fields.
-
o
= None Current ordering
-
orderings
= {} Ordering specification
-
persist
(request) Persist the search in the session, or load saved search if user isn’t searching right now.
-
post_init
(request) Hook for customizations.
-
prepare_data
(data, request) Fill in default values from
default
if they aren’t provided by the user.
-
query
= None Full text search query
-
query_data
() Return a fulltext query and structured data which can be converted into simple filter() calls
-
queryset
(model) Return the result of the search
-
quick_rules
= [] Quick rules, a list of (regex, mapper) tuples
-
s
= None Search form active?
-
safe_cleaned_data
Safely return a dictionary of values, even if search form isn’t valid.
-
searching
() Returns
searching
for use as CSS class if results are filtered by this search form in any way.
-
-
class
towel.forms.
StrippedTextInput
(attrs=None) TextInput
form widget subclass returning stripped contents only
-
class
towel.forms.
StrippedTextarea
(attrs=None) Textarea
form widget subclass returning stripped contents only
-
class
towel.forms.
WarningsForm
(*args, **kwargs) Form subclass which allows implementing validation warnings
In contrast to Django’s
ValidationError
, these warnings may be ignored by checking a checkbox.The warnings support consists of the following methods and properties:
WarningsForm.add_warning(<warning>)
: Adds a new warning messageWarningsForm.warnings
: A list of warnings or an empty list if there are none.WarningsForm.is_valid()
: OverriddenForm.is_valid()
implementation which returnsFalse
for otherwise valid forms with warnings, if those warnings have not been explicitly ignored (by checking a checkbox or by passingignore_warnings=True
tois_valid()
.- An additional form field named
ignore_warnings
is available - this field should only be displayed ifWarningsForm.warnings
is non-emtpy.
-
add_warning
(warning) Adds a new warning, should be called while cleaning the data
-
is_valid
(ignore_warnings=False) is_valid()
override which returnsFalse
for forms with warnings if these warnings haven’t been explicitly ignored
-
towel.forms.
autocompletion_response
(queryset, limit=10) Helper which returns a
HttpResponse
list of instances in a format suitable for consumption by jQuery UI Autocomplete, respectivelytowel.forms.ModelAutocompleteWidget
.
-
towel.forms.
towel_formfield_callback
(field, **kwargs) Use this callback as
formfield_callback
if you want to use stripped text inputs and textareas automatically without manually specifying the widgets. Adds adateinput
class to date and datetime fields too.
Managers¶
-
class
towel.managers.
SearchManager
Stupid searching manager
Does not use fulltext searching abilities of databases. Constructs a query searching specified fields for a freely definable search string. The individual terms may be grouped by using apostrophes, and can be prefixed with + or - signs to specify different searching modes:
+django "shop software" -satchmo
Usage example:
class MyModelManager(SearchManager): search_fields = ('field1', 'name', 'related__field') class MyModel(models.Model): # ... objects = MyModelManager() MyModel.objects.search('yeah -no')
-
search
(query) This implementation stupidly forwards to _search, which does the gruntwork.
Put your customizations in here.
-
-
towel.managers.
normalize_query
(query_string, findterms=<built-in method findall of _sre.SRE_Pattern object>, normspace=<built-in method sub of _sre.SRE_Pattern object>) Splits the query string in invidual keywords, getting rid of unecessary spaces and grouping quoted words together.
Example:
>>> normalize_query(' some random words "with quotes " and spaces') ['some', 'random', 'words', 'with quotes', 'and', 'spaces']
ModelView¶
Multitenancy¶
Assumptions¶
The following settings are required:
TOWEL_MT_CLIENT_MODEL
: The tenant model, e.g.clients.Client
.TOWEL_MT_ACCESS_MODEL
: The model linking a Django user with a client, must have the following fields:user
: Foreign key toauth.User
.access
: An integer describing the access level of the given user. Higher numbers mean higher access. You have to define those numbers yourself.- The lowercased class name of the client model above as a foreign key
to the client model. If your client model is named
Customer
, the name of this foreign key must becustomer
.
All model managers have a
for_access()
method with a single argument, an instance of the access model, which returns a queryset containing only the objects the current user is allowed to see. The access model should be available asrequest.access
, which means that you are free to put anything there which can be understood by thefor_access()
methods. Therequest.access
attribute is made available by thetowel.mt.middleware.LazyAccessMiddleware
middleware.towel.mt.modelview.ModelView
automatically fills in acreated_by
foreign key pointing toauth.User
if it exists.The form classes in
towel.mt.forms
, those beingModelForm
,Form
andSearchForm
all require the request (the two former on initialization, the latter onpost_init
). Model choice fields are postprocessed to only contain values from the current tenant. This does not work if you customize thechoices
field at the same time as setting thequeryset
. If you do that you’re on your own.The model authentication backend
towel.mt.auth.ModelBackend
also allows email addresses as username. It preloads the access and client model and assigns it torequest.user
if possible. This is purely a convenience – you are not required to use the backend.
Forms¶
These three form subclasses will automatically add limitation by tenant
to all form fields with a queryset
attribute.
Warning
If you customized the dropdown using choices
you have to limit the
choices by the current tenant yourself.
Middleware for a lazy request.access
attribute¶
-
class
towel.mt.middleware.
LazyAccessMiddleware
This middleware (or something equivalent providing a
request.access
attribute must be put inMIDDLEWARE_CLASSES
to use the helpers intowel.mt
.
Models for multitenant Django projects¶
The models for towel.mt
have to be provided by the project where
towel.mt
is used, that’s why this file is empty.
The simplest models might look like that:
from django.contrib.auth.models import User
from django.db import models
class Client(models.Model):
name = models.CharField(max_length=100)
class Access(models.Model):
EMPLOYEE = 10
MANAGEMENT = 20
ACCESS_CHOICES = (
(EMPLOYEE, 'employee'),
(MANAGEMENT, 'management'),
)
client = models.ForeignKey(Client)
user = models.OneToOneField(User)
access = models.SmallIntegerField(choices=ACCESS_CHOICES)
API methods can be protected as follows:
from towel.api import API
from towel.api.decorators import http_basic_auth
from towel.mt.api import Resource, api_access
# Require a valid login and an associated Access model:
api_v1 = API('v1', decorators=[
csrf_exempt,
http_basic_auth,
api_access(Access.EMPLOYEE),
])
api_v1.register(SomeModel,
view_class=Resource,
)
Other views:
from towel.mt import AccessDecorator
# Do this once somewhere in your project
access = AccessDecorator()
@access(Access.MANAGEMENT)
def management_only_view(request):
# ...
Paginator¶
Drop-in replacement for Django’s django.core.paginator
with additional
goodness
Django’s paginator class has a page_range
method returning a list of all
available pages. If you got lots and lots of pages this is not very helpful.
Towel’s page class (not paginator class!) sports a page_range
method
too which only returns a few pages at the beginning and at the end of the page
range and a few pages around the current page.
All you have to do to use this module is replacing all imports from
django.core.paginator
with towel.paginator
. All important classes and
all exceptions are available inside this module too.
The page range parameters can be customized by adding a PAGINATION
setting.
The defaults are as follows:
PAGINATION = {
'START': 6, # pages at the beginning of the range
'END': 6, # pages at the end of the range
'AROUND': 5, # pages around the current page
}
-
exception
towel.paginator.
InvalidPage
-
exception
towel.paginator.
PageNotAnInteger
-
exception
towel.paginator.
EmptyPage
-
class
towel.paginator.
Paginator
(object_list, per_page, orphans=0, allow_empty_first_page=True) Custom paginator returning a Page object with an additional page_range method which can be used to implement Digg-style pagination
-
page
(number) Returns a Page object for the given 1-based page number.
-
-
class
towel.paginator.
Page
(page) Page object for Digg-style pagination
-
page_range
Generates a list for displaying Digg-style pagination
The page numbers which are left out are indicated with a
None
value. Please note that Django’s paginator ownpage_range
method isn’t overwritten – Django’spage_range
is a method of thePaginator
class, not thePage
class.Usage:
{% for p in page.page_range %} {% if p == page.number %} {{ p }} <!-- current page --> {% else %} {% if p is None %} … {% else %} <a href="?page={{ p }}">{{ p }}</a> {% endif %} {% endif %} {% endfor %}
-
Queryset transform¶
django_queryset_transform¶
Allows you to register a transforming map function with a Django QuerySet that will be executed only when the QuerySet itself has been evaluated.
This allows you to build optimisations like “fetch all tags for these 10 rows” while still benefiting from Django’s lazy QuerySet evaluation.
For example:
def lookup_tags(item_qs):
item_pks = [item.pk for item in item_qs]
m2mfield = Item._meta.get_field('tags')[0]
tags_for_item = Tag.objects.filter(
item__in = item_pks
).extra(select = {
'item_id': '%s.%s' % (
m2mfield.m2m_db_table(), m2mfield.m2m_column_name()
)
})
tag_dict = {}
for tag in tags_for_item:
tag_dict.setdefault(tag.item_id, []).append(tag)
for item in item_qs:
item.fetched_tags = tag_dict.get(item.pk, [])
qs = Item.objects.filter(name__contains = 'e').transform(lookup_tags)
for item in qs:
print(item, item.fetched_tags)
Prints:
Winter comes to Ogglesbrook [<sledging>, <snow>, <winter>, <skating>]
Summer now [<skating>, <sunny>]
But only executes two SQL queries - one to fetch the items, and one to fetch ALL of the tags for those items.
Since the transformer function can transform an evaluated QuerySet, it doesn’t need to make extra database calls at all - it should work for things like looking up additional data from a cache.multi_get() as well.
Originally inspired by http://github.com/lilspikey/django-batch-select/
LICENSE¶
Copyright (c) 2010, Simon Willison. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
- Neither the name of Django nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
class
towel.queryset_transform.
TransformQuerySet
(*args, **kwargs) -
iterator
() An iterator over the results from applying this QuerySet to the database.
-
Quick¶
This module beefs up the default full text search field to be a little
bit more versatile. It allows specifying patterns such as is:unread
or !important
which are extracted from the query string and returned
as standalone values allowing the implementation of a search syntax
known from f.e. Google Mail.
Quick rules always consist of two parts: A regular expression pulling values out of the query string and a mapper which maps the values from the regex to something else which may be directly usable by forms.
Usage example:
QUICK_RULES = [
(re.compile(r'!!'), quick.static(important=True)),
(re.compile(r'@(?P<username>\w+)'),
quick.model_mapper(User.objects.all(), 'assigned_to')),
(re.compile(r'\^\+(?P<due>\d+)'),
lambda v: {'due': date.today() + timedelta(days=int(v['due']))}),
(re.compile(r'=(?P<estimated_hours>[\d\.]+)h'),
quick.identity()),
]
data, rest = quick.parse_quickadd(
request.POST.get('quick', ''),
QUICK_RULES)
data['notes'] = ' '.join(rest) # Everything which could not be parsed
# is added to the ``notes`` field.
form = TicketForm(data)
Note
The mappers always get the regex matches dict
and return a
dict
.
-
towel.quick.
bool_mapper
(attribute) Maps
yes
,1
andon
toTrue
andno
,0
andoff
toFalse
.
-
towel.quick.
due_mapper
(attribute) Understands
Today
,Tomorrow
, the following five localized week day names or (partial) dates such as20.12.
and01.03.2012
.
-
towel.quick.
identity
() Identity mapper. Returns the values from the regular expression directly.
-
towel.quick.
model_choices_mapper
(data, attribute) Needs a
value
provided by the regular expression and returns the correspondingkey
value.Example:
class Ticket(models.Model): VISIBILITY_CHOICES = ( ('public', _('public')), ('private', _('private')), ) visibility = models.CharField(choices=VISIBILITY_CHOICES) QUICK_RULES = [ (re.compile(r'~(?P<value>[^\s]+)'), quick.model_choices_mapper( Ticket.VISIBILITY_CHOICES, 'visibility')), ]
-
towel.quick.
model_mapper
(queryset, attribute) The regular expression needs to return a dict which is directly passed to
queryset.get()
. As a speciality, this mapper returns both the primary key of the instance under theattribute
name, and the instance itself asattribute_
.
-
towel.quick.
parse_quickadd
(quick, regexes) The main workhorse. Named
parse_quickadd
for historic reasons, can be used not only for adding but for searching etc. too. In fact,towel.forms.SearchForm
supports quick rules out of the box when they are specified inquick_rules
.
-
towel.quick.
static
(**kwargs) Return a predefined
dict
when the given regex matches.
Template tags¶
ModelView
template tags¶
-
towel.templatetags.modelview_detail.
model_details
(instance, fields=None) Returns a stream of
verbose_name
,value
pairs for the specified model instance:<table> {% for verbose_name, value in object|model_details %} <tr> <th>{{ verbose_name }}</th> <td>{{ value }}</td> </tr> {% endfor %} </table>
-
towel.templatetags.modelview_list.
model_row
(instance, fields) Shows a row in a modelview object list:
{% for object in object_list %} <tr> {% for verbose_name, field in object|model_row:"name,url" %} <td>{{ field }}</td> {% endfor %} </tr> {% endfor %}
Batch form template tags¶
-
towel.templatetags.towel_batch_tags.
batch_checkbox
(form, id) Checkbox which allows selecting objects for batch processing:
{% for object in object_list %} {% batch_checkbox batch_form object.id %} {{ object }} etc... {% endfor %}
This tag returns an empty string if
batch_form
does not exist for some reason. This makes it easier to write templates when you don’t know if the batch form will be available or not (f.e. because of a permissions requirement).
Generally helpful form tags¶
-
towel.templatetags.towel_form_tags.
dynamic_formset
(parser, token) Implements formsets where subforms can be added using the
towel_add_subform
javascript method:{% dynamic_formset formset "activities" %} ... form code {% enddynamic_formset %}
-
towel.templatetags.towel_form_tags.
form_errors
(parser, token) Show all form and formset errors:
{% form_errors form formset1 formset2 %}
Silently ignores non-existant variables.
-
towel.templatetags.towel_form_tags.
form_item
(item, additional_classes=None) Helper for easy displaying of form items:
{% for field in form %} {% form_item field %} {% endfor %}
-
towel.templatetags.towel_form_tags.
form_item_plain
(item, additional_classes=None) Helper for easy displaying of form items without any additional tags (table cells or paragraphs) or labels:
{% form_item_plain field %}
-
towel.templatetags.towel_form_tags.
form_items
(form) Render all form items:
{% form_items form %}
-
towel.templatetags.towel_form_tags.
form_warnings
(parser, token) Show all form and formset warnings:
{% form_warnings form formset1 formset2 %}
Silently ignores non-existant variables.
Template tags for pulling out the verbose_name(_plural)?
from almost any object¶
-
towel.templatetags.verbose_name_tags.
verbose_name
(item) Pass in anything and it tries hard to return its
verbose_name
:{{ form|verbose_name }} {{ object|verbose_name }} {{ formset|verbose_name }} {{ object_list|verbose_name }}
-
towel.templatetags.verbose_name_tags.
verbose_name_plural
(item) Pass in anything and it tries hard to return its
verbose_name_plural
:{{ form|verbose_name_plural }} {{ object|verbose_name_plural }} {{ formset|verbose_name_plural }} {{ object_list|verbose_name_plural }}
Utils¶
-
towel.utils.
app_model_label
(model) Stop those deprecation warnings
-
towel.utils.
changed_regions
(regions, fields) Returns a subset of regions which have to be updated when fields have been edited. To be used together with the
{% regions %}
template tag.Usage:
regions = {} render(request, 'detail.html', { 'object': instance, 'regions': regions, }) return HttpResponse( json.dumps(changed_regions(regions, ['emails', 'phones'])), content_type='application/json')
-
towel.utils.
parse_args_and_kwargs
(parser, bits) Parses template tag arguments and keyword arguments
Returns a tuple
args, kwargs
.Usage:
@register.tag def custom(parser, token): return CustomNode(*parse_args_and_kwargs(parser, token.split_contents()[1:])) class CustomNode(template.Node): def __init__(self, args, kwargs): self.args = args self.kwargs = kwargs def render(self, context): args, kwargs = resolve_args_and_kwargs(context, self.args, self.kwargs) return self._render(context, *args, **kwargs): def _render(self, context, ...): # The real workhorse
-
towel.utils.
related_classes
(instance) Return all classes which would be deleted if the passed instance were deleted too by employing the cascade machinery of Django itself. Does not return instances, only classes.
Note! When using Django 1.5, autogenerated models (many to many through models) are returned too.
-
towel.utils.
resolve_args_and_kwargs
(context, args, kwargs) Resolves arguments and keyword arguments parsed by
parse_args_and_kwargs
using the passed context instanceSee
parse_args_and_kwargs
for usage instructions.
-
towel.utils.
safe_queryset_and
(head, *tail) Safe AND-ing of querysets. If one of both queries has its DISTINCT flag set, sets distinct on both querysets. Also takes extra care to preserve the result of the following queryset methods:
reverse()
transform()
select_related()
prefetch_related()
-
towel.utils.
substitute_with
(to_delete, instance) Substitute the first argument with the second in all relations, and delete the first argument afterwards.
-
towel.utils.
tryreverse
(*args, **kwargs) Calls
django.core.urlresolvers.reverse
, and returnsNone
on failure instead of raising an exception.