django-catalyst


Adds chained views to Django (similar to chained actions from Catalyst)

Adds chained views to Django (similar to chained actions from Catalyst). For an introduction to Catalyst's dispatch types, see this tutorial.

Warning * the API is definitely not stable and the code is not commented (I intend to fix this) * although the code is just a quick hack, it can be hard to understand by a beginner ... but just send me an email if you experience issues / bugs (I would love to get bug reports / feature requests) * it is not production ready (unit-testing is missing for example)

Next on my TODO list

Purpose

Consider the following:

/user/30/article/25/delete

And then you write: 1. one view that gets all users for "/users" 1. one view that gets user with ID 30 for "/30" 1. one view that gets all articles of user ID 30 for "/articles" 1. one view that gets article ID 25 for "/25 1. one view that deletes the selected article for "/delete"

The difference between this approach and simple method calls is that even the "partial views" are still views that can return HttpResponse objects. When that happens then the chain of execution is short-circuited and no other view is executed.

You can have complex URLs like ... /user/30/settings/edit /user/30/article/25/delete /user/30/article/25/comments/list /user/30/article/25/comments/20/mark_spam /user/30/article/25/similar_articles/list

The advantages of chained views, as I see it ... * logic that can be easily discovered (the convention used is easy to understand and remembered) * no redundant logic ... the view only contains the necessary logic for its purpose and nothing more * no redundant parameters in views * no redundant method calls or error checking in views * because there is no redundancy, you can reconfigure the chain segments and refactor the code more easily * because the conventions are clear, you can automatically generate the URLs * from my experience, chained views invites you to write reusable code

I know that a lot of "magic" seems to happen, making implicit conventions, but I've worked with chained actions in Catalyst, and they are easy to understand, use and debug.

USAGE

settings.py

MIDDLEWARE_CLASSES = ( # ... 'dcatalyst.middleware.CatalystMiddleware', )

Currently this only initiates a "context" dictionary on the request object ... used to pass around data between views in the same chain.

views.py

The view chain is a queue of views. The request gets passed through those views, each view grabbing a relevant piece of the GET request.

So to chain a view onto another view, you use the following syntax to decorate your view ... @Chained(method=previous_view) def all_articles(request, arg1, arg2): # ...

What happens is that "previous_view" gets called before "all_articles". "previous_view" captures the arguments from the URL that it needs, then passes on control to "all_articles".

Example

``` from dcatalyst import Chained

...

@Chained() def user(req, user_id='current'): if user_id == 'current': user_id = req.user.id # req.context is a hash initialized by the the dcatalyst middleware # it is used to pass data between views in the same chain user = User.objects.get(user_id) if not user: redirect_to("user_not_found.html") # or something

@Chained(method=user) def article(req): req.context['articles'] = req.context['user'].articles # you could add extra filtering here # like selecting only the "active" articles

@Chained(method=article) def list(req): # req.context contains "articles", a QuerySet that gets passed to the template return direct_to_template(req, 'list.html', extra_context=req.context)

@Chained(method=article, path_part='') def item(req, article_id): articles = req.context['articles'] req.context['item'] = articles.get(id=article_id) if not req.context['item']: # the context passed still has the list of all articles # no other view is executed # this example could be re-factored to include a 404 header return direct_to_template(req, "article_not_found.html", extra_context=req.context)

@Chained(method=item) def show(req): # req.context contains "item", # the current selected article that gets passed to the template return direct_to_template(req, 'show.html', extra_context=req.context)

@Chained(method=item) def delete(req): req.context['item'].delete() return direct_to_template(req, 'deleted.html', extra_context=req.context) ```

Rule: If the method (view) returns a result, any result ... then the chain is interrupted and no other method (view) is executed (as it happens in view "item").

Chained views make a good fit for Django's ORM laziness. When you create a QuerySet it doesn't get executed, and you can make further filtering of the QuerySet in upcoming chained views.

So what happens when "views.show" gets called with params (user_id=30, article_id=12)?

The following views are executed in order: 1. user(request, user_id=30) 1. article(request) 1. item(request, article_id=12) 1. show(request)

Again, if everything went well, only "show" should return a HttpResponse. But if, say article with ID 12 isn't found, then "item" can short-circuit the chain by returning a HttpResponse by its own (like redirecting to a "not_found.html" template, or forwarding to a view).

urls.py

urlpatterns = patterns('', (r'^user/(\d+)/article/list/$', views.list), (r'^user/(\d+)/article/(\d+)/show$', views.show), (r'^user/(\d+)/article/(\d+)/delete$', views.delete), )

Named parameters (combined with non-named parameters) also work ...

urlpatterns = patterns('', (r'^user/(\d+)/article/list/$', views.list), (r'^user/(\d+)/article/(?P<article_id>\d+)/show$', views.show), (r'^user/(\d+)/article/(?P<article_id>\d+)/delete$', views.delete), )

Named parameters with default values also work ...

```

in urls.py ...

urlpatterns = patterns('', (r'^user/current/article/list/$', views.list), (r'^user/current/article/(?P\d+)/show$', views.show), # ... ) ```

Automatic generation of URLs

Explicit urls specified in urls.py are useful, but when you have a clear convention, they are kind of redundant. So this would be useful for eliminating the need to explicitly define urls, which is a good thing if you're lazy.

Because we are using these conventions, we could automatically generate the urls in urls.py for these chained methods.

The Chained decorator has this syntax ... @Chained(method=articles, path_part='list') def list_items(request): # do something ...

If path_part is omitted then it is defaulted to the name of the view method. Or when an empty string is given, then it doesn't capture any string, only the arguments (useful in the above example for the "item" view).

Example

In urls.py ...

``` from django.conf.urls.defaults import * from our_project import views

from dcatalyst.urls import include_url_pattern

urlpatterns = patterns('', include_url_pattern(views.list), include_url_pattern(views.show), include_url_pattern(views.delete), ) ```

This allows mixing of regular views, with chained views.

Or shorter:

``` from django.conf.urls.defaults import * from our_project import views

from dcatalyst.urls import chained_patterns

urlpatterns = chained_patterns('', views.list, views.show, views.delete, ) ```

In our example above, the following URLs would be automatically generated ...

(r'^user/(?P<user_id>\d+|current)/article/list/$', views.list), (r'^user/(?P<user_id>\d+|current)/article/(?P<article_id>\d+)/show/$', views.show), (r'^user/(?P<user_id>\d+|current)/article/(?P<article_id>\d+)/delete/$', views.delete),

Yes, for "user_id", because a default value is specified in the method definition, that default value is placed as an option instead of a numeric ID. I could just get rid of the parameter, but for now it is easier to deal with this.

Project Information

Labels:
python django catalyst