Login

DRY with common model fields (another way)

Author:
jmrbcu
Posted:
July 19, 2007
Language:
Python
Version:
.96
Score:
4 (after 6 ratings)

Some time you want to add some common fields to a group of models, for example, in a Generalization/Specialization relationship. One could have a base model as the generalization class and specialized models with a foreign key to that base model with an unique attribute but I don't like it that way so, I just do this code to add some commons attributes to a lot of models. If you have many models that all share the same fields, this might be an option. The fields are added directly to each model, e.g. while they will be duplicated on the database level, you only have to define them once in your python code. This code is a cleaner way(I think!!!) to do it and will do the same that this one. I hope this piece of code will be useful for you.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import types
from copy import deepcopy
from django.db import models

class ExpanderField(object):
    """
    This type of field can be used to include some common fields and methods in models.
    It take a class with those common things as parameter in the constructor and will 
    add all fields in that class to models classes in which a field of
    this class type is declared. If the class containing common fields 
    have fields of type: ForeignKey and ManyToManyField, then the related_name attribute
    of those fields will be renamed to a value of the form: model_name_original_value .
    Ej: if related_name = child_set and model name is MenuItem, it will be renamed to 
    MenuItem_child_set. To add methods of the common class you must inherit from it.
    An importan thing to note here is that you must inherit first from the common class, 
    order matters.

    Use case:
    class ContentManager(models.Manager):
        pass
        
    class CommonFields:
        pub_date = models.DateTimeField()
        created_by = models.ForeignKey(User, related_name = 'created_by_set')
        last_modified_by = models.ForeignKey(User, related_name = 'last_modified_by_set')

        objects = ContentManager()

        def save(self):
            #do something
            pass
            
    class NewsItem(CommonFields, models.Model):
        title = models.CharField(maxlength = 100)
        body = models.CharField(maxlength = 200)
        common = ExpanderField(CommonFields)

    this will create a class of this form:

    class NewsItem(models.Model):
        title = models.CharField(maxlength = 100)
        body = models.CharField(maxlength = 200)
        pub_date = models.DateTimeField()
        created_by = models.ForeignKey(User, related_name = 'created_by_set')
        last_modified_by = models.ForeignKey(User, related_name = 'last_modified_by_set')

        objects = ContentManager()

    """
    def __init__(self, field_container_class):
        self.field_container_class = field_container_class

    def contribute_to_class(self, cls, name):
        attr_list = [attr for attr in dir(self.field_container_class) 
                     if attr not in  ('__doc__', '__module__', '__class__', '__dict__')]
        container = self.field_container_class
        
        for attr in attr_list:
            clone = None
            attr_value = getattr(container, attr)
            
            if type(attr_value) != types.MethodType:
                clone = deepcopy(attr_value)
                
                if (isinstance(clone, models.ForeignKey) or 
                    isinstance(clone, models.ManyToManyField)) and \
                    clone.rel.related_name is not None:
                    clone.rel.related_name = cls.__name__ + '_' + clone.rel.related_name
                
            if clone is not None:
                cls.add_to_class(attr, clone)

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 11 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
  5. Help text hyperlinks by sa2812 1 year, 7 months ago

Comments

david_bgk (on July 20, 2007):

Just a few improvements: - you can directly define objects = models.Manager() even if a custom manager could be useful ; - you can use a tuple instead of a list for performances issues.

Anyway, thanks for sharing this. Useful.

#

jmrbcu (on July 20, 2007):

It's only to save you from typing, I hate to repeat the same code over and over again!!!

#

miracle2k (on January 10, 2008):

Great snippet, much better than my attempt.

I suggest using:

attr_list = [attr for attr in dir(self.field_container_class) if not attr.startswith('__')]

Which makes it work better with new-style classes.

#

ogw (on May 21, 2008):

I have a problem when validating. I get this:

File ".../utils.py", line 93, in contribute_to_class cls.add_to_class(attr, clone)

File "/var/lib/python-support/python2.5/django/db/models/base.py", line 89, in add_to_class value.contribute_to_class(cls, name)

File "/var/lib/python-support/python2.5/django/db/models/options.py", line 49, in contribute_to_class if self.meta:

AttributeError: 'Options' object has no attribute 'meta'

#

awolinoz (on October 16, 2011):

I found the same problem as ogw and the solution for me was that my "expander field" models needed to inherit from object rather than models.Models that is;

mycommonfields(object)

rather than

mycommonfields(models.Models)

#

Please login first before commenting.