django-smarter
==============
Another approach for declarative style generic views for Django. I beleive, it's a bit smarter :)
Overview
--------
So many times we have to write:
.. sourcecode:: python
@login_required
def edit_post(request, pk):
post = get_object_or_404(Post, pk=pk)
if request.method == 'POST':
form = EditPostForm(request.POST, instance=post)
if form.is_valid():
post = form.save()
return redirect(post.get_absolute_url())
else:
form = EditPostForm()
return render(request, 'edit_post.html', {'form': form})
Right? Well, it's ok to write some reusable helpers for such repeatable views, but when we don't need sophisticated ones here we go:
.. sourcecode:: python
class PostViews(smarter.GenericViews):
model = Post
options = {
'add': {
'form': NewPostForm,
'decorators': (login_required,)
},
'edit': {
'form': EditPostForm,
'decorators': (login_required,)
},
'remove': {
'decorators': (login_required,)
}
}
That's it.
Changes in v1.0
---------------
API is finally and completely changed since v0.6 release.
We've made a "quantum jump" by breaking old-and-not-so-good API to new one - solid and nice. Hope you'll like it.
Here are `some hints that may help you with migration <https://django-smarter.readthedocs.org/en/latest/migrate_0.x_to_1.0.html>`_. I'm actually successfully migrated my real-production project, so the hints are based on "real-battle" example.
Contributors
------------
* `Fabio Santos <https://github.com/fabiosantoscode>`_
* `Sameer Al-Sakran <https://github.com/salsakran>`_
Thank you, comrades! :)
Installation
------------
Requirements:
- Django >= 1.4
Installation::
pip install django-smarter
You *may* add ``smarter`` to your ``INSTALLED_APPS`` to get default templates and tests, but you *don't have to*:
.. sourcecode:: python
INSTALLED_APPS = (
# ...
'smarter',
# ...
)
Then you should define your views and include them in URLs, see `Getting started`_ section below.
Getting started
---------------
Create your models
~~~~~~~~~~~~~~~~~~
Let’s define a simple model:
.. sourcecode:: python
class Page(models.Model):
owner = models.ForeignKey('auth.User')
title = models.CharField(max_length=100)
text = models.TextField()
def __unicode__(self):
return self.title
Register views
~~~~~~~~~~~~~~
Now you can add generic views for the model.
In your `urls.py`:
.. sourcecode:: python
import smarter
from myapp.models import Page
site = smarter.Site()
site.register(smarter.GenericViews, Page)
urlpatterns = patterns('',
url(r'^', include(site.urls)),
# other urls ...
)
This code creates generic views for ``Page`` model, accessed by urls:
- /page/
- /page/add/
- /page/``<pk>``/
- /page/``<pk>``/edit/
- /page/``<pk>``/remove/
Customize views
~~~~~~~~~~~~~~~
Subclass from ``smarter.GenericViews`` and set custom options and/or override methods.
.. sourcecode:: python
from django.contrib.auth.decorators import login_required
import smarter
from .models import Page
class PageViews(smarter.GenericViews):
model = Page
options = {
'add': {
'decorators': (login_required,)
'exclude': ('owner',)
},
}
def add__save(self, request, form, **kwargs):
obj = form.save(commit=False)
obj.owner = request.user
obj.save()
return obj
And don't forget to register new views in `urls.py`:
.. sourcecode:: python
import smarter
from myapp.views import PageViews
site = smarter.Site()
site.register(PageViews) # model argument is not required as model is already set in PageViews
urlpatterns = patterns('',
url(r'^', include(site.urls)),
)
Customize templates
~~~~~~~~~~~~~~~~~~~
In the example above each URL by default to template.
====================== ======================= =====================
URL Template Context
====================== ======================= =====================
/page/ myapp/page/index.html {{ objects_list }}
/page/add/ myapp/page/add.html {{ obj }}, {{ form }}
/page/``<pk>``/ myapp/page/details.html {{ obj }}
/page/``<pk>``/edit/ myapp/page/edit.html {{ obj }}, {{ form }}
/page/``<pk>``/remove/ myapp/page/remove.html {{ obj }}
====================== ======================= =====================
Default template search paths are:
.. sourcecode:: python
('%(app)s/%(model)s/%(action)s.html',
'%(app)s/%(model)s/%(action)s.ajax.html',
'smarter/%(action)s.html',
'smarter/_form.html',
'smarter/_ajax.html',)
So, you have some easy way options:
1. you may override matching templates
2. you may set 'template' key in ``PageViews.options`` for each action
3. you may override default search paths by settings new ``PageViews.defaults`` (read `Options`_ section for details)
Singleton Site
~~~~~~~~~~~~~~
A very special instance of `smarter.Site` is in the smarter module. It allows you to register your applications' views outside your urls.py file, and works well with `autodiscover()`.
Here is smarter_views.py in your app:
.. sourcecode:: python
from smarter import site, GenericViews
from models import Model
class Views(GenericViews):
model = Model
# ...
site.register(Views)
... And urls.py:
.. sourcecode:: python
from django.conf.urls import patterns, include, url
import smarter
smarter.autodiscover()
urlpatterns = patterns('',
url(r'^', include(smarter.site.urls)),
)
This is mostly recommended for non-reusable applications local to your Django project.
API reference
-------------
Actions
~~~~~~~
**Actions** are actually "ids" for views. Well, each action has id like 'add', 'edit', 'bind-to-user' and is mapped to view method with underscores instead of '-': `add`, `edit`, `bind_to_user`.
In ``smarter.GenericViews`` class such actions are defined by default:
======= ================= ========================= ========================
Action URL View method Named URL
======= ================= ========================= ========================
index / index(``request``) [prefix]-[model]-index
add /add/ add(``request``) [prefix]-[model]-add
details /``<pk>``/ details(``request, pk``) [prefix]-[model]-details
edit /``<pk>``/edit/ edit(``request, pk``) [prefix]-[model]-edit
remove /``<pk>``/remove/ remove(``request, pk``) [prefix]-[model]-remove
======= ================= ========================= ========================
What is **[prefix]**? Prefix is defined for ``smarter.Site`` instance:
.. sourcecode:: python
site = smarter.Site(prefix='myapp')
site.register(PageViews)
# ...
So, it **can be empty** and URL names without prefix are defined as `[model]-index`. Please, read `Reversing urls`_ section for more details.
Options
~~~~~~~
**Options** is a ``GenericViews.options`` dict, class property, it contains actions names as keys and actions parameters as values. Parameters structure is:
.. sourcecode:: python
{
'url': <string for url pattern>,
'form': <form class>,
'decorators': <tuple/list of decorators>,
'fields': <tuple/list of form fields>,
'exclude': <tuple/list of excluded form fields>,
'initial': <tuple/list of form fields initialized by request.GET>,
'permissions': <tuple/list of required permissions>,
'widgets': <dict for widgets overrides>,
'help_text': <dict for help texts overrides>,
'required': <dict for required fields overrides>,
'template': <string template name>,
'redirect': <string or callable returning redirect path>
}
Every key here is optional. So, here's how options can be defined for views:
.. sourcecode:: python
import smarter
class Views(smarter.GenericViews):
model = <model>
defaults = <default parameters>
options = {
'<action 1>': <parameters 1>,
'<action 2>': <parameters 2>
}
And here's ``GenericViews.defaults`` class attribute:
.. sourcecode:: python
defaults = {
'initial': None,
'form': ModelForm,
'exclude': None,
'fields': None,
'labels': None,
'widgets': None,
'required': None,
'help_text': None,
'next': None,
'template': (
'%(app)s/%(model)s/%(action)s.html',
'%(app)s/%(model)s/%(action)s.ajax.html',
'smarter/%(action)s.html',
'smarter/_form.html',
'smarter/_ajax.html',),
'decorators': None,
'permissions': None,
}
When option value can't be found in options dict for action it's searched in `GenericViews.defaults`. Note, that defaults are applied to **all actions**.
Action names and URLs
~~~~~~~~~~~~~~~~~~~~~
Actions are named so they can be mapped to views methods and they should not override reserved attributes and methods, so they:
1. **must contain only** latin symbols and '_' or '-', **no spaces**
2. **can't** be in this list: 'model', 'defaults', 'options', 'deny'
3. **can't** start with '-', '_' or 'get\_'
4. **can't** contain '`__`'
Sure, you'll get an exception if something goes wrong with that. We're following `'errors should never pass silently'` here.
And here's how URLs for default views are defined:
.. sourcecode:: python
{
'index': {
'url': r'',
},
'details': {
'url': r'(?P<pk>\d+)/',
},
'add': {
'url': r'add/',
},
'edit': {
'url': r'(?P<pk>\d+)/edit/',
},
'remove': {
'url': r'(?P<pk>\d+)/remove/',
}
}
smarter.Site
~~~~~~~~~~~~
| **Site**\(prefix=None, delim='-')
| - constructor
|
| **register**\(views, model=None, base_url=None, prefix=None)
| - method to add your views for model
|
| **urls**
| - property, returns URLs sequence for all registered views that can be included in `urlpatterns`
|
| **autodiscover**
| - method which goes over `settings.INSTALLED_APPS` and looks for apps with `smarter_views` modules, which it imports, so they can register their views.
Site
++++
Constructor gets two keyword arguments:
1. `prefix=None`, for prefixing URL names for views registered with site object, like '**%(prefix)s**-%(model)s-%(action)s'. If prefix if empty, URLs are named without prefix, like '%(model)s-%(action)s'.
2. `delim='-'`, delimiter for URL names, can be '-', '_' or empty string. URL names are composed with specified delimiter and with uderscore it would be like '%(prefix)s_%(model)s_%(action)s'.
Site.register
+++++++++++++
This method gets 1 required argument for views class and optional keyword arguments:
1. `model=None`, model class for views. This argument is required if views class doesn't have 'model' property.
2. `base_url=None`, base URL for views. If empty, then lower-case model name is used, so base URL becomes '%(model)s/'.
3. `prefix=None`, prefix for URL names. If empty, then lower-case model name is used.
smarter.GenericViews
~~~~~~~~~~~~~~~~~~~~
| **model**
| - class property, model class for views
|
| **defaults**
| - class property, dict with default options applied to all actions until being overriden by `options`
|
| **options**
| - class property, dict for views configration, each key corresponds to single action like 'add', 'edit', 'remove' etc.
|
| **deny**\(``request, message=None``)
| - method, is called when action is not permitted for user, raises ``PermissionDenied`` exception or can return ``HttpResponse`` object for redirecting or rendering some page
|
| **get_url**\(``action, *args, **kwargs``)
| - method, returns url for given action name
|
| **get_form**\(``request, **kwargs``)
| - method, returns form for request
|
| **get_object**\(``request, **kwargs``)
| - method, returns single object for request
|
| **get_objects_list**\(``request, **kwargs``)
| - method, returns objects for request
|
| **get_template**\(``request_or_action``)
| - method, returns template name or sequence of template names by action name or per-request
|
| **get_param**\(``self, request_or_action, name, default=None``)
| - method, returns option parameter by name for action or per-request
|
| **get_initial**\(``self, request``)
| - method, returns form initial data per-request
|
| **<action>**\(``request, **kwargs``)
| - method, 1st (starting) handler in default pipeline
|
| **<action>__perm**\(``request, **kwargs``)
| - method, 2nd handler in default pipeline, checks extended permissions, e.g. per-object permissions (basic checks are handler separatelly)
|
| **<action>__form**\(``request, **kwargs``)
| - method, 3rd handler in default pipeline, manages form processing
|
| **<action>__save**\(``request, form, **kwargs``)
| - method, called from **<action>__form** when form is ready to save, saves the form and returns saved instance
|
| **<action>__post**\(``request, **kwargs``)
| - method, 4th handler in default pipeline for post-processing: save messages, extend render context, etc.
|
| **<action>__done**\(``request, **kwargs``)
| - method, 5th (last) view handler in default pipeline, performs render or redirect
Pipeline
~~~~~~~~
Each action like 'add', 'edit' or 'remove' is a **pipeline**: a sequence (list) of methods called one after another. A result of each method is passed to the next one.
The result is either **None** or **dict** or **HttpResponse** object:
1. **None** - result from previous pipeline method is used for next one,
2. **dict** - result is passed to next pipeline method,
3. **HttpResponse** - returned immidiately as view response.
For example, 'edit' action pipeline is: 'edit' -> 'edit__perm' -> 'edit__form' -> 'edit__post' -> 'edit__done'.
Note about **__perm** step. Basic permissions are checked **before** pipeline start view (e.g 'edit'), as if view were decorated with ``permission_required`` decorator. Actualy we're not using decorator, because we need to call our custom ``deny()`` method if permissions are not sufficient, but it's not the key. The key is **you don't need to check basic permissions in custom __perm method, it's necessary for per-object permissions checks.**
========== ===================================== ===================================================
Method Parameters Result
========== ===================================== ===================================================
edit ``request, **kwargs`` 'pk' ``{'obj': obj, 'form': {'instance': obj}}``
edit__perm ``request, **kwargs`` 'obj', 'form' pass (``None``) or ``PermissionDenied`` exception
edit__form ``request, **kwargs`` 'obj', 'form' | ``{'form': form, 'obj': obj, 'form_saved': True}``
| - form successfully saved
| ``{'form': form, 'obj': obj}``
| - first open or form contains errors
edit__post ``request, **kwargs`` pass (``None``) by default
'obj', 'form', 'form_saved'
edit__done ``request, **kwargs``
'obj', 'form', 'form_saved' render template or redirect to
``obj.get_absolute_url()``
========== ===================================== ===================================================
Note, that in general you won't need to redefine pipeline methods, as in many cases custom behavior can be reached with declarative style using **options**. If you're going too far with overriding views, that may mean you'd better write some views from scratch separate from "smarter".
Reversing URLs
~~~~~~~~~~~~~~
Every action mapped to named URL. Names are composed as::
[site prefix][delimiter][views prefix][delimiter][action]
Where:
- **site prefix** is 'prefix' parameter in `smarter.Site`_ constructor
- **delimiter** is 'delim' paratemer in `smarter.Site`_ constructor
- **views prefix** is 'prefix' parameter in `Site.register`_ method
So, in `Getting started`_ example named URLs are 'page-add', 'page-edit', 'page-remove', etc., as we don't provide any custom prefixes and delimiter is '-' by default.
Pipeline example
----------------
For deeper understanding here's an example of custom pipeline for 'edit' action. It's not actually a **recommended** way, as we can reach the same effect without overriding ``edit`` method by defining ``options['edit']['initial']``, but it illustrates the principle of pipeline.
.. sourcecode:: python
import smarter
class PageViews(smarter.GenericViews):
model = Page
def edit(request, pk=None):
# Custom initial title
initial = {'title': request.GET.get('title': '')}
return {
'obj': self.get_object(request, pk=pk),
'form' {'initial': initial, 'instance': obj}
}
def edit__perm(request, **kwargs):
# Custom permission check
if kwargs['obj'].owner != request.user:
return self.deny(request)
def edit__form(request, **kwargs):
# Actually, nothing custom here, it's totally generic:
# we should validate & save form and then return dict
# with 'form_saved' set to True if it's ok.
kwargs['form'] = self.get_form(request, **kwargs)
if kwargs['form'].is_valid():
kwargs['obj'] = self.edit__save(request, **kwargs)
kwargs['form_saved'] = True
return kwargs
def edit__done(request, obj=None, form=None, form_saved=None):
# Custom redirect to pages index on success
if form_saved:
# Success, redirecting!
return redirect(self.get_url('index'))
else:
# Start edit or form has errors
return render(request, self.get_template(request),
{'obj': obj, 'form': form})
Complete example
----------------
| You may look at complete example source here:
| https://github.com/05bit/django-smarter/tree/master/example
License
-------
Copyright (c) 2013, Alexey Kinyov <rudy@05bit.com>
Licensed under BSD, see LICENSE for more details.