django-eav2/eav/models/value.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

232 lines
6.5 KiB
Python
Raw Normal View History

from __future__ import annotations
2023-11-30 18:05:20 +00:00
from typing import TYPE_CHECKING, ClassVar, Optional
2023-11-30 18:05:20 +00:00
from django.contrib.contenttypes import fields as generic
from django.contrib.contenttypes.models import ContentType
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.db.models import ForeignKey
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from eav.fields import CSVField
2023-11-30 18:11:59 +00:00
from eav.logic.managers import ValueManager
2023-11-30 18:05:20 +00:00
from eav.logic.object_pk import get_pk_format
if TYPE_CHECKING:
from .attribute import Attribute
from .enum_value import EnumValue
class Value(models.Model):
"""
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.
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='Fav Drink', datatype='text')
Value.objects.create(entity = u, attribute = a, value_text = 'red bull')
# = <Value: crazy_dev_user - Fav Drink: "red bull">
"""
id = get_pk_format()
# Direct foreign keys
attribute: ForeignKey[Attribute] = ForeignKey(
2023-11-30 18:05:20 +00:00
"eav.Attribute",
db_index=True,
on_delete=models.PROTECT,
verbose_name=_("Attribute"),
2023-11-30 18:05:20 +00:00
)
# 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,
verbose_name=_("Entity id"),
2023-11-30 18:05:20 +00:00
)
entity_uuid = models.UUIDField(
blank=True,
null=True,
verbose_name=_("Entity uuid"),
2023-11-30 18:05:20 +00:00
)
entity_ct = ForeignKey(
ContentType,
on_delete=models.PROTECT,
related_name="value_entities",
verbose_name=_("Entity ct"),
2023-11-30 18:05:20 +00:00
)
entity_pk_int = generic.GenericForeignKey(
ct_field="entity_ct",
fk_field="entity_id",
2023-11-30 18:05:20 +00:00
)
entity_pk_uuid = generic.GenericForeignKey(
ct_field="entity_ct",
fk_field="entity_uuid",
2023-11-30 18:05:20 +00:00
)
# Model attributes
created = models.DateTimeField(
default=timezone.now,
verbose_name=_("Created"),
2023-11-30 18:05:20 +00:00
)
modified = models.DateTimeField(
auto_now=True,
verbose_name=_("Modified"),
2023-11-30 18:05:20 +00:00
)
# Value attributes
value_bool = models.BooleanField(
blank=True,
null=True,
verbose_name=_("Value bool"),
2023-11-30 18:05:20 +00:00
)
value_csv = CSVField(
blank=True,
null=True,
verbose_name=_("Value CSV"),
2023-11-30 18:05:20 +00:00
)
value_date = models.DateTimeField(
blank=True,
null=True,
verbose_name=_("Value date"),
2023-11-30 18:05:20 +00:00
)
value_float = models.FloatField(
blank=True,
null=True,
verbose_name=_("Value float"),
2023-11-30 18:05:20 +00:00
)
value_int = models.BigIntegerField(
blank=True,
null=True,
verbose_name=_("Value int"),
2023-11-30 18:05:20 +00:00
)
value_text = models.TextField(
blank=True,
default="",
verbose_name=_("Value text"),
2023-11-30 18:05:20 +00:00
)
value_json = models.JSONField(
default=dict,
encoder=DjangoJSONEncoder,
blank=True,
null=True,
verbose_name=_("Value JSON"),
2023-11-30 18:05:20 +00:00
)
value_enum: ForeignKey[Optional[EnumValue]] = ForeignKey(
2023-11-30 18:05:20 +00:00
"eav.EnumValue",
blank=True,
null=True,
on_delete=models.PROTECT,
related_name="eav_values",
verbose_name=_("Value enum"),
2023-11-30 18:05:20 +00:00
)
# Value object relationship
generic_value_id = models.IntegerField(
blank=True,
null=True,
verbose_name=_("Generic value id"),
2023-11-30 18:05:20 +00:00
)
generic_value_ct = ForeignKey(
ContentType,
blank=True,
null=True,
on_delete=models.PROTECT,
related_name="value_values",
verbose_name=_("Generic value content type"),
2023-11-30 18:05:20 +00:00
)
value_object = generic.GenericForeignKey(
ct_field="generic_value_ct",
fk_field="generic_value_id",
2023-11-30 18:05:20 +00:00
)
objects = ValueManager()
class Meta:
verbose_name = _("Value")
verbose_name_plural = _("Values")
constraints: ClassVar[list[models.Constraint]] = [
models.UniqueConstraint(
fields=["entity_ct", "attribute", "entity_uuid"],
name="unique_entity_uuid_per_attribute",
),
models.UniqueConstraint(
fields=["entity_ct", "attribute", "entity_id"],
name="unique_entity_id_per_attribute",
),
models.CheckConstraint(
check=(
models.Q(entity_id__isnull=False, entity_uuid__isnull=True)
| models.Q(entity_id__isnull=True, entity_uuid__isnull=False)
),
name="ensure_entity_id_xor_entity_uuid",
),
]
def __str__(self) -> str:
"""String representation of a Value."""
entity = self.entity_pk_uuid if self.entity_uuid else self.entity_pk_int
return f'{self.attribute.name}: "{self.value}" ({entity})'
def __repr__(self) -> str:
"""Representation of Value object."""
entity = self.entity_pk_uuid if self.entity_uuid else self.entity_pk_int
return f'{self.attribute.name}: "{self.value}" ({entity})'
def save(self, *args, **kwargs):
"""Validate and save this value."""
self.full_clean()
super().save(*args, **kwargs)
def natural_key(self) -> tuple[tuple[str, str], int, str]:
2023-11-30 18:05:20 +00:00
"""
Retrieve the natural key for the Value instance.
The natural key for a Value is a combination of its `attribute` natural key,
`entity_id`, and `entity_uuid`. This method returns a tuple containing these
three elements.
Returns
-------
tuple: A tuple containing the natural key of the attribute, entity ID,
and entity UUID of the Value instance.
"""
return (self.attribute.natural_key(), self.entity_id, self.entity_uuid)
def _get_value(self):
"""Return the python object this value is holding."""
return getattr(self, f"value_{self.attribute.datatype}")
2023-11-30 18:05:20 +00:00
def _set_value(self, new_value):
"""Set the object this value is holding."""
setattr(self, f"value_{self.attribute.datatype}", new_value)
2023-11-30 18:05:20 +00:00
value = property(_get_value, _set_value)