Changes between Version 4 and Version 5 of NewManipulators


Ignore:
Timestamp:
Aug 21, 2006, 5:35:09 PM (18 years ago)
Author:
brantley
Comment:

Update to present.

Legend:

Unmodified
Added
Removed
Modified
  • NewManipulators

    v4 v5  
    77 3. Apply the data to the object and save, aka do something with the data.
    88
    9 A 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'])}}}.
     9A validation aware model alone cannot complete these tasks 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'])}}}.
    1010
    1111So 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.
    1212
    13 This 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.
     13This 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.
    1414
    1515There 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().
     
    1818{{{
    1919#!python
    20 def create_article(request):
     20def create_poll(request):
    2121    try:
    22         m = Article.CreateManipulator(request)
    23         article = m.save()
    24         return HttpResponseRedirect('/articles/update/%d/' % article.id)
     22        m = Poll.CreateManipulator()
     23        poll = m.process(request.POST)
     24        return HttpResponseRedirect('/poll/%d/' % poll.id)
    2525    except Form, form:
    26         return render_to_response('articles/create.html', {'form': form})
     26        return render_to_response('poll/create.html', {'form': form})
    2727}}}
    2828
    29 As 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.
    30 
     29As you can see, our form is now more of an exception.  And our manipulation process is very simple, most of it takes place in {{{process()}}}, and if at any point a form is raised, it is presented.  Otherwise the manipulator calls and returns {{{save()}}}.  In this case it creates and returns a new poll object. 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.
     30
     31There is a lot more code to this, as FormWrapper and FormFieldWrapper and some other pieces have been rewritten.  Here is the rub, though:
    3132
    3233{{{
     
    3536    """     
    3637    A Manipulator "manipulates" data and objects. 
    37     Sample usage with Article.CreateManipulator:
    38    
    39     def create_article(request):
     38    Sample usage with Poll.CreateManipulator:
     39   
     40    def create_poll(request):
    4041    try:
    41         m = Article.CreateManipulator(request)
    42         article = m.save()
    43         return HttpResponseRedirect(article.get_update_url())
     42        m = Poll.CreateManipulator()
     43        poll = m.process(request.POST)
     44        return HttpResponseRedirect('/poll/%d/' % poll.id)
    4445    except Form, form:
    45         return render_to_response('articles/create.html', {'form': form})
     46        return render_to_response('poll/create.html', {'form': form})
    4647   
    4748    As you can see, if a Manipulator needs to get information with a form, it
    48     will raise it's form.  At that point you can display it.
     49    will raise it's form, which can then be placed right into a template context.
    4950
    5051    To create a custom manipulator you have four functions that you may
     
    6364                    in the template context.
    6465    """
    65    
    66     def __init__(self, request):
    67         """
    68         Initializes the Manipulator and instigates the process. 
    69         Mainly you will want to define your fields here.
    70         """
    71         self.fields = (
    72             SlugField("slug", attributes={'class': 'slug'}),
    73             TextField("title", label="Headline", maxlength=60, attributes={'class': 'title'}),
    74             LargeTextField("body", label="Story", attributes={'class': 'body'}),
    75             DateTimeField("pub_date", label="This article will publish on this date"),
    76         )
    77         self.process(request.POST)
     66
     67    def __getitem__(self, k):
     68        "Allows access of the fields via the brackets, like: manipulator['field_name']"
     69        for f in self.fields:
     70            if (f.name == k):
     71                return f
     72        raise KeyError, "Could not find field with the name %r." % k
     73       
     74    def __repr__(self):
     75        return "Manipulator <%r, %r, %r>" % (self.fields, self.data, self.errors)
     76   
     77    def __init__(self):
     78        # Properties that must be created:
     79        self.fields = ()
     80        # Properties that can optionally be created:
     81        self.data = {}  # The default data given
     82        self.errors = {}
    7883   
    7984    def convert(self, data):
     
    9196        pass
    9297   
    93     def save(self):
     98    def save(self, data):
    9499        """
    95100        Once the data has been converted and validated it is ready to be
    96         processed.  Usually this will entail updating an model instance.  To aid in
    97         that task, there is a convenience self.update <see below>.
    98         """
    99         pass
    100    
    101     ### Utility Functions ###
    102     def update(self, object, data):
    103         """
    104         Updates and saves a model object.  This will raise a form if there are any errors.
    105         """
    106         self.errors = object.update(data)
    107         object.save()
    108         if (self.errors):
    109             raise Form
    110         return object
    111            
    112     def add_errors(self, field, errors):
    113         "A convenience function to add errors to a field's error list."
    114         errors = list(errors)
    115         self.errors.setdefault(field.name, []).extend(errors)
     101        processed.  Usually this will entail creating or updating an model instance.
     102        To aid in that task, there are convenience functions edit() and create(),
     103        see below for more details.
     104        """
     105        raise NotImplementedError
    116106   
    117107    ### Interneal functions ###
     
    119109        "Perform the manipulation process."
    120110        if (not data):
    121             raise form
    122         self.data = data.copy()
     111            # Here we want to delete any data we have, just in case this manipulator is being re-used.
     112            if 'data' in self.__dict__:
     113                del self.data
     114            if 'errors' in self.__dict__:
     115                del self.errors
     116            # Raise the form
     117            raise self.form
     118        self.data = copy_dict(data)   # copy_dict -> lambda m: dict((k, v) for k, v in m.items())
    123119        self.errors = {}
    124120        self._convert()
    125121        self._validate()
     122        return self._save()
    126123   
    127124    def _get_form(self):
     
    129126    form = property(_get_form)
    130127   
    131     def _convert(self)
     128    def _convert(self):
    132129        """
    133130        Goes through each field and converts the data in place on the data dictionary.
    134         This will raise a form if there are validation errors by the end.
    135         """
     131        This will raise a Form if there are validation errors by the end.
     132        """
     133       
    136134        for field in self.fields:
    137             if (field.name in self.data):
     135            name = field.name
     136            if (name in self.data):
    138137                try:
    139                     self.data[field.name] = field.to_python(self.data[field.field_name])
     138                    #~ self.data[name] = field.to_python(self.data[name])
     139                    self.data[name] = field.to_python(self.data[name])
    140140                except (validators.ValidationError, validators.CriticalValidationError), e:
    141                     self.errors.setdefault(field.field_name, []).extend(e.messages)
     141                    self.errors.setdefault(name, []).extend(e.messages)
    142142        if (self.errors):
    143143            raise self.form
    144144        self.convert(self.data)
     145        if (self.errors):
     146            raise self.form
    145147   
    146148    def _validate(self):
     
    149151        validation errors by the end.
    150152        """
     153       
    151154        for field in self.fields:
    152             for validator in field.validators:
    153                 try:
    154                     validator(self.data[field.name])
    155                 except (validators.ValidationError, validators.CriticalValidationError), e:
    156                     self.add_errors(e.messages)
     155            name = field.name
     156            #~ for validator in field.validators:
     157                #~ try:
     158                    #~ validator(self.data[name])
     159                #~ except (validators.ValidationError, validators.CriticalValidationError), e:
     160                    #~ self.errors.setdefault(name, []).extend(e.messages)
     161            errors = field.validate(self.data)
     162            if (errors):
     163                self.errors.setdefault(name, []).extend(errors)
    157164        if (self.errors):
    158165            raise self.form
    159166        self.validate(self.data)
     167        if (self.errors):
     168            raise self.form
     169   
     170    def _save(self):
     171        try:
     172            return self.save(self.data)
     173        except (validators.ValidationError, validators.CriticalValidationError), e:
     174            name = "Some field"
     175            self.errors.setdefault(name, []).extend(e.messages)
     176            raise self.form
    160177   
    161178    ### Default Properties ###
     
    164181    fields = ()
    165182
    166 ### Manipulator factories ###
     183### Manipulator Factories / Helpers ###
    167184def create_manipulator(model_cls):
    168     "Factory, making a CreateManipulator for a specified model class"
    169    
    170185    class CreateManipulator(Manipulator):
    171         def __init__(self, request):
    172             self.fields = model_cls._meta.create_fields
    173             self.process(request.POST)
    174            
    175         def save(self):
    176             self.update(model_cls(), data)
    177            
     186        def __init__(self):
     187            self.fields = forms.generate_fields(model_cls)
     188       
     189        def save(self, data):
     190            return create(Poll, data)
    178191    return CreateManipulator
    179    
    180 def update_manipulator(model_cls):
    181     "Factory, making a UpdateManipulator for a specified model class"
    182    
    183     class UpdateManipulator(Manipulator):
    184         def __init__(self, request, object):
     192
     193def edit_manipulator(model_cls):
     194    class EditManipulator(Manipulator):       
     195        def __init__(self, object):
     196            self.fields = forms.generate_fields(model_cls)
    185197            self.object = object
    186             self.process(request.POST)
    187             self.fields = model_cls._meta.update_fields
    188            
    189         def save(self):
    190             self.update(self.object, data)
    191            
    192     return UpdateManipulator
     198       
     199        def save(self, data):
     200            return forms.edit(self.object, data)
     201    return EditManipulator
     202
     203def create(Model, data):
     204    """
     205    Usefull in the .save of a custom manipulator, this will quickly create an object for you.
     206    """
     207    m = Model(**data)
     208    # In a validation aware model, any problems would arise here.
     209    m.save()
     210    return m
     211   
     212def edit(i, data):
     213    """
     214    Like create() above, this will update an object for you.
     215    """
     216    i.__dict__.update(data)
     217    # Also validation errors would arise here.
     218    i.save()
     219    return i
    193220
    194221### Attach to classes ###
    195 Article.CreateManipulator = create_manipulator(Article)
    196 Article.UpdateManipulator = update_manipulator(Article)
     222Poll.CreateManipulator = create_manipulator(Poll)
     223Poll.EditManipulator = edit_manipulator(Poll)
    197224
    198225### Custom Manipulator ###
    199226class CustomManipulator(Manipulator):
    200227    def __init__(self, request):
    201         self.fields = (
    202             SlugField("slug", attributes={'class': 'slug'}),
    203             TextField("title", label="Headline", maxlength=60, attributes={'class': 'title'}),
    204             LargeTextField("body", label="Story", attributes={'class': 'body'}),
    205             DateTimeField("pub_date", label="This article will publish on this date"),
    206         )
    207         self.process(request.POST)
     228        self.fields = generate_fields(Poll)   # Generate fields creates default fields for a Model
     229
     230        # Fields have an "attributes" attribute, that adds itself to the tag that is created for it.
     231        # Here, we change our class to "vPopupDateField" so that a javascript function will know to
     232        # add a Date popup window for when the field gets focus.
     233        self.fields['pub_date'].attributes = {'class': 'vPopupDateField'}
    208234   
    209235    def save(self):
    210236        # do something with the data...
    211237        pass
    212        
     238
     239### Custom views ###
     240def create_poll(request):
     241    try:
     242        m = Poll.CreateManipulator()
     243        poll = m.process(request)
     244        return HttpResponseRedirect('/polls/%d/' % poll.id)
     245    except Form, form:
     246        return render_to_response('polls/create.html', {'form': form})
     247           
     248def update_poll(request, id):
     249    poll = get_object_or_404(Poll, pk=id)
     250    try:
     251        m = Poll.EditManipulator(request, poll)
     252        m.process(request)
     253        return HttpResponseRedirect('/polls/%d/' % poll.id)
     254    except Form, form:
     255        return render_to_response('polls/update.html', {'form': form, 'poll': poll})
     256
    213257### Template Tag ###
    214 {% render-form form %}
    215 # or
    216 {% render-form-row form.slug %}
    217 # or ... etc.
     258#
     259# Forms should be renderable with a simple tag, that plops a form down for the user:
     260#
     261# <form action='.' method='post'>
     262#    {% render-form form %}
     263#    <input type='submit' value='Create Poll'/>
     264# </form>
     265#
     266# or {% render-form-row form.slug %} could be used to create just a row.  Many more options exist.
     267#
    218268
    219269### Generic Views ###
     
    229279    def create_func(request):
    230280        try:
    231             m = model.CreateManipulator(request)
    232             object = m.save()
     281            m = model.CreateManipulator()
     282            object = m.process(request)
    233283            if hasattr(object, 'get_update_url'):
    234284                return HttpResponseRedirect(object.get_update_url())
     
    239289    return create_func
    240290   
    241 def update_view(model, template=None):
     291def update_view(model, template=None, pk=None):
    242292    """
    243293    Generic update view.  Redirects to object.get_absolute_url() when it's done.
     
    250300        object = get_object_or_404(model, pk=pk)
    251301        try:
    252             m = model.UpdateManipulator(request)
    253             m.save()
     302            m = model.EditManipulator(object)
     303            m.process(request)
    254304            return HttpResponseRedirect(object.get_absolute_url())
    255305        except Form, form:
     
    257307    return update_func   
    258308
    259 ### urls.py usage ###
     309### urls.py generic usage ###
    260310from app.models import Article
    261 ('^articles/create/%d/$', 'django.views.generic.create_update.create_view', {'model':Article})
    262 ('^articles/update/$',      'django.views.generic.create_update.update_view', {'model':Article})
    263 
    264 ### Custom views ###
    265 def create_article(request):
    266     try:
    267         m = Article.CreateManipulator(request)
    268         article = m.save()
    269         return HttpResponseRedirect('/articles/update/%d/' % article.id)
    270     except Form, form:
    271         return render_to_response('articles/create.html', {'form': form})
    272            
    273 def update_article(request, id):
    274     article = get_object_or_404(Article, pk=id)
    275     try:
    276         m = Article.UpdateManipulator(request, article)
    277         m.save()
    278         return HttpResponseRedirect('/articles/%d/' % article.id)
    279     except Form, form:
    280         return render_to_response('articles/update.html', {'form': form, 'object': object})
     311('^polls/create/%d/$', 'django.views.generic.create_update.create_view', {'model':Poll})
     312('^polls/update/$',    'django.views.generic.create_update.update_view', {'model':Poll})
     313
    281314}}}
    282315
     316== Benefits ==
     317The benefits of this system include:
     318 * Custom form views become very simple, and intuitive.
     319 * Validation aware models are harnessed in this system.
     320 * Custom Manipulators could be defined in the model itself, which hightens the sense of everything in one place.  Likewise, it can be seperated out if it's too much code in one spot.
     321 * Defining Custom Manipulators allows one greater control of how the Admin deals with objects.  Having a field automatically update with the author's User object would be a snap.  Also, some options could move from the {{{class Admin:}}} into the Custom Manipulator, which would tighten things up a great deal.
     322 * Making your own Admin app now becomes a lot easier.  People often want to be able to simply plop a form down, and this gives them that ability.
     323 * Because the form is raised, like an exception, it allows the writer to assume the data is correct the whole way through.  But when there is a validation error, it is handled easily.
     324
    283325== Compatibility ==
    284 There would be some changes needed in some other django code for all of this to work out right:
    285  * The '''FormWrapper''' class would be destroyed, I believe, and would be supplanted by '''Form'''.
    286  * 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)
    287  * Models would gain an "update" function that took a dictionary, applied it to the properties, and then validated.
    288  * 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.
     326There would be many changes needed in some other django code for all of this to work out right:
     327 * The '''FormWrapper''' class would be destroyed, and would be supplanted by the easier '''Form'''.
     328 * Form fields would be altered, both cosmetically "html2python" -> "to_python", and functionally for optimization / simplicity
     329 * Models would have to gain the validation awareness that we've been talking about.
     330 * The Model metaclass would need to create default Create and Edit manipulators if they weren't supplied (this would be very easily done).
Back to Top