Skip to content

Instantly share code, notes, and snippets.

Created July 14, 2010 20:57
Show Gist options
  • Save anonymous/476058 to your computer and use it in GitHub Desktop.
Save anonymous/476058 to your computer and use it in GitHub Desktop.
from django.core.exceptions import ValidationError
from django.db.models import Field, CharField
__all__ = ['MultiColumnField']
try:
from hashlib import md5
except ImportError:
from md5 import new as md5
def _make_property(self, field_name):
def _get_amount(self, default=None):
return getattr(self.instance, field_name, default)
def _set_amount(self, value):
return setattr(self.instance, field_name, value)
return property(_get_amount, _set_amount)
def _generate_instance_class(owner_field):
class MultiColumnFieldInstance(object):
"""
Represents a single instance of a MultiColumnField on an object, by
keeping track of both a reference to the parent instance and a
reference to the MultiColumnField-derived class. This allows for
"natural" access to the subfield values by using properties.
"""
def __init__(self, instance, field):
self.content_type = None
self.instance = instance
self.field = field
self.names = owner_field.names
for name in self.names:
prop = _make_property(self, self.field.field_names[name])
setattr(MultiColumnFieldInstance, name, prop)
def to_dict(self):
d = {}
for name in self.names:
d[name] = getattr(self.instance, self.field.field_names[name], None)
return d
def __repr__(self):
return self.field._instance_repr(self)
return MultiColumnFieldInstance
class MultiColumnField(Field):
"A field containing multiple sub-fields and spanning multiple columns"
def __init__(self, *args, **kwargs):
if not self.fields:
self.fields = kwargs.pop('fields', None)
if not self.fields:
raise ValidationError("no fields attribute or argument provided")
super(MultiColumnField, self).__init__(*args, **kwargs)
def contribute_to_class(self, cls, name):
self.name = name
self.key = md5(self.name).hexdigest()
self.names = self.fields.keys() # Names we want to call the fields
self.field_names = {} # What they are internally named
# Add all of the 'real' fields to the class, cache the calc'd field names
for suffix,field in self.fields.items():
field_name = "%s_%s" % (self.name, suffix)
self.field_names[suffix] = field_name
cls.add_to_class(field_name, field)
# Generate the 'instance class' for this MCF-derived class
self.instance_class = _generate_instance_class(self)
# Add this field as a class member
setattr(cls, name, self)
def get_db_prep_save(self, value):
pass
def get_db_prep_lookup(self, lookup_type, value):
raise NotImplementedError(self.get_db_prep_lookup)
def __get__(self, instance, type=None):
"""
Accessor wrapper for a MultiColumnField, to allow for on-the-fly
MultiColumnFieldInstance generation when used outside of the class.
"""
if instance is None:
return self
# TODO: Add caching here
return self.instance_class(instance, self)
def __set__(self, instance, value):
"sets all values in a MultiColumnField at once"
if isinstance(value, self.instance_class):
# TODO: Use added cache here
temp_field_instance = self.instance_class(instance, self)
for name in self.names:
setattr(temp_field_instance, name, getattr(value, name, None))
elif isinstance(value, dict):
temp_field_instance = self.instance_class(instance, self)
for name in self.names:
setattr(temp_field_instance, name, value[name])
else:
raise TypeError
def _instance_repr(self, instance):
"""
A stock implementation of the __repr__ function for a generated instance of
a MultiColumnField-derived class. The generated instance class uses the
parent class' __instance_repr__ function to allow for easy overriding.
"""
return "<'%s' field (MultiColumnField) on instance '%s'>" % (self.name, instance.instance.__repr__())
# Example MultiColumnField-derived class
#class TestField(MultiColumnField):
# fields = {
# 'awesome': CharField(max_length=200, null=True),
# 'town': CharField(max_length=200, null=True),
# }
@supervacuo
Copy link

If you use more than one of these TestFields in your models.py, Django gets confused: all column names and verbose_name get taken from the first TestField appearance.

e.g. with the following models, B.f should not exist (but does).

class A(models.Model):
  f = TestField()

class B(models.Model):
  g = TestField()

@olivergeorge made a different suggestion for how to define fields which addresses this problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment