Changes between Initial Version and Version 1 of ModelMiddleware


Ignore:
Timestamp:
Apr 17, 2006, 12:00:29 AM (18 years ago)
Author:
Gevara
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • ModelMiddleware

    v1 v1  
     1'''Warning:''' Requires magic-removal!
     2
     3'''Warning:''' Experimental!
     4
     5This came up on IRC: someone wanted a way to supply versioning of the db contents from a single point in the system (such as an app). The app would then somehow recognize those fields that require version information to be saved and record their changes in the db.
     6
     7This then requires several problems to be resolved:
     8
     9 1. Gaining access to save() and delete() methods of any model, installed anywhere in a project.
     10 2. Leaving save() and delete() methods of models intact, as overriding them would mean breaking the "pluggable" idea behind this.
     11 3. Allowing for many pre/post callbacks to be inserted into the same model.
     12 4. Keeping the usage simple.
     13
     14The proposed solution solves the problem in a hackish, but convenient way. To dispel some worries: it doesn't involve any changes to Django's source. After playing with the ideas of method decorators and model inheritance, I've rejected both: decorators are too rigid and spammy, since they require you to explicitely override {{{save()}}} and {{{delete()}}} even when it's not needed; model inheritance would interfere with manually overriding the methods. So the final choice was made in favour of using a metaclass to insert the callbacks.
     15
     16All Django models already have a metaclass - {{{django.db.models.ModelBase}}}. A new metaclass was defined, inheritting from {{{ModelBase}}} - {{{MetaModelMiddleware}}}. This new metaclass lets your model inherit from custom classes, that can define any or all of {{{pre_save()}}}, {{{post_save()}}}, {{{pre_delete()}}}, {{{post_delete()}}} methods. At the construction of your model class these methods are automatically added to relevant points, so that the actual {{{save()}}} and {{{delete()}}} methods remain untouched. Through multiple inheritance, your model can acquire as many of these callbacks as needed - they all will be called in order. The callback methods receive a single argument - the instance of your model for which {{{save()}}} or {{{delete()}}} is being executed.
     17
     18In practice it works like this:
     19
     20{{{
     21# import the model middleware module from wherever it is on your path
     22from myproject.utils import model_utils
     23
     24# define a descendant of model_utils.ModelMiddleware with pre/post_* methods
     25
     26class TestMiddleware(model_utils.ModelMiddleware):
     27    def pre_save(self):
     28        print self, "is about to be saved."
     29
     30    def post_save(self):
     31        print self, " has been saved."
     32
     33    def pre_delete(self):
     34        print self, "is about to be deleted."
     35
     36    def post_delete(self):
     37        print self, "has been deleted."
     38
     39# your model then inherits from the above class
     40# note that inheritting from models.Model is unneeded
     41
     42class MyModel(TestMiddleware):
     43  ...
     44}}}
     45
     46Here's a more practical example, that I used for ReST parsing
     47
     48{{{
     49class ReSTMiddleware(ModelMiddleware):
     50    def pre_save(self):
     51        try:
     52            cont = self.content.decode('utf_8')
     53            parts = build_document(cont, initial_header_level=2)
     54            self.html_body = parts['html_body'].encode('utf_8')
     55            self.html_toc = parts['toc'].encode('utf_8')
     56        except:
     57            pass
     58        d = datetime.now()
     59        pdate = datetime(d.year, d.month, d.day, d.hour, d.minute)
     60        if self.pub_date is None:
     61            self.pub_date = pdate
     62        self.last_modified = pdate
     63}}}
     64
     65This is still fairly contrived, since you can see it somehow knowing which field must be ReST'ified, as well as setting publication and modification times (and how does it know how to do that?). To solve that problem the metaclass can also save your model's "middleware settings", much like those for Admin are saved. To apply the settings to your model you'll then do the same thing as for Admin, but with a different name:
     66
     67{{{
     68class MyModel(ModelMiddleware):
     69    ...
     70    # fields, custom methods, Admin, etc.
     71
     72    class Middle:
     73        ReST = ({"field" : "content", "save_body" : "html_body", "save_toc" : "html_toc", "init_header" : 2},)
     74}}}
     75
     76My ReST parser takes a string and returns two parts: the ReST version of the same string and the table of contents, generated from that string. Therefore, it needs to know which field to get the raw string from, and where to save the body and toc parts.
     77
     78The middleware options are accessed through the class object with {{{MyModelKlass._middle}}}. Since the actual middleware class only has access to the instance object, it would need to do {{{self.__class__._middle}}} to get the options. Here's how it looks with that change:
     79
     80{{{
     81class ReSTMiddleware(ModelMiddleware):
     82    def pre_save(self):
     83        try:
     84            opts = self.__class__._middle["ReST"] # individual options are saved in a dict
     85        except AttributeError:
     86            return  # just fail silently, though it might not be a very good idea in practice
     87
     88        # lets be nice to ourselves and provide a default value for the initial header level
     89        for opt in opts:
     90            if not opt.has_key("init_header"):
     91                opt["init_header"] = 1
     92       
     93        # parse for as many fields as we have options for
     94        for opt in opts: 
     95            try:
     96                cont = getattr(self, opt["field"]).decode("utf_8")
     97                parts = build_document(cont, initial_header_level=opt["init_header"])
     98                setattr(self, opt["save_body"], parts["html_body"].encode('utf_8'))
     99                setattr(self, opt["save_toc"], parts["toc"].encode('utf_8'))
     100            except:
     101                pass # another silent fail, needs fixing
     102
     103        d = datetime.now()
     104        pdate = datetime(d.year, d.month, d.day, d.hour, d.minute)
     105        if self.pub_date is None:
     106            self.pub_date = pdate
     107        self.last_modified = pdate
     108}}}
     109
     110Now ReST parsing can operate on any model, given that this model is correctly set up for such an operation, e.g. has the needed fields for html parts of every ReST-enabled field and the relevant {{{Middle}}} options.
     111
     112There's still that "date problem" left though - this mucking has no business being in a {{{ReSTMiddleware}}} class. But there's a positive side to every case of coding negligence. In this particular case, I have an opportunity to show off an important feature of this approach: separation of generic and custom aspects of record changes.
     113
     114We don't really want to define a model middleware class to handle this date juggling, since this is just a result of lazyness and there's probably a built in way to do the same thing in Django, which I just didn't bother to look up. But nor do I want to go looking for it right now, as I have a highly impatient dog to walk (or a rug to scrub, depending on how soon I finish writting this), so we'll move it inside the model's {{{save()}}} method:
     115
     116{{{
     117class MyModel(ReSTMiddleware):
     118    ...
     119
     120    def save(self):
     121        d = datetime.now()
     122        pdate = datetime(d.year, d.month, d.day, d.hour, d.minute)
     123        if self.pub_date is None:
     124            self.pub_date = pdate
     125        self.last_modified = pdate
     126        super(Model, self).save()
     127
     128}}}
     129
     130And the final version of {{{ReSTMiddleware}}} then becomes:
     131
     132{{{
     133class ReSTMiddleware(ModelMiddleware):
     134    def pre_save(self):
     135        try:
     136            opts = self.__class__._middle["ReST"] # individual options are saved in a dict
     137        except AttributeError:
     138            return  # just fail silently, though it might not be a very good idea in practice
     139
     140        # lets be nice to ourselves and provide a default value for the initial header level
     141        for opt in opts:
     142            if not opt.has_key("init_header"):
     143                opt["init_header"] = 1
     144       
     145        # parse for as many fields as we have options for
     146        for opt in opts: 
     147            try:
     148                cont = getattr(self, opt["field"]).decode("utf_8")
     149                parts = build_document(cont, initial_header_level=opt["init_header"])
     150                setattr(self, opt["save_body"], parts["html_body"].encode('utf_8'))
     151                setattr(self, opt["save_toc"], parts["toc"].encode('utf_8'))
     152            except:
     153                pass # another silent fail, needs fixing
     154}}}
     155
     156Now when an instance of {{{MyModel}}} class is saved, the {{{ReSTMiddleware.pre_save()}}} method will be called before the actual {{{MyModel.save()}}}, preparing all the ReST'ified fields. And this ReST parser can be applied to as many models as needed, automatically inserting the same {{{pre_save()}}} method into all of them. If you wanted another {{{ModelMiddleware}}} class to be included in your model, you'd just add it to the list of parents from which the model inherits.
     157
     158Your model can mix {{{ModelMiddleware}}} classes and "straight" Django models in the inheritance list - since the {{{MetaModelMiddleware}}} inherits from the {{{ModelBase}}} metaclass, no inheritance clashes will occur, and since middleware classes are actually removed from the parents list of the inheritting model class (leaving everything else in (hopefully)), you won't get any errors about missing tables in your db.
     159
     160The provided module includes two model middleware classes: the already described {{{ReSTMiddleware}}} and {{{TimestampMiddleware}}}, which puts that "date mucking" from the first version of code above into a model middleware class. The {{{ReSTMiddleware}}} class won't work without an extra module that includes the function used for parsing ReST strings.
     161
     162'''Problems''':
     163
     164 * Not really tested yet, in a formal sense of the word.
     165 * No guarantee is given that "mixed" inheritance ({{{ModelMiddleware}}} + {{{models.Model}}} descendants) really works.
Back to Top