From 6f3329d2ae97a8423798fa6462b46f2a617b5b49 Mon Sep 17 00:00:00 2001 From: Iwo Herka Date: Wed, 20 Jun 2018 14:23:30 +0200 Subject: [PATCH] Work on code style in models (#3) --- eav/models.py | 460 ++++++++++++++++++++++++------------------- tests/misc_models.py | 2 +- 2 files changed, 261 insertions(+), 201 deletions(-) diff --git a/eav/models.py b/eav/models.py index aa8f471..6d72d9c 100644 --- a/eav/models.py +++ b/eav/models.py @@ -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.objects.create(name='has fever?', datatype=Attribute.TYPE_ENUM, enum_group=ynu) + # = .. 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 ''.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 ''.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.objects.create(name='Height', datatype=Attribute.TYPE_INT) + # = - >>> Attribute.objects.create(name='Color', datatype=Attribute.TYPE_TEXT) - + Attribute.objects.create(name='Color', datatype=Attribute.TYPE_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) - + 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) + # = .. 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') - + 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') + # = ''' - 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. + Tha magic getattr helper. This is called whenever user invokes:: + + instance. 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 diff --git a/tests/misc_models.py b/tests/misc_models.py index 4f28512..0c0a02f 100644 --- a/tests/misc_models.py +++ b/tests/misc_models.py @@ -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('', str(e)) def test_attribute_help_text(self): desc = 'Patient Age'