Package expedient :: Package common :: Package extendable :: Module models
[hide private]
[frames] | no frames]

Source Code for Module expedient.common.extendable.models

  1  ''' 
  2  @author: jnaous 
  3  ''' 
  4  import logging 
  5  from django.db import models 
  6  from django.db.models.base import ModelBase 
  7  from django.utils.importlib import import_module 
  8  from expedient.common.tests.utils import drop_to_shell 
  9   
 10  logger = logging.getLogger("extendable.models") 
 11   
12 -class ExtendableMeta(ModelBase):
13 ''' 14 Metaclass for all Extendable models. 15 ''' 16
17 - def __new__(cls, name, bases, attrs):
18 '''Add the fields in 'extended_fields to all 19 grandchildren of Extendable. Also connect the post_save signal 20 to save the content_object.''' 21 22 # get the inner Extend class 23 class Extend: pass 24 extend = attrs.setdefault('Extend', Extend) 25 26 # Only do this for subclasses of Extendable 27 if (name != "Extendable" or 28 attrs['__module__'] != ExtendableMeta.__module__): 29 30 # get the extended fields of the parents 31 extended_fields = {} 32 repl_kw = set() 33 for base in bases: 34 if isinstance(base, ExtendableMeta): 35 base_extend = getattr(base, "Extend", Extend) 36 # check for clashes in field names and replacement keywords 37 # and add to dict 38 for fname, fval in \ 39 getattr(base_extend, "fields", {}).items(): 40 field_cls, args, kwargs, args_repl, kwargs_repl = fval 41 if fname in extended_fields: 42 raise Exception( 43 "Extended field '%s' from %s " % (fname, base) 44 +"already defined in another parent of " 45 +"%s.%s" % (attrs['__module__'], name)) 46 else: 47 clashes = set(args_repl).intersection(repl_kw) 48 clashes = clashes.union(set(kwargs_repl.values())\ 49 .intersection(repl_kw)) 50 if clashes: 51 raise Exception( 52 "Replacement keywords '%s' " % clashes 53 +"from %s already " % attrs['__module__'] 54 +"defined in another parent of %s.%s" % ( 55 name, base)) 56 else: 57 extended_fields[fname] = fval 58 59 # get the delegations, check that they are all in extended_fields 60 delegations = getattr(extend, "redelegate", []) 61 for d in delegations: 62 if d not in extended_fields: 63 raise Exception( 64 "Redelegated field '%s' is not an extended" % d 65 +" field in any of %s.%s's parents." %( 66 attrs['__module__'], name)) 67 else: 68 # remove the field from the list to add to this class 69 # and put it in this class's inner Extend 70 extend.fields = getattr(extend, "fields", {}) 71 extend.fields[d] = extended_fields[d] 72 del extended_fields[d] 73 74 if extended_fields: 75 # get replacements 76 repl = getattr(extend, 'replacements', {}) 77 78 # create each field in the child, replacing when necessary 79 for fname, fval in extended_fields.items(): 80 # expand the information about the field 81 field_cls, args, kwargs, args_repl, kwargs_repl = fval 82 83 # check if there are any replacement for arguments 84 if args_repl: 85 if len(args_repl) != len(args): 86 raise Exception("Arguments list must be of the " 87 + "same length as its replacement " 88 + "keywords.") 89 z = zip(args, args_repl) 90 args = [repl[t[1]] \ 91 if t[1] != None and t[1] in repl \ 92 else t[0] for t in z] 93 94 # check for keyword argument replacements 95 kwargs = kwargs.copy() 96 if kwargs_repl: 97 for repl_arg, repl_key in kwargs_repl.items(): 98 if repl_key in repl: 99 kwargs[repl_arg] = repl[repl_key] 100 101 # check for clashes 102 if fname in attrs: 103 raise Exception( 104 "Field '%s' clashes with extended field " % fname 105 +"in class %s.%s" % (attrs['__module__'], name)) 106 107 # now create the field in this class 108 attrs[fname] = field_cls(*args, **kwargs) 109 110 new_cls = super(ExtendableMeta, cls).__new__(cls, name, bases, attrs) 111 112 return new_cls
113 114
115 -class ExtendableManager(models.Manager):
116 """ 117 A manager for Extendable objects. 118 """ 119
120 - def filter_for_class(self, klass):
121 """ 122 Return a filtered QuerySet that only has instances whose 123 leaf class is C{klass}. 124 125 @param klass: The leaf model class of instances we are looking for. 126 @type klass: a class 127 """ 128 return self.filter(leaf_name=klass.__name__.lower())
129
130 - def filter_for_classes(self, klasses):
131 """ 132 Return a filtered QuerySet that only has instances whose 133 leaf class are in the list C{klasses}. 134 135 @param klasses: List of leaf model classes of instances we are 136 looking for. 137 @type klasses: list of classes 138 """ 139 cls_names = [c.__name__.lower() for c in klasses] 140 return self.filter(leaf_name__in=cls_names)
141
142 -class Extendable(models.Model):
143 ''' 144 Extendable object. 145 146 An extendable object that enables an instance to be obtained as its 147 class's farthest descendant. 148 149 Additionally, fields can be defined that would be added only 150 to a direct subclass. This can be useful to automatically add relationships 151 to subclasses so that relationships exist in the subclasses but not in the 152 parents, allowing the same field name to be used for different types of 153 relationships. 154 155 Further, the class that inherits from C{Extendable} may specify which 156 field initialization arguments 157 subclasses may override. Subclasses can override the arguments by adding 158 values to a L{dict} in an inner class. 159 160 Adding fields is done by adding an C{Extend} inner class. Three fields can 161 be used in the C{Extend} inner class: 162 1. C{fields}: This is a L{dict} that describes what the fields to be added 163 in the subclass are. The format is as follows :: 164 165 {field_name : (field_class, args, kwargs, repl_args, repl_kwargs)} 166 167 - C{field_name}: name of the field to add to subclass 168 - C{field_class}: class of the field e.g. ManyToManyField 169 - C{args}: a list of pos args with which to init field_class 170 - C{kwargs}: a list of keyword args with which to init field_class 171 - C{repl_args}: either None or a list with the same length as 172 C{args} that specifies which arguments can be overridden, and 173 what keyword to use to replace that argument. 174 - C{repl_kwargs}: either None or a dict that specifies which kwargs 175 can be overridden by subclasses and what keyword to use for the 176 override. 177 178 2. C{mandatory}: C{iterable} of keywords that subclasses must replace. 179 180 3. C{replacements}: L{dict} that specifies the replacements to use in the 181 subclass using the keywords defined in the C{repl_args} and 182 C{repl_kwargs} in a parent's C{Extend.fields} values. 183 184 4. C{redelegate}: C{iterable} of field names from C{fields} in a 185 parent's C{Extend} inner class that specifies which fields should not 186 be added in this class but its subclass. 187 188 Using the C{mandatory} field in a child class of C{Exendable} forces 189 grandchildren of to specify replacements for particular keywords. 190 191 Grandchildren may further delegate adding fields to their own children i.e. 192 the great grandchildren by specifying the field names in their C{redelegate} 193 field of their inner C{Extend} class. 194 195 For example: :: 196 197 from django.db import models 198 199 class OtherModel(models.Model): pass 200 201 class OtherModelRel(models.Model): 202 parent = models.ForeignKey("TestParent") 203 other = models.ForeignKey(OtherModel) 204 205 class YetAnotherModel(models.Model): pass 206 207 class YetAnotherModelRel(models.Model): 208 child = models.ForeignKey("TestChild") 209 other = models.ForeignKey(YetAnotherModel) 210 211 class TestParent(Extendable): 212 class Extend: 213 fields = { 214 'other': (models.ManyToManyField, 215 [OtherModel,], 216 {'through': OtherModelRel}, 217 ['other_model',], 218 {'through': 'other_through'}, 219 ), 220 } 221 mandatory = [ 222 'other_through', 223 ] 224 225 class TestChild(TestParent): 226 class Extend: 227 replacements = { 228 'other_model': YetAnotherModel, 229 'other_through': YetAnotherModelRel, 230 } 231 ''' 232 233 objects = ExtendableManager() 234 235 leaf_name = models.CharField(max_length=100, blank=True, editable=False) 236 module_name = models.CharField(max_length=100, blank=True, editable=False) 237 238 __metaclass__ = ExtendableMeta 239
240 - class Meta:
241 abstract = True
242
243 - def __init__(self, *args, **kwargs):
244 kwargs.setdefault("leaf_name", self.__class__.__name__) 245 kwargs.setdefault("module_name", self.__class__.__module__) 246 super(Extendable, self).__init__(*args, **kwargs)
247
248 - def as_leaf_class(self):
249 '''Return this instance as the farthest descendant of its class''' 250 if self.is_instance_of(self.__class__): 251 return self 252 else: 253 try: 254 mod = import_module(self.module_name) 255 except ImportError: 256 logger.debug("init import error.") 257 drop_to_shell(local=locals()) 258 raise 259 klass = getattr(mod, self.leaf_name) 260 return klass.objects.get(pk=self.pk)
261
262 - def is_instance_of(self, klass):
263 """Is the object an instance of the passed class C{klass}?""" 264 265 return self.leaf_name == klass.__name__ and self.module_name == klass.__module__
266