Work on code style in models (#3)

This commit is contained in:
Iwo Herka 2018-06-20 14:23:30 +02:00
parent d8251d2646
commit 6f3329d2ae
2 changed files with 261 additions and 201 deletions

View file

@ -27,52 +27,46 @@ class EnumValue(models.Model):
*EnumValue* objects are the value 'choices' to multiple choice
*TYPE_ENUM* :class:`Attribute` objects.
They have only one field, *value*, a``CharField`` that must be unique.
They have only one field, *value*, a ``CharField`` that must be unique.
For example::
For example:
yes = EnumValue.objects.create(value='Yes') # doctest: SKIP
no = EnumValue.objects.create(value='No')
unknown = EnumValue.objects.create(value='Unknown')
>>> yes = EnumValue.objects.create(value='Yes') # doctest: SKIP
>>> no = EnumValue.objects.create(value='No')
>>> unkown = EnumValue.objects.create(value='Unkown')
ynu = EnumGroup.objects.create(name='Yes / No / Unknown')
ynu.enums.add(yes, no, unknown)
>>> ynu = EnumGroup.objects.create(name='Yes / No / Unkown')
>>> ynu.enums.add(yes, no, unkown)
>>> Attribute.objects.create(name='Has Fever?',
... datatype=Attribute.TYPE_ENUM,
... enum_group=ynu)
<Attribute: Has the fever? (Multiple Choice)>
Attribute.objects.create(name='has fever?', datatype=Attribute.TYPE_ENUM, enum_group=ynu)
# = <Attribute: has fever? (Multiple Choice)>
.. note::
The same *EnumValue* objects should be reused within multiple
*EnumGroups*. For example, if you have one *EnumGroup*
called: *Yes / No / Unkown* and another called *Yes / No /
called: *Yes / No / Unknown* and another called *Yes / No /
Not applicable*, you should only have a total of four *EnumValues*
objects, as you should have used the same *Yes* and *No* *EnumValues*
for both *EnumGroups*.
'''
value = models.CharField(_(u"value"), db_index=True,
unique=True, max_length=50)
value = models.CharField(_('value'), db_index=True, unique=True, max_length=50)
def __str__(self):
return self.value
return '<EnumValue {}>'.format(self.value)
class EnumGroup(models.Model):
'''
*EnumGroup* objects have two fields- a *name* ``CharField`` and *enums*,
*EnumGroup* objects have two fields - a *name* ``CharField`` and *enums*,
a ``ManyToManyField`` to :class:`EnumValue`. :class:`Attribute` classes
with datatype *TYPE_ENUM* have a ``ForeignKey`` field to *EnumGroup*.
See :class:`EnumValue` for an example.
'''
name = models.CharField(_(u"name"), unique=True, max_length=100)
enums = models.ManyToManyField(EnumValue, verbose_name=_(u"enum group"))
name = models.CharField(_('name'), unique = True, max_length = 100)
enums = models.ManyToManyField(EnumValue, verbose_name = _('enum group'))
def __str__(self):
return self.name
return '<EnumGroup {}>'.format(self.name)
class Attribute(models.Model):
@ -103,81 +97,120 @@ class Attribute(models.Model):
* object (TYPE_OBJECT)
* enum (TYPE_ENUM)
Examples:
Examples::
>>> Attribute.objects.create(name='Height', datatype=Attribute.TYPE_INT)
<Attribute: Height (Integer)>
Attribute.objects.create(name='Height', datatype=Attribute.TYPE_INT)
# = <Attribute: Height (Integer)>
>>> Attribute.objects.create(name='Color', datatype=Attribute.TYPE_TEXT)
<Attribute: Color (Text)>
Attribute.objects.create(name='Color', datatype=Attribute.TYPE_TEXT)
# = <Attribute: Color (Text)>
>>> yes = EnumValue.objects.create(value='yes')
>>> no = EnumValue.objects.create(value='no')
>>> unkown = EnumValue.objects.create(value='unkown')
>>> ynu = EnumGroup.objects.create(name='Yes / No / Unkown')
>>> ynu.enums.add(yes, no, unkown)
>>> Attribute.objects.create(name='Has Fever?',
... datatype=Attribute.TYPE_ENUM,
... enum_group=ynu)
<Attribute: Has Fever? (Multiple Choice)>
yes = EnumValue.objects.create(value='yes')
no = EnumValue.objects.create(value='no')
unknown = EnumValue.objects.create(value='unknown')
ynu = EnumGroup.objects.create(name='Yes / No / Unknown')
ynu.enums.add(yes, no, unknown)
Attribute.objects.create(name='has fever?', datatype=Attribute.TYPE_ENUM, enum_group=ynu)
# = <Attribute: has fever? (Multiple Choice)>
.. warning:: Once an Attribute has been used by an entity, you can not
change it's datatype.
'''
class Meta:
ordering = ['name']
TYPE_TEXT = 'text'
TYPE_FLOAT = 'float'
TYPE_INT = 'int'
TYPE_DATE = 'date'
TYPE_TEXT = 'text'
TYPE_FLOAT = 'float'
TYPE_INT = 'int'
TYPE_DATE = 'date'
TYPE_BOOLEAN = 'bool'
TYPE_OBJECT = 'object'
TYPE_ENUM = 'enum'
TYPE_OBJECT = 'object'
TYPE_ENUM = 'enum'
DATATYPE_CHOICES = (
(TYPE_TEXT, _(u"Text")),
(TYPE_FLOAT, _(u"Float")),
(TYPE_INT, _(u"Integer")),
(TYPE_DATE, _(u"Date")),
(TYPE_BOOLEAN, _(u"True / False")),
(TYPE_OBJECT, _(u"Django Object")),
(TYPE_ENUM, _(u"Multiple Choice")),
(TYPE_TEXT, _('Text')),
(TYPE_DATE, _('Date')),
(TYPE_FLOAT, _('Float')),
(TYPE_INT, _('Integer')),
(TYPE_BOOLEAN, _('True / False')),
(TYPE_OBJECT, _('Django Object')),
(TYPE_ENUM, _('Multiple Choice')),
)
name = models.CharField(_(u"name"), max_length=100,
help_text=_(u"User-friendly attribute name"))
# Core attributes
slug = EavSlugField(_(u"slug"), max_length=50, db_index=True, unique=True,
help_text=_(u"Short unique attribute label"))
datatype = EavDatatypeField(
verbose_name = _('Data Type'),
choices = DATATYPE_CHOICES,
max_length = 6
)
description = models.CharField(_(u"description"), max_length=256,
blank=True, null=True,
help_text=_(u"Short description"))
name = models.CharField(
verbose_name = _('Name'),
max_length = 100,
help_text = _('User-friendly attribute name')
)
enum_group = models.ForeignKey(EnumGroup, verbose_name=_(u"choice group"),
on_delete=models.PROTECT,
blank=True, null=True)
'''
Main identifer for the attribute.
Upon creation, slug is autogenerated from the name.
(see :meth:`~eav.fields.EavSlugField.create_slug_from_name`).
'''
slug = EavSlugField(
verbose_name = _('Slug'),
max_length = 50,
db_index = True,
unique = True,
help_text = _('Short unique attribute label')
)
'''
.. warning::
This attribute should be used with caution. Setting this to *True*
means that *all* entities that *can* have this attribute will
be required to have a value for it.
'''
required = models.BooleanField(verbose_name = _('Required'), default = False)
enum_group = models.ForeignKey(
EnumGroup,
verbose_name = _('Choice Group'),
on_delete = models.PROTECT,
blank = True,
null = True
)
description = models.CharField(
verbose_name = _('Description'),
max_length = 256,
blank = True,
null = True,
help_text = _('Short description')
)
# Useful meta-information
display_order = models.PositiveIntegerField(
verbose_name = _('Display order'),
default = 1
)
modified = models.DateTimeField(
verbose_name = _('Modified'),
auto_now = True
)
created = models.DateTimeField(
verbose_name = _('Created'),
default = timezone.now,
editable = False
)
@property
def help_text(self):
return self.description
datatype = EavDatatypeField(_(u"data type"), max_length=6,
choices=DATATYPE_CHOICES)
created = models.DateTimeField(_(u"created"), default=timezone.now,
editable=False)
modified = models.DateTimeField(_(u"modified"), auto_now=True)
required = models.BooleanField(_(u"required"), default=False)
display_order = models.PositiveIntegerField(_(u"display order"), default=1)
objects = models.Manager()
def get_validators(self):
'''
Returns the appropriate validator function from :mod:`~eav.validators`
@ -189,17 +222,16 @@ class Attribute(models.Model):
validators to return as well as the default, built-in one.
'''
DATATYPE_VALIDATORS = {
'text': validate_text,
'float': validate_float,
'int': validate_int,
'date': validate_date,
'bool': validate_bool,
'text': validate_text,
'float': validate_float,
'int': validate_int,
'date': validate_date,
'bool': validate_bool,
'object': validate_object,
'enum': validate_enum,
'enum': validate_enum,
}
validation_function = DATATYPE_VALIDATORS[self.datatype]
return [validation_function]
return [DATATYPE_VALIDATORS[self.datatype]]
def validate_value(self, value):
'''
@ -208,19 +240,22 @@ class Attribute(models.Model):
'''
for validator in self.get_validators():
validator(value)
if self.datatype == self.TYPE_ENUM:
if value not in self.enum_group.enums.all():
raise ValidationError(_(u"%(enum)s is not a valid choice "
u"for %(attr)s") % \
{'enum': value, 'attr': self})
raise ValidationError(
_('%(val)s is not a valid choice for %(attr)s')
% dict(val = value, attr = self)
)
def save(self, *args, **kwargs):
'''
Saves the Attribute and auto-generates a slug field if one wasn't
provided.
Saves the Attribute and auto-generates a slug field
if one wasn't provided.
'''
if not self.slug:
self.slug = EavSlugField.create_slug_from_name(self.name)
self.full_clean()
super(Attribute, self).save(*args, **kwargs)
@ -231,27 +266,25 @@ class Attribute(models.Model):
or if the attribute is not *TYPE_ENUM* and the enum group is set.
'''
if self.datatype == self.TYPE_ENUM and not self.enum_group:
raise ValidationError(_(
u"You must set the choice group for multiple choice" \
u"attributes"))
raise ValidationError(
_('You must set the choice group for multiple choice attributes')
)
if self.datatype != self.TYPE_ENUM and self.enum_group:
raise ValidationError(_(
u"You can only assign a choice group to multiple choice " \
u"attributes"))
raise ValidationError(
_('You can only assign a choice group to multiple choice attributes')
)
def get_choices(self):
'''
Returns a query set of :class:`EnumValue` objects for this attribute.
Returns None if the datatype of this attribute is not *TYPE_ENUM*.
'''
if not self.datatype == Attribute.TYPE_ENUM:
return None
return self.enum_group.enums.all()
return self.enum_group.enums.all() if self.datatype == Attribute.TYPE_ENUM else None
def save_value(self, entity, value):
'''
Called with *entity*, any django object registered with eav, and
Called with *entity*, any Django object registered with eav, and
*value*, the :class:`Value` this attribute for *entity* should
be set to.
@ -260,19 +293,26 @@ class Attribute(models.Model):
.. note::
If *value* is None and a :class:`Value` object exists for this
Attribute and *entity*, it will delete that :class:`Value` object.
Attribute and *entity*, it will delete that :class:`Value` object.
'''
ct = ContentType.objects.get_for_model(entity)
try:
value_obj = self.value_set.get(entity_ct=ct,
entity_id=entity.pk,
attribute=self)
value_obj = self.value_set.get(
entity_ct = ct,
entity_id = entity.pk,
attribute = self
)
except Value.DoesNotExist:
if value == None or value == '':
return
value_obj = Value.objects.create(entity_ct=ct,
entity_id=entity.pk,
attribute=self)
value_obj = Value.objects.create(
entity_ct = ct,
entity_id = entity.pk,
attribute = self
)
if value == None or value == '':
value_obj.delete()
return
@ -281,8 +321,8 @@ class Attribute(models.Model):
value_obj.value = value
value_obj.save()
def __unicode__(self):
return u"%s (%s)" % (self.name, self.get_datatype_display())
def __str__(self):
return '{} ({})'.format(self.name, self.get_datatype_display())
class Value(models.Model):
@ -293,49 +333,71 @@ class Value(models.Model):
As with most EAV implementations, most of the columns of this model will
be blank, as onle one *value_* field will be used.
Example:
Example::
>>> import eav
>>> from django.contrib.auth.models import User
>>> eav.register(User)
>>> u = User.objects.create(username='crazy_dev_user')
>>> a = Attribute.objects.create(name='Favorite Drink', datatype='text',
... slug='fav_drink')
>>> Value.objects.create(entity=u, attribute=a, value_text='red bull')
<Value: crazy_dev_user - Favorite Drink: "red bull">
import eav
from django.contrib.auth.models import User
eav.register(User)
u = User.objects.create(username='crazy_dev_user')
a = Attribute.objects.create(name='Fav Drink', datatype='text')
Value.objects.create(entity = u, attribute = a, value_text = 'red bull')
# = <Value: crazy_dev_user - Fav Drink: "red bull">
'''
entity_ct = models.ForeignKey(ContentType, on_delete=models.PROTECT, related_name='value_entities')
entity_id = models.IntegerField()
entity = generic.GenericForeignKey(ct_field='entity_ct',
fk_field='entity_id')
entity_ct = models.ForeignKey(
ContentType,
on_delete = models.PROTECT,
related_name = 'value_entities'
)
value_text = models.TextField(blank=True, null=True)
value_float = models.FloatField(blank=True, null=True)
value_int = models.IntegerField(blank=True, null=True)
value_date = models.DateTimeField(blank=True, null=True)
value_bool = models.NullBooleanField(blank=True, null=True)
value_enum = models.ForeignKey(EnumValue, blank=True, null=True,
on_delete=models.PROTECT,
related_name='eav_values')
entity_id = models.IntegerField()
entity = generic.GenericForeignKey(ct_field = 'entity_ct', fk_field = 'entity_id')
value_text = models.TextField(blank = True, null = True)
value_float = models.FloatField(blank = True, null = True)
value_int = models.IntegerField(blank = True, null = True)
value_date = models.DateTimeField(blank = True, null = True)
value_bool = models.NullBooleanField(blank = True, null = True)
value_enum = models.ForeignKey(
EnumValue,
blank = True,
null = True,
on_delete = models.PROTECT,
related_name = 'eav_values'
)
generic_value_id = models.IntegerField(blank=True, null=True)
generic_value_ct = models.ForeignKey(ContentType, blank=True, null=True,
on_delete=models.PROTECT,
related_name='value_values')
value_object = generic.GenericForeignKey(ct_field='generic_value_ct',
fk_field='generic_value_id')
created = models.DateTimeField(_(u"created"), default=timezone.now)
modified = models.DateTimeField(_(u"modified"), auto_now=True)
generic_value_ct = models.ForeignKey(
ContentType,
blank = True,
null = True,
on_delete = models.PROTECT,
related_name ='value_values'
)
attribute = models.ForeignKey(Attribute, db_index=True,
on_delete=models.PROTECT,
verbose_name=_(u"attribute"))
value_object = generic.GenericForeignKey(
ct_field = 'generic_value_ct',
fk_field = 'generic_value_id'
)
created = models.DateTimeField(_('Created'), default = timezone.now)
modified = models.DateTimeField(_('Modified'), auto_now = True)
attribute = models.ForeignKey(
Attribute,
db_index = True,
on_delete = models.PROTECT,
verbose_name = _('attribute')
)
def save(self, *args, **kwargs):
'''
Validate and save this value
Validate and save this value.
'''
self.full_clean()
super(Value, self).save(*args, **kwargs)
@ -345,13 +407,12 @@ class Value(models.Model):
Raises ``ValidationError`` if this value's attribute is *TYPE_ENUM*
and value_enum is not a valid choice for this value's attribute.
'''
if self.attribute.datatype == Attribute.TYPE_ENUM and \
self.value_enum:
if self.attribute.datatype == Attribute.TYPE_ENUM and self.value_enum:
if self.value_enum not in self.attribute.enum_group.enums.all():
raise ValidationError(_(u"%(choice)s is not a valid " \
u"choice for %s(attribute)") % \
{'choice': self.value_enum,
'attribute': self.attribute})
raise ValidationError(
_('%(enum)s is not a valid choice for %(attr)s')
% dict(enum = self.value_enum, attr = self.attribute)
)
def _get_value(self):
'''
@ -376,22 +437,43 @@ class Value(models.Model):
class Entity(object):
'''
The helper class that will be attached to any entity registered with
eav.
The helper class that will be attached to any entity
registered with eav.
'''
@staticmethod
def post_save_handler(sender, *args, **kwargs):
'''
Post save handler attached to self.model. Calls :meth:`save` when
the model instance we are attached to is saved.
'''
instance = kwargs['instance']
entity = getattr(instance, instance._eav_config_cls.eav_attr)
entity.save()
@staticmethod
def pre_save_handler(sender, *args, **kwargs):
'''
Pre save handler attached to self.model. Called before the
model instance we are attached to is saved. This allows us to call
:meth:`validate_attributes` before the entity is saved.
'''
instance = kwargs['instance']
entity = getattr(kwargs['instance'], instance._eav_config_cls.eav_attr)
entity.validate_attributes()
def __init__(self, instance):
'''
Set self.model equal to the instance of the model that we're attached
to. Also, store the content type of that instance.
to. Also, store the content type of that instance.
'''
self.model = instance
self.ct = ContentType.objects.get_for_model(instance)
def __getattr__(self, name):
'''
Tha magic getattr helper. This is called whenevery you do
this_instance.<whatever>
Tha magic getattr helper. This is called whenever user invokes::
instance.<attribute>
Checks if *name* is a valid slug for attributes available to this
instances. If it is, tries to lookup the :class:`Value` with that
@ -403,13 +485,16 @@ class Entity(object):
try:
attribute = self.get_attribute_by_slug(name)
except Attribute.DoesNotExist:
raise AttributeError(_(u"%(obj)s has no EAV attribute named " \
u"'%(attr)s'") % \
{'obj': self.model, 'attr': name})
raise AttributeError(
_('%(obj)s has no EAV attribute named %(attr)s')
% dict(obj = self.model, attr = name)
)
try:
return self.get_value_by_attribute(attribute).value
except Value.DoesNotExist:
return None
return getattr(super(Entity, self), name)
def get_all_attributes(self):
@ -421,15 +506,17 @@ class Entity(object):
def _hasattr(self, attribute_slug):
'''
Since we override __getattr__ with a backdown to the database, this exists as a way of
checking whether a user has set a real attribute on ourselves, without going to the db if not
Since we override __getattr__ with a backdown to the database, this
exists as a way of checking whether a user has set a real attribute on
ourselves, without going to the db if not.
'''
return attribute_slug in self.__dict__
def _getattr(self, attribute_slug):
'''
Since we override __getattr__ with a backdown to the database, this exists as a way of
getting the value a user set for one of our attributes, without going to the db to check
Since we override __getattr__ with a backdown to the database, this
exists as a way of getting the value a user set for one of our
attributes, without going to the db to check.
'''
return self.__dict__[attribute_slug]
@ -453,6 +540,7 @@ class Entity(object):
for attribute in self.get_all_attributes():
value = None
if self._hasattr(attribute.slug):
value = self._getattr(attribute.slug)
else:
@ -460,19 +548,21 @@ class Entity(object):
if value is None:
if attribute.required:
raise ValidationError(_(u"%(attr)s EAV field cannot " \
u"be blank") % \
{'attr': attribute.slug})
raise ValidationError(_(
'{} EAV field cannot be blank'.format(attribute.slug)
))
else:
try:
attribute.validate_value(value)
except ValidationError as e:
raise ValidationError(_(u"%(attr)s EAV field %(err)s") % \
{'attr': attribute.slug,
'err': e})
raise ValidationError(
_('%(attr)s EAV field %(err)s')
% dict(attr = attribute.slug, err = e)
)
def get_values_dict(self):
values_dict = dict()
for value in self.get_values():
values_dict[value.attribute.slug] = value.value
@ -482,8 +572,10 @@ class Entity(object):
'''
Get all set :class:`Value` objects for self.model
'''
return Value.objects.filter(entity_ct=self.ct,
entity_id=self.model.pk).select_related()
return Value.objects.filter(
entity_ct=self.ct,
entity_id=self.model.pk
).select_related()
def get_all_attribute_slugs(self):
'''
@ -493,52 +585,20 @@ class Entity(object):
def get_attribute_by_slug(self, slug):
'''
Returns a single :class:`Attribute` with *slug*
Returns a single :class:`Attribute` with *slug*.
'''
return self.get_all_attributes().get(slug=slug)
def get_value_by_attribute(self, attribute):
'''
Returns a single :class:`Value` for *attribute*
Returns a single :class:`Value` for *attribute*.
'''
return self.get_values().get(attribute=attribute)
def __iter__(self):
'''
Iterate over set eav values.
Iterate over set eav values. This would allow you to do::
This would allow you to do:
>>> for i in m.eav: print i # doctest:SKIP
for i in m.eav: print(i)
'''
return iter(self.get_values())
@staticmethod
def post_save_handler(sender, *args, **kwargs):
'''
Post save handler attached to self.model. Calls :meth:`save` when
the model instance we are attached to is saved.
'''
instance = kwargs['instance']
entity = getattr(instance, instance._eav_config_cls.eav_attr)
entity.save()
@staticmethod
def pre_save_handler(sender, *args, **kwargs):
'''
Pre save handler attached to self.model. Called before the
model instance we are attached to is saved. This allows us to call
:meth:`validate_attributes` before the entity is saved.
'''
instance = kwargs['instance']
entity = getattr(kwargs['instance'], instance._eav_config_cls.eav_attr)
entity.validate_attributes()
if 'django_nose' in settings.INSTALLED_APPS:
'''
The django_nose test runner won't automatically create our Patient model
database table which is required for tests, unless we import it here.
Please, someone tell me a better way to do this.
'''
from .tests.models import Patient, Encounter

View file

@ -11,7 +11,7 @@ class MiscModels(TestCase):
def test_enumgroup_str(self):
name = 'Yes / No'
e = EnumGroup.objects.create(name=name)
self.assertEqual(str(e), name)
self.assertEqual('<EnumGroup Yes / No>', str(e))
def test_attribute_help_text(self):
desc = 'Patient Age'