Changes between Initial Version and Version 1 of NewManipulators


Ignore:
Timestamp:
Jul 19, 2006, 7:18:07 PM (18 years ago)
Author:
brantley
Comment:

Started

Legend:

Unmodified
Added
Removed
Modified
  • NewManipulators

    v1 v1  
     1== New Manipulators ==
     2[''This is section is a lot of explination, to get right to some code scroll down.'']
     3Although validation aware models are great for database integrity, and should be leveraged within the manipulator system, it is still necessary for manipulators to do most of the work.  There are three main steps to the process of taking post data and updating or creating a model object with that data:
     4 1. Gather and convert the post data.
     5 2. Validate the converted data.
     6 3. Apply the data to the object and save, aka do something with the data.
     7
     8A validation aware model alone cannot complete this task as information needs to be provided that details the sorts of fields that should be used, and processes are needed to convert from string / post data to the pythonic data.  Furthermore many manipulation and form tasks require form fields that do not map directly to a model field, for instance a password changing form that requires a password to be given twice.  This example also illuminates how a form field might map directly to a model field, but may not want to give its direct value to the field.  As in a password, one probably wouldn't want: user.password = data['password'], but rather: user.set_password(data['password']).
     9
     10So we change Manipulators some, and then add model validation.  But we need to make our manipulator system leverage it.  In other words, when our model throws a ValidationError, we need to add it to our errors and then present our form back to the user.  This should be made simple for the developer so that they don't have to create a try/except block every time they want to save an object.
     11
     12This all means that validation exceptions could be thrown at any point in our manipulation process: conversion, validation, and model object saving.  There might even be more steps in the future, or with manipulators that aren't HTML based but something else like JSON.
     13
     14There are a few ways of doing this, from the developer's perspective.  Currently manipulator process in a custom view for the developer is needlessly complicated.  Compare the [http://www.djangoproject.com/documentation/forms/#using-the-changemanipulator current process] to the [http://code.djangoproject.com/wiki/CookBookManipulatorCustomManipulator rather basic one that added to the cookbook].  But with model validation it complicates both with even more code for checking for errors after the object.save().
     15
     16So it occured to me that a form was a lot like exception.  At various points during the manipulation process it becomes evident that user input is needed, and that we cannot continue on.  This comes to a head at one point in the view where it returns a render_to_response('form_template', {'form': form}).  So why not make the Form class an actual Exception?  This way we can raise the form all the way back to the view.  Here then is the basic form of my proposed view:
     17{{{
     18#!python
     19def create_article(request):
     20    try:
     21        m = Article.CreateManipulator(request)
     22        article = m.save()
     23        return HttpResponseRedirect('/articles/update/%d/' % article.id)
     24    except Form, form:
     25        return render_to_response('articles/create.html', {'form': form})
     26}}}
     27
     28As you can see, our form is now more of an exception.  And our manipulation process is very simple, most of it takes place in the constructor itself, if durring that time a form is raised, it is presented.  Otherwise call "save()" on our manipulator.  In this case it creates and returns a new article. It will also raise a form if the model validation returns an error.  If no form is raised, we are done and return our Response.
     29
     30
     31{{{
     32#!python
     33class Manipulator(object):
     34    """     
     35    A Manipulator "manipulates" data and objects. 
     36    Sample usage with Article.CreateManipulator:
     37   
     38    def create_article(request):
     39    try:
     40        m = Article.CreateManipulator(request)
     41        article = m.save()
     42        return HttpResponseRedirect(article.get_update_url())
     43    except Form, form:
     44        return render_to_response('articles/create.html', {'form': form})
     45   
     46    As you can see, if a Manipulator needs to get information with a form, it
     47    will raise it's form.  At that point you can display it.
     48
     49    To create a custom manipulator you have four functions that you may
     50    want to override, each is a step in the maniplator process:
     51        * __init__
     52        * convert
     53        * validate
     54        * save
     55       
     56    And there are four more attributes that you will deal with:
     57        * fields - A sequence that defines the fields of the Manipulator
     58        * errors - A dictionary that holds a mapping of field-name -> error list
     59        * data - A dictionary of the current data.  Remember that this may be
     60                    raw data if conversion has not yet been done.
     61        * form - This is actually a property that returns the form to be given
     62                    in the template context.
     63    """
     64   
     65    def __init__(self, request):
     66        """
     67        Initializes the Manipulator and instigates the process. 
     68        Mainly you will want to define your fields here.
     69        """
     70        self.fields = (
     71            SlugField("slug", attributes={'class': 'slug'}),
     72            TextField("title", label="Headline", maxlength=60, attributes={'class': 'title'}),
     73            LargeTextField("body", label="Story", attributes={'class': 'body'}),
     74            DateTimeField("pub_date", label="This article will publish on this date"),
     75        )
     76        self.process(request.POST)
     77   
     78    def convert(self, data):
     79        """
     80        This conversion function will happen AFTER the data is already converted
     81        by the fields.  More conversion can be done here.
     82        """
     83        pass
     84   
     85    def validate(self, data):
     86        """
     87        Here you can further validation after the fields have already had their
     88        go at it.
     89        """
     90        pass
     91   
     92    def save(self):
     93        """
     94        Once the data has been converted and validated it is ready to be
     95        processed.  Usually this will entail updating an model instance.  To aid in
     96        that task, there is a convenience self.update <see below>.
     97        """
     98        pass
     99   
     100    ### Utility Functions ###
     101    def update(self, object, data):
     102        """
     103        Updates and saves a model object.  This will raise a form if there are any errors.
     104        """
     105        self.errors = object.update(data)
     106        object.save()
     107        if (self.errors):
     108            raise Form
     109        return object
     110           
     111    def add_errors(self, field, errors):
     112        "A convenience function to add errors to a field's error list."
     113        errors = list(errors)
     114        self.errors.setdefault(field.name, []).extend(errors)
     115   
     116    ### Interneal functions ###
     117    def process(self, data):
     118        "Perform the manipulation process."
     119        if (not data):
     120            raise form
     121        self.data = data.copy()
     122        self.errors = {}
     123        self._convert()
     124        self._validate()
     125   
     126    def _get_form(self):
     127        return Form(self.fields, self.data, self.errors)
     128    form = property(_get_form)
     129   
     130    def _convert(self)
     131        """
     132        Goes through each field and converts the data in place on the data dictionary.
     133        This will raise a form if there are validation errors by the end.
     134        """
     135        for field in self.fields:
     136            if (field.name in self.data):
     137                try:
     138                    self.data[field.name] = field.to_python(self.data[field.field_name])
     139                except (validators.ValidationError, validators.CriticalValidationError), e:
     140                    self.errors.setdefault(field.field_name, []).extend(e.messages)
     141        if (self.errors):
     142            raise self.form
     143        self.convert(self.data)
     144   
     145    def _validate(self):
     146        """
     147        Goes through each field and validates the data.  This will raise a form if there are
     148        validation errors by the end.
     149        """
     150        for field in self.fields:
     151            for validator in field.validators:
     152                try:
     153                    validator(self.data[field.name])
     154                except (validators.ValidationError, validators.CriticalValidationError), e:
     155                    self.add_errors(e.messages)
     156        if (self.errors):
     157            raise self.form
     158        self.validate(self.data)
     159   
     160    ### Default Properties ###
     161    data = {}
     162    errors = {}
     163    fields = ()
     164
     165### Manipulator factories ###
     166def create_manipulator(model_cls):
     167    "Factory, making a CreateManipulator for a specified model class"
     168   
     169    class CreateManipulator(Manipulator):
     170        def __init__(self, request):
     171            self.fields = model_cls._meta.create_fields
     172            self.process(request.POST)
     173           
     174        def save(self):
     175            self.update(model_cls(), data)
     176           
     177    return CreateManipulator
     178   
     179def update_manipulator(model_cls):
     180    "Factory, making a UpdateManipulator for a specified model class"
     181   
     182    class UpdateManipulator(Manipulator):
     183        def __init__(self, request, object):
     184            self.object = object
     185            self.process(request.POST)
     186            self.fields = model_cls._meta.update_fields
     187           
     188        def save(self):
     189            self.update(self.object, data)
     190           
     191    return UpdateManipulator
     192
     193### Attach to classes ###
     194Article.CreateManipulator = create_manipulator(Article)
     195Article.UpdateManipulator = update_manipulator(Article)
     196
     197### Custom Manipulator ###
     198class CustomManipulator(Manipulator):
     199    def __init__(self, request):
     200        self.fields = (
     201            SlugField("slug", attributes={'class': 'slug'}),
     202            TextField("title", label="Headline", maxlength=60, attributes={'class': 'title'}),
     203            LargeTextField("body", label="Story", attributes={'class': 'body'}),
     204            DateTimeField("pub_date", label="This article will publish on this date"),
     205        )
     206        self.process(request.POST)
     207   
     208    def save(self):
     209        Article=
     210        pass
     211       
     212### Template Tag ###
     213{% render-form form %}
     214# or
     215{% render-form-row form.slug %}
     216# or ... etc.
     217
     218### Generic Views ###
     219def create_view(model, template=None):
     220    """
     221    Generic create view.  Redirects to object.get_update_url() or
     222    object.get_absolute_url() when it's done.
     223   
     224    TODO: Add all the other stuff already in the create view.
     225    """
     226    if template == None:
     227        template = "%s/%s_create.html" % (model._meta.app_label, model._meta.object_name.lower())
     228    def create_func(request):
     229        try:
     230            m = model.CreateManipulator(request)
     231            object = m.save()
     232            if hasattr(object, 'get_update_url'):
     233                return HttpResponseRedirect(object.get_update_url())
     234            else:
     235                return HttpResponseRedirect(object.get_absolute_url())
     236        except Form, form:
     237            return render_to_response(template, {'form': form})
     238    return create_func
     239   
     240def update_view(model, template=None):
     241    """
     242    Generic update view.  Redirects to object.get_absolute_url() when it's done.
     243   
     244    TODO: Add all the other stuff already in the update view.
     245    """
     246    if template == None:
     247        template = "%s/%s_update.html" % (model._meta.app_label, model._meta.object_name.lower())
     248    def update_func(request, pk):
     249        object = get_object_or_404(model, pk=pk)
     250        try:
     251            m = model.UpdateManipulator(request)
     252            m.save()
     253            return HttpResponseRedirect(object.get_absolute_url())
     254        except Form, form:
     255            return render_to_response(template, {'form': form, 'object': object})
     256    return update_func   
     257
     258### urls.py usage ###
     259from app.models import Article
     260('^articles/create/%d/$', 'django.views.generic.create_update.create_view', {'model':Article})
     261('^articles/update/$',      'django.views.generic.create_update.update_view', {'model':Article})
     262
     263### Custom views ###
     264def create_article(request):
     265    try:
     266        m = Article.CreateManipulator(request)
     267        article = m.save()
     268        return HttpResponseRedirect('/articles/update/%d/' % article.id)
     269    except Form, form:
     270        return render_to_response('articles/create.html', {'form': form})
     271           
     272def update_article(request, id):
     273    article = get_object_or_404(Article, pk=id)
     274    try:
     275        m = Article.UpdateManipulator(request, article)
     276        m.save()
     277        return HttpResponseRedirect('/articles/%d/' % article.id)
     278    except Form, form:
     279        return render_to_response('articles/update.html', {'form': form, 'object': object})
     280}}}
     281
     282== Compatibility ==
     283There would be some changes needed in some other django code for all of this to work out right:
     284 * The '''FormWrapper''' class would be destroyed, I believe, and would be supplanted by '''Form'''.
     285 * Form fields would alter somewhat, most of it would be cosmetic (e.g. changing "html2python" to "to_python" and adding a "label" and "attributes" keyword arguments in the constructor)
     286 * Models would gain an "update" function that took a dictionary, applied it to the properties, and then validated.
     287 * Meta in the Models would gain a "update_fields" and "create_fields" attribute.  Here you could define the fields used for your model's default Manipulators.
Back to Top