diff --git a/fields.py b/fields.py index 058879a..911b63e 100644 --- a/fields.py +++ b/fields.py @@ -1,7 +1,18 @@ import uuid +import re from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext as _ +from django.core.exceptions import ValidationError + +class EavSlugField(models.SlugField): + def validate(self, value, instance): + super(EavSlugField, self).validate(value, instance) + slug_regex = r'[a-z]+[a-z0-9_]*' + if not re.match(slug_regex, value): + raise ValidationError(_(u"Must be all lower case, "\ + u"not start with a number, and contain "\ + u"only letters, numbers, or underscores.")) class UuidField(models.CharField): @@ -13,7 +24,8 @@ class UuidField(models.CharField): ''' __metaclass__ = models.SubfieldBase - def __init__(self, version=4, node=None, clock_seq=None, namespace=None, auto=False, name=None, *args, **kwargs): + def __init__(self, version=4, node=None, clock_seq=None, + namespace=None, auto=False, name=None, *args, **kwargs): self.auto = auto self.version = version # Set this as a fixed value, we store UUIDs in text. @@ -36,7 +48,7 @@ class UuidField(models.CharField): args = (self.namespace, self.name) else: args = () - return getattr(uuid, 'uuid%s' % (self.version,))(*args) + return getattr(uuid, 'uuid%s' % self.version)(*args) def db_type(self): return 'char' diff --git a/models.py b/models.py index bacb07f..b1a7dcc 100644 --- a/models.py +++ b/models.py @@ -1,9 +1,11 @@ +import re + from django.db import models from django.utils.translation import ugettext_lazy as _ from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic -from .fields import UuidField +from .fields import UuidField, EavSlugField class EavAttributeLabel(models.Model): @@ -16,11 +18,11 @@ class EavAttribute(models.Model): The A model in E-A-V. This holds the 'concepts' along with the data type something like: - >>> EavAttribute.objects.create(name='height', datatype='float') - + >>> EavAttribute.objects.create(name='Height', datatype='float', slug='height') + - >>> EavAttribute.objects.create(name='color', datatype='text') - + >>> EavAttribute.objects.create(name='Color', datatype='text', slug='color') + ''' class Meta: ordering = ['name'] @@ -41,22 +43,34 @@ class EavAttribute(models.Model): #(TYPE_MANY, _('multiple choices')), ) - #TODO Force name to lowercase? Don't allow spaces in name + slug = EavSlugField(_(u"slug"), max_length=50, db_index=True, + help_text=_(u"Short unique attribute label"), + unique=True) + name = models.CharField(_(u"name"), max_length=100, help_text=_(u"User-friendly attribute name")) - help_text = models.CharField(_(u"help text"), max_length=250, + help_text = models.CharField(_(u"help text"), max_length=256, blank=True, null=True, help_text=_(u"Short description")) datatype = models.CharField(_(u"data type"), max_length=6, choices=DATATYPE_CHOICES) - uuid = UuidField(_(u"UUID"), auto=True) + uuid = UuidField(verbose_name=_(u"UUID"), auto=True, db_index=True) labels = models.ManyToManyField(EavAttributeLabel, verbose_name=_(u"labels")) + + def save(self, *args, **kwargs): + self.full_clean() + super(EavAttribute, self).save(*args, **kwargs) + + def add_label(self, label): + pass + + def get_value_for_entity(self, entity): ''' Passed any object that may be used as an 'entity' object (is linked @@ -135,8 +149,8 @@ class EavEntity(object): def __getattr__(self, name): if not name.startswith('_'): - if name in self.get_all_attribute_names(): - attribute = self.get_attribute_by_name(name) + if slug in self.get_all_attribute_slugs(): + attribute = self.get_attribute_by_slug(name) value = attribute.get_value_for_entity(self.model) return value.value if value else None raise AttributeError(_(u"%s EAV does not have attribute " \ @@ -145,8 +159,8 @@ class EavEntity(object): def save(self): for attribute in self.get_all_attributes(): - if hasattr(self, attribute.name): - attribute_value = getattr(self, attribute.name) + if hasattr(self, attribute.slug): + attribute_value = getattr(self, attribute.slug) attribute.save_value(self.model, attribute_value) def get_all_attributes(self): @@ -157,22 +171,22 @@ class EavEntity(object): pass self._attributes_cache = self.get_eav_attributes().select_related() - self._attributes_cache_dict = dict((s.name, s) for s in self._attributes_cache) + self._attributes_cache_dict = dict((s.slug, s) for s in self._attributes_cache) return self._attributes_cache def get_values(self): return EavValue.objects.filter(content_type=self.ct, object_id=self.model.pk).select_related() - def get_all_attribute_names(self): + def get_all_attribute_slugs(self): if not hasattr(self, '_attributes_cache_dict'): self.get_all_attributes() return self._attributes_cache_dict.keys() - def get_attribute_by_name(self, name): + def get_attribute_by_slug(self, slug): if not hasattr(self, '_attributes_cache_dict'): self.get_all_attributes() - return self._attributes_cache_dict[name] + return self._attributes_cache_dict[slug] def get_attribute_by_id(self, attribute_id): for attr in self.get_all_attributes(): diff --git a/tests/basics.py b/tests/basics.py index 459749f..e288d37 100644 --- a/tests/basics.py +++ b/tests/basics.py @@ -20,8 +20,8 @@ class EavBasicTests(TestCase): EavRegistry.register(Patient) self.attribute = EavAttribute.objects.create(datatype=EavAttribute.TYPE_TEXT, - name='City', - help_text='The City') + name='City', help_text='The City', slug='city') + self.entity = Patient.objects.create(name="Doe") self.value = EavValue.objects.create(object=self.entity, @@ -35,7 +35,7 @@ class EavBasicTests(TestCase): def test_can_create_attribute(self): EavAttribute.objects.create(datatype=EavAttribute.TYPE_TEXT, - name='My text test', + name='My text test', slug='test', help_text='My help text') def test_attribute_unicode(self): @@ -62,7 +62,7 @@ class EavBasicTests(TestCase): def test_value_types(self): _text = EavAttribute.objects.create(datatype=EavAttribute.TYPE_TEXT, - name='Text', + name='Text', slug='text', help_text='The text') val = EavValue.objects.create(object=self.entity, attribute = _text) @@ -72,7 +72,7 @@ class EavBasicTests(TestCase): self.assertEqual(val.value, value) _float = EavAttribute.objects.create(datatype=EavAttribute.TYPE_FLOAT, - name='Float', + name='Float', slug='float', help_text='The float') val = EavValue.objects.create(object=self.entity, attribute = _float) @@ -83,7 +83,7 @@ class EavBasicTests(TestCase): _int = EavAttribute.objects.create(datatype=EavAttribute.TYPE_INT, - name='Int', + name='Int', slug='int', help_text='The int') val = EavValue.objects.create(object=self.entity, attribute = _int) @@ -93,7 +93,7 @@ class EavBasicTests(TestCase): self.assertEqual(val.value, value) _date = EavAttribute.objects.create(datatype=EavAttribute.TYPE_DATE, - name='Date', + name='Date', slug='date', help_text='The date') val = EavValue.objects.create(object=self.entity, attribute = _date) @@ -103,7 +103,7 @@ class EavBasicTests(TestCase): self.assertEqual(val.value, value) _bool = EavAttribute.objects.create(datatype=EavAttribute.TYPE_BOOLEAN, - name='Bool', + name='Bool', slug='bool', help_text='The bool') val = EavValue.objects.create(object=self.entity, attribute = _bool)