Auto-updating Properties in Google AppEngine Models
EDIT: I have an updated version of this code available in a newer post, please go to Magic Properties on Google AppEngine for the updated information. For the generic information that has not changed, and the idea behind the code please continue below.
In Google AppEngine a model may contain many different properties, from this model you can generate a form using djangoforms. This is all good and well until you have a required property on a model that needs to have part of it be calculated based upon something the user types in. For example, a blog entry generally has a "slug" which is generated from the title, now in Google AppEngine's models, if a property is required it has to be set at object creation time.
Djangoforms has a form.save(committed=False) that allows one to get back an unsaved Model, however since slug is not in the form (since it is auto generated) it cannot be set at model creation time. Even adding a hidden field to the form with the name of the property is not going to satisfy the model since a value of None is considered to be empty, which means it does not meet the required=True status.
After trying a multitude of different options I ran across an article about extending models that specifically talked about adding custom properties that were saved into the DB, and could be searched on, this got me digging and I found a blog post on the Google AppEngine blogspot about the exact same thing, using yet again a different example. The one that got me on the right track was the following post by Rodrigo Moraes on his custom model properties.
I came up with the following fairly quickly after also taking a look at googleappengine.ext.db within the Google AppEngine framework, however there is still an issue, for now the MagicProperty is only updated the first time the property it affects is set. I am not yet sure how to work around that, I will have to do some more digging, maybe there is a way to get notified if another property gets modified or updated.
import logging from google.appengine.ext import db def MagicProperty(prop, magic_func=None, *args, **kw): if magic_func: # No pants required. return _MagicProperty(prop, magic_func, *args, **kw) else: # We are putting some pants on the function. def pants(magic_func): return _MagicProperty(prop, magic_func, *args, **kw) return pants class _MagicProperty(db.Property): """MagicProperty which will modify output based on a function that it is given. Inspired by: http://appengine-cookbook.appspot.com/recipe/custom-model-properties-are-cute http://code.google.com/appengine/articles/extending_models.html http://googleappengine.blogspot.com/2009/07/writing-custom-property-classes.html """ def __init__(self, prop, magic_func, *args, **kw): super(_MagicProperty, self).__init__(*args, **kw) self.magic_func = magic_func self.magic_prop = prop def attr_name(self): # In google.appengine.ex.db there is an explicit warning not to use this method, so we test for it first. if self._attr_name: return self._attr_name() else: return "_" + self.name def __get__(self, model_instance, class_instance): if model_instance is None: return self # TODO: If the property that this one is functioning on is changed, we need a way to know that so that we recalculate # Original __get__ in google.appengine.ext.db has getattr to retrieve the value from the model instance magic_done = getattr(model_instance, self.attr_name(), None) if magic_done is None: magic_done = self.magic_func(self.magic_prop.__get__(model_instance, class_instance)) # Set the attribute in the model setattr(model_instance, self._attr_name(), magic_done) return magic_done def __set__(self, *args): raise db.DerivedPropertyError("MagicProperty is magic. Magic may not be modified.")</pre>
The approach that the DateTimeProperty takes is one that I can't use because my class is dependant upon an external property, not whether or not a value was set when the object was instantiated.
Google AppEngine is posing some rather interesting challenges. Although, for slugs it may not be entirely bad that it can only be set once, as that means title changes on the article won't cause the slug to change possibly breaking old links. This definitely requires more research and more thinking.