Package: django-smarter
Another approach for declarative style generic views for Django. I beleive, it’s a bit smarter :)OverviewSo many times we have to write:@login_requireddefedit_post(request,pk):post=get_object_or_404(Post,pk=pk)ifrequest.method=='POST':form=EditPostForm(request.POST,instance=post)ifform.is_valid():post=form.save()returnredirect(post.get_absolute_url())else:form=EditPostForm()returnrender(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:classPostViews(smarter.GenericViews):model=Postoptions={'add':{'form':NewPostForm,'decorators':(login_required,)},'edit':{'form':EditPostForm,'decorators':(login_required,)},'remove':{'decorators':(login_required,)}}That’s it.Changes in v1.0API 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 aresome hints that may help you with migration. I’m actually successfully migrated my real-production project, so the hints are based on “real-battle” example.ContributorsFabio SantosSameer Al-SakranThank you, comrades! :)InstallationRequirements:Django >= 1.4Installation:pip install django-smarterYoumayaddsmarterto yourINSTALLED_APPSto get default templates and tests, but youdon’t have to:INSTALLED_APPS=(# ...'smarter',# ...)Then you should define your views and include them in URLs, seeGetting startedsection below.Getting startedCreate your modelsLet’s define a simple model:classPage(models.Model):owner=models.ForeignKey('auth.User')title=models.CharField(max_length=100)text=models.TextField()def__unicode__(self):returnself.titleRegister viewsNow you can add generic views for the model.In yoururls.py:importsmarterfrommyapp.modelsimportPagesite=smarter.Site()site.register(smarter.GenericViews,Page)urlpatterns=patterns('',url(r'^',include(site.urls)),# other urls ...)This code creates generic views forPagemodel, accessed by urls:/page//page/add//page/<pk>//page/<pk>/edit//page/<pk>/remove/Customize viewsSubclass fromsmarter.GenericViewsand set custom options and/or override methods.fromdjango.contrib.auth.decoratorsimportlogin_requiredimportsmarterfrom.modelsimportPageclassPageViews(smarter.GenericViews):model=Pageoptions={'add':{'decorators':(login_required,)'exclude':('owner',)},}defadd__save(self,request,form,**kwargs):obj=form.save(commit=False)obj.owner=request.userobj.save()returnobjAnd don’t forget to register new views inurls.py:importsmarterfrommyapp.viewsimportPageViewssite=smarter.Site()site.register(PageViews)# model argument is not required as model is already set in PageViewsurlpatterns=patterns('',url(r'^',include(site.urls)),)Customize templatesIn the example above each URL by default to template.URLTemplateContext/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:('%(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:you may override matching templatesyou may set ‘template’ key inPageViews.optionsfor each actionyou may override default search paths by settings newPageViews.defaults(readOptionssection for details)Singleton SiteA very special instance ofsmarter.Siteis in the smarter module. It allows you to register your applications’ views outside your urls.py file, and works well withautodiscover().Here is smarter_views.py in your app:fromsmarterimportsite,GenericViewsfrommodelsimportModelclassViews(GenericViews):model=Model# ...site.register(Views)… And urls.py:fromdjango.conf.urlsimportpatterns,include,urlimportsmartersmarter.autodiscover()urlpatterns=patterns('',url(r'^',include(smarter.site.urls)),)This is mostly recommended for non-reusable applications local to your Django project.API referenceActionsActionsare 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.Insmarter.GenericViewsclass such actions are defined by default:ActionURLView methodNamed URLindex/index(request)[prefix]-[model]-indexadd/add/add(request)[prefix]-[model]-adddetails/<pk>/details(request, pk)[prefix]-[model]-detailsedit/<pk>/edit/edit(request, pk)[prefix]-[model]-editremove/<pk>/remove/remove(request, pk)[prefix]-[model]-removeWhat is[prefix]? Prefix is defined forsmarter.Siteinstance:site=smarter.Site(prefix='myapp')site.register(PageViews)# ...So, itcan be emptyand URL names without prefix are defined as[model]-index. Please, readReversing urlssection for more details.OptionsOptionsis aGenericViews.optionsdict, class property, it contains actions names as keys and actions parameters as values. Parameters structure is:{'url':<stringforurlpattern>,'form':<formclass>,'decorators':<tuple/listofdecorators>,'fields':<tuple/listofformfields>,'exclude':<tuple/listofexcludedformfields>,'initial':<tuple/listofformfieldsinitializedbyrequest.GET>,'permissions':<tuple/listofrequiredpermissions>,'widgets':<dictforwidgetsoverrides>,'help_text':<dictforhelptextsoverrides>,'required':<dictforrequiredfieldsoverrides>,'template':<stringtemplatename>,'redirect':<stringorcallablereturningredirectpath>}Every key here is optional. So, here’s how options can be defined for views:importsmarterclassViews(smarter.GenericViews):model=<model>defaults=<defaultparameters>options={'<action 1>':<parameters1>,'<action 2>':<parameters2>}And here’sGenericViews.defaultsclass attribute: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 inGenericViews.defaults. Note, that defaults are applied toall actions.Action names and URLsActions are named so they can be mapped to views methods and they should not override reserved attributes and methods, so they:must contain onlylatin symbols and ‘_’ or ‘-’,no spacescan’tbe in this list: ‘model’, ‘defaults’, ‘options’, ‘deny’can’tstart with ‘-’, ‘_’ or ‘get_’can’tcontain ‘__’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:{'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.SiteSite(prefix=None, delim=’-‘)- constructorregister(views, model=None, base_url=None, prefix=None)- method to add your views for modelurls- property, returns URLs sequence for all registered views that can be included inurlpatternsautodiscover- method which goes oversettings.INSTALLED_APPSand looks for apps withsmarter_viewsmodules, which it imports, so they can register their views.SiteConstructor gets two keyword arguments: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’.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.registerThis method gets 1 required argument for views class and optional keyword arguments:model=None, model class for views. This argument is required if views class doesn’t have ‘model’ property.base_url=None, base URL for views. If empty, then lower-case model name is used, so base URL becomes ‘%(model)s/’.prefix=None, prefix for URL names. If empty, then lower-case model name is used.smarter.GenericViewsmodel- class property, model class for viewsdefaults- class property, dict with default options applied to all actions until being overriden byoptionsoptions- 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, raisesPermissionDeniedexception or can returnHttpResponseobject for redirecting or rendering some pageget_url(action, *args, **kwargs)- method, returns url for given action nameget_form(request, **kwargs)- method, returns form for requestget_object(request, **kwargs)- method, returns single object for requestget_objects_list(request, **kwargs)- method, returns objects for requestget_template(request_or_action)- method, returns template name or sequence of template names by action name or per-requestget_param(self, request_or_action, name, default=None)- method, returns option parameter by name for action or per-requestget_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>__formwhen 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 redirectPipelineEach action like ‘add’, ‘edit’ or ‘remove’ is apipeline: a sequence (list) of methods called one after another. A result of each method is passed to the next one.The result is eitherNoneordictorHttpResponseobject:None- result from previous pipeline method is used for next one,dict- result is passed to next pipeline method,HttpResponse- returned immidiately as view response.For example, ‘edit’ action pipeline is: ‘edit’ -> ‘edit__perm’ -> ‘edit__form’ -> ‘edit__post’ -> ‘edit__done’.Note about__permstep. Basic permissions are checkedbeforepipeline start view (e.g ‘edit’), as if view were decorated withpermission_requireddecorator. Actualy we’re not using decorator, because we need to call our customdeny()method if permissions are not sufficient, but it’s not the key. The key isyou don’t need to check basic permissions in custom __perm method, it’s necessary for per-object permissions checks.MethodParametersResulteditrequest, **kwargs‘pk’{'obj': obj, 'form': {'instance': obj}}edit__permrequest, **kwargs‘obj’, ‘form’pass (None) orPermissionDeniedexceptionedit__formrequest, **kwargs‘obj’, ‘form’{'form': form, 'obj': obj, 'form_saved': True}- form successfully saved{'form': form, 'obj': obj}- first open or form contains errorsedit__postrequest, **kwargs‘obj’, ‘form’, ‘form_saved’pass (None) by defaultedit__donerequest, **kwargs‘obj’, ‘form’, ‘form_saved’render template or redirect toobj.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 usingoptions. If you’re going too far with overriding views, that may mean you’d better write some views from scratch separate from “smarter”.Reversing URLsEvery action mapped to named URL. Names are composed as:[site prefix][delimiter][views prefix][delimiter][action]Where:site prefixis ‘prefix’ parameter insmarter.Siteconstructordelimiteris ‘delim’ paratemer insmarter.Siteconstructorviews prefixis ‘prefix’ parameter inSite.registermethodSo, inGetting startedexample named URLs are ‘page-add’, ‘page-edit’, ‘page-remove’, etc., as we don’t provide any custom prefixes and delimiter is ‘-’ by default.Pipeline exampleFor deeper understanding here’s an example of custom pipeline for ‘edit’ action. It’s not actually arecommendedway, as we can reach the same effect without overridingeditmethod by definingoptions['edit']['initial'], but it illustrates the principle of pipeline.importsmarterclassPageViews(smarter.GenericViews):model=Pagedefedit(request,pk=None):# Custom initial titleinitial={'title':request.GET.get('title':'')}return{'obj':self.get_object(request,pk=pk),'form'{'initial':initial,'instance':obj}}defedit__perm(request,**kwargs):# Custom permission checkifkwargs['obj'].owner!=request.user:returnself.deny(request)defedit__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)ifkwargs['form'].is_valid():kwargs['obj']=self.edit__save(request,**kwargs)kwargs['form_saved']=Truereturnkwargsdefedit__done(request,obj=None,form=None,form_saved=None):# Custom redirect to pages index on successifform_saved:# Success, redirecting!returnredirect(self.get_url('index'))else:# Start edit or form has errorsreturnrender(request,self.get_template(request),{'obj':obj,'form':form})Complete exampleYou may look at complete example source here:https://github.com/05bit/django-smarter/tree/master/exampleLicenseCopyright (c) 2013, Alexey Kinyov <rudy@05bit.com>
Licensed under BSD, see LICENSE for more details.