mirror of
https://github.com/jazzband/django-eav2.git
synced 2026-03-16 22:40:26 +00:00
Add support for Entity models with UUID as a primary key. (#89)
* chore: fix repo links * style: flake8 format models.Value * test: add test for an entity with uuid pk * feat: support entity models with uuid as pk * test: check Value repr and str methods
This commit is contained in:
parent
edd050dd98
commit
a98766fc55
8 changed files with 190 additions and 61 deletions
0
eav/logic/__init__.py
Normal file
0
eav/logic/__init__.py
Normal file
12
eav/logic/entity_pk.py
Normal file
12
eav/logic/entity_pk.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from django.db.models.fields import UUIDField
|
||||
|
||||
|
||||
def get_entity_pk_type(entity_cls) -> str:
|
||||
"""Returns the entity PK type to use.
|
||||
|
||||
These values map to `models.Value` as potential fields to use to relate
|
||||
to the proper entity via the correct PK type.
|
||||
"""
|
||||
if isinstance(entity_cls._meta.pk, UUIDField):
|
||||
return 'entity_uuid'
|
||||
return 'entity_id'
|
||||
22
eav/migrations/0006_add_entity_uuid.py
Normal file
22
eav/migrations/0006_add_entity_uuid.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
"""Creates UUID field to map to Entity FK."""
|
||||
|
||||
dependencies = [
|
||||
('eav', '0005_auto_20210510_1305'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='value',
|
||||
name='entity_uuid',
|
||||
field=models.UUIDField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='value',
|
||||
name='entity_id',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
159
eav/models.py
159
eav/models.py
|
|
@ -20,6 +20,8 @@ from django.db.models.base import ModelBase
|
|||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from eav.logic.entity_pk import get_entity_pk_type
|
||||
|
||||
try:
|
||||
from django.db.models import JSONField
|
||||
except ImportError:
|
||||
|
|
@ -334,17 +336,19 @@ class Attribute(models.Model):
|
|||
"""
|
||||
ct = ContentType.objects.get_for_model(entity)
|
||||
|
||||
entity_filter = {
|
||||
'entity_ct': ct,
|
||||
'attribute': self,
|
||||
'{0}'.format(get_entity_pk_type(entity)): entity.pk,
|
||||
}
|
||||
|
||||
try:
|
||||
value_obj = self.value_set.get(
|
||||
entity_ct=ct, entity_id=entity.pk, attribute=self
|
||||
)
|
||||
value_obj = self.value_set.get(**entity_filter)
|
||||
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_filter)
|
||||
|
||||
if value == None or value == '':
|
||||
value_obj.delete()
|
||||
|
|
@ -358,10 +362,11 @@ class Attribute(models.Model):
|
|||
return '{} ({})'.format(self.name, self.get_datatype_display())
|
||||
|
||||
|
||||
class Value(models.Model):
|
||||
"""
|
||||
Putting the **V** in *EAV*. This model stores the value for one particular
|
||||
:class:`Attribute` for some entity.
|
||||
class Value(models.Model): # noqa: WPS110
|
||||
"""Putting the **V** in *EAV*.
|
||||
|
||||
This model stores the value for one particular :class:`Attribute` for
|
||||
some entity.
|
||||
|
||||
As with most EAV implementations, most of the columns of this model will
|
||||
be blank, as onle one *value_* field will be used.
|
||||
|
|
@ -380,22 +385,58 @@ class Value(models.Model):
|
|||
# = <Value: crazy_dev_user - Fav Drink: "red bull">
|
||||
"""
|
||||
|
||||
entity_ct = models.ForeignKey(
|
||||
ContentType, on_delete=models.PROTECT, related_name='value_entities'
|
||||
# Direct foreign keys
|
||||
attribute = models.ForeignKey(
|
||||
Attribute,
|
||||
db_index=True,
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_('Attribute'),
|
||||
)
|
||||
|
||||
entity_id = models.IntegerField()
|
||||
entity = generic.GenericForeignKey(ct_field='entity_ct', fk_field='entity_id')
|
||||
# Entity generic relationships. Rather than rely on database casting,
|
||||
# this will instead use a separate ForeignKey field attribute that matches
|
||||
# the FK type of the entity.
|
||||
entity_id = models.IntegerField(blank=True, null=True)
|
||||
entity_uuid = models.UUIDField(blank=True, null=True)
|
||||
|
||||
value_text = models.TextField(blank=True, null=True)
|
||||
entity_ct = models.ForeignKey(
|
||||
ContentType,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='value_entities',
|
||||
)
|
||||
|
||||
entity_pk_int = generic.GenericForeignKey(
|
||||
ct_field='entity_ct',
|
||||
fk_field='entity_id',
|
||||
)
|
||||
|
||||
entity_pk_uuid = generic.GenericForeignKey(
|
||||
ct_field='entity_ct',
|
||||
fk_field='entity_uuid',
|
||||
)
|
||||
|
||||
# Model attributes
|
||||
created = models.DateTimeField(
|
||||
_('Created'),
|
||||
default=timezone.now,
|
||||
)
|
||||
|
||||
modified = models.DateTimeField(_('Modified'), auto_now=True)
|
||||
|
||||
# Value attributes
|
||||
value_bool = models.BooleanField(blank=True, null=True)
|
||||
value_csv = CSVField(blank=True, null=True)
|
||||
value_date = models.DateTimeField(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.BooleanField(blank=True, null=True)
|
||||
value_text = models.TextField(blank=True, null=True)
|
||||
|
||||
value_json = JSONField(
|
||||
default=dict, encoder=DjangoJSONEncoder, blank=True, null=True
|
||||
default=dict,
|
||||
encoder=DjangoJSONEncoder,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
value_csv = CSVField(blank=True, null=True)
|
||||
|
||||
value_enum = models.ForeignKey(
|
||||
EnumValue,
|
||||
|
|
@ -405,6 +446,7 @@ class Value(models.Model):
|
|||
related_name='eav_values',
|
||||
)
|
||||
|
||||
# Value object relationship
|
||||
generic_value_id = models.IntegerField(blank=True, null=True)
|
||||
|
||||
generic_value_ct = models.ForeignKey(
|
||||
|
|
@ -416,42 +458,46 @@ class Value(models.Model):
|
|||
)
|
||||
|
||||
value_object = generic.GenericForeignKey(
|
||||
ct_field='generic_value_ct', fk_field='generic_value_id'
|
||||
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.
|
||||
"""
|
||||
self.full_clean()
|
||||
super(Value, self).save(*args, **kwargs)
|
||||
|
||||
def _get_value(self):
|
||||
"""
|
||||
Return the python object this value is holding
|
||||
"""
|
||||
return getattr(self, 'value_%s' % self.attribute.datatype)
|
||||
|
||||
def _set_value(self, new_value):
|
||||
"""
|
||||
Set the object this value is holding
|
||||
"""
|
||||
setattr(self, 'value_%s' % self.attribute.datatype, new_value)
|
||||
|
||||
value = property(_get_value, _set_value)
|
||||
|
||||
def __str__(self):
|
||||
return '{}: "{}" ({})'.format(self.attribute.name, self.value, self.entity)
|
||||
"""String representation of a Value."""
|
||||
entity = self.entity_pk_int
|
||||
if self.entity_uuid:
|
||||
entity = self.entity_pk_uuid
|
||||
return '{0}: "{1}" ({2})'.format(
|
||||
self.attribute.name,
|
||||
self.value,
|
||||
entity,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return '{}: "{}" ({})'.format(self.attribute.name, self.value, self.entity.pk)
|
||||
"""Representation of Value object."""
|
||||
entity = self.entity_pk_int
|
||||
if self.entity_uuid:
|
||||
entity = self.entity_pk_uuid
|
||||
return '{0}: "{1}" ({2})'.format(
|
||||
self.attribute.name,
|
||||
self.value,
|
||||
entity.pk,
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Validate and save this value."""
|
||||
self.full_clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def _get_value(self):
|
||||
"""Return the python object this value is holding."""
|
||||
return getattr(self, 'value_{0}'.format(self.attribute.datatype))
|
||||
|
||||
def _set_value(self, new_value):
|
||||
"""Set the object this value is holding."""
|
||||
setattr(self, 'value_{0}'.format(self.attribute.datatype), new_value)
|
||||
|
||||
value = property(_get_value, _set_value) # noqa: WPS110
|
||||
|
||||
|
||||
class Entity(object):
|
||||
|
|
@ -604,12 +650,13 @@ class Entity(object):
|
|||
return {v.attribute.slug: v.value for v in self.get_values()}
|
||||
|
||||
def get_values(self):
|
||||
"""
|
||||
Get all set :class:`Value` objects for self.instance
|
||||
"""
|
||||
return Value.objects.filter(
|
||||
entity_ct=self.ct, entity_id=self.instance.pk
|
||||
).select_related()
|
||||
"""Get all set :class:`Value` objects for self.instance."""
|
||||
entity_filter = {
|
||||
'entity_ct': self.ct,
|
||||
'{0}'.format(get_entity_pk_type(self.instance)): self.instance.pk,
|
||||
}
|
||||
|
||||
return Value.objects.filter(**entity_filter).select_related()
|
||||
|
||||
def get_all_attribute_slugs(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ from django.db.models.signals import post_init, post_save, pre_save
|
|||
from eav.managers import EntityManager
|
||||
from eav.models import Attribute, Entity, Value
|
||||
|
||||
from eav.logic.entity_pk import get_entity_pk_type
|
||||
|
||||
|
||||
class EavConfig(object):
|
||||
"""
|
||||
|
|
@ -149,9 +151,7 @@ class Registry(object):
|
|||
post_save.disconnect(Entity.post_save_handler, sender=self.model_cls)
|
||||
|
||||
def _attach_generic_relation(self):
|
||||
"""
|
||||
Set up the generic relation for the entity
|
||||
"""
|
||||
"""Set up the generic relation for the entity."""
|
||||
rel_name = (
|
||||
self.config_cls.generic_relation_related_name or self.model_cls.__name__
|
||||
)
|
||||
|
|
@ -159,7 +159,7 @@ class Registry(object):
|
|||
gr_name = self.config_cls.generic_relation_attr.lower()
|
||||
generic_relation = generic.GenericRelation(
|
||||
Value,
|
||||
object_id_field='entity_id',
|
||||
object_id_field=get_entity_pk_type(self.model_cls),
|
||||
content_type_field='entity_ct',
|
||||
related_query_name=rel_name,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import uuid
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
from test_project.models import MAX_CHARFIELD_LEN
|
||||
|
|
@ -137,4 +139,22 @@ class Migration(migrations.Migration):
|
|||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Doctor',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
('name', models.CharField(max_length=MAX_CHARFIELD_LEN)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import sys
|
||||
import uuid
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Final, final
|
||||
|
|
@ -24,6 +25,15 @@ class TestBase(models.Model):
|
|||
abstract = True
|
||||
|
||||
|
||||
@final
|
||||
@register_eav()
|
||||
class Doctor(TestBase):
|
||||
"""Test model using UUID as primary key."""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=MAX_CHARFIELD_LEN)
|
||||
|
||||
|
||||
@final
|
||||
class Patient(TestBase):
|
||||
name = models.CharField(max_length=MAX_CHARFIELD_LEN)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import eav
|
|||
from eav.exceptions import IllegalAssignmentException
|
||||
from eav.models import Attribute, Value
|
||||
from eav.registry import EavConfig
|
||||
from test_project.models import Encounter, Patient, RegisterTestModel
|
||||
from test_project.models import Doctor, Encounter, Patient, RegisterTestModel
|
||||
|
||||
|
||||
class Attributes(TestCase):
|
||||
|
|
@ -66,6 +66,11 @@ class Attributes(TestCase):
|
|||
self.assertEqual(t.eav.age, 6)
|
||||
self.assertEqual(t.eav.height, 10)
|
||||
|
||||
# Validate repr of Value for an entity with an INT PK
|
||||
v1 = Value.objects.filter(entity_id=p.pk).first()
|
||||
assert isinstance(repr(v1), str)
|
||||
assert isinstance(str(v1), str)
|
||||
|
||||
def test_illegal_assignemnt(self):
|
||||
class EncounterEavConfig(EavConfig):
|
||||
@classmethod
|
||||
|
|
@ -81,3 +86,16 @@ class Attributes(TestCase):
|
|||
with self.assertRaises(IllegalAssignmentException):
|
||||
e.eav.color = 'red'
|
||||
e.save()
|
||||
|
||||
def test_uuid_pk(self):
|
||||
"""Tests for when model pk is UUID."""
|
||||
d1 = Doctor.objects.create(name='Lu')
|
||||
d1.eav.age = 10
|
||||
d1.save()
|
||||
|
||||
assert d1.eav.age == 10
|
||||
|
||||
# Validate repr of Value for an entity with a UUID PK
|
||||
v1 = Value.objects.filter(entity_uuid=d1.pk).first()
|
||||
assert isinstance(repr(v1), str)
|
||||
assert isinstance(str(v1), str)
|
||||
|
|
|
|||
Loading…
Reference in a new issue