django-smarter icon indicating copy to clipboard operation
django-smarter copied to clipboard

Declarative style generic views for Django.

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 | | (request, **kwargs) | - method, 1st (starting) handler in default pipeline | | __perm(request, **kwargs) | - method, 2nd handler in default pipeline, checks extended permissions, e.g. per-object permissions (basic checks are handler separatelly) | | __form(request, **kwargs) | - method, 3rd handler in default pipeline, manages form processing | | __save(request, form, **kwargs) | - method, called from __form when form is ready to save, saves the form and returns saved instance | | __post(request, **kwargs) | - method, 4th handler in default pipeline for post-processing: save messages, extend render context, etc. | | __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 [email protected] Licensed under BSD, see LICENSE for more details.