mirror of
https://github.com/jazzband/django-eav2.git
synced 2026-03-17 06:50:24 +00:00
232 lines
6.6 KiB
Python
232 lines
6.6 KiB
Python
# ruff: noqa: UP007
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, ClassVar, Optional
|
|
|
|
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
|
|
from eav.logic.managers import ValueManager
|
|
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(
|
|
"eav.Attribute",
|
|
db_index=True,
|
|
on_delete=models.PROTECT,
|
|
verbose_name=_("Attribute"),
|
|
)
|
|
|
|
# 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"),
|
|
)
|
|
|
|
entity_uuid = models.UUIDField(
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_("Entity uuid"),
|
|
)
|
|
|
|
entity_ct = ForeignKey(
|
|
ContentType,
|
|
on_delete=models.PROTECT,
|
|
related_name="value_entities",
|
|
verbose_name=_("Entity ct"),
|
|
)
|
|
|
|
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(
|
|
default=timezone.now,
|
|
verbose_name=_("Created"),
|
|
)
|
|
|
|
modified = models.DateTimeField(
|
|
auto_now=True,
|
|
verbose_name=_("Modified"),
|
|
)
|
|
|
|
# Value attributes
|
|
value_bool = models.BooleanField(
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_("Value bool"),
|
|
)
|
|
value_csv = CSVField(
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_("Value CSV"),
|
|
)
|
|
value_date = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_("Value date"),
|
|
)
|
|
value_float = models.FloatField(
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_("Value float"),
|
|
)
|
|
value_int = models.BigIntegerField(
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_("Value int"),
|
|
)
|
|
value_text = models.TextField(
|
|
blank=True,
|
|
default="",
|
|
verbose_name=_("Value text"),
|
|
)
|
|
|
|
value_json = models.JSONField(
|
|
default=dict,
|
|
encoder=DjangoJSONEncoder,
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_("Value JSON"),
|
|
)
|
|
|
|
value_enum: ForeignKey[Optional[EnumValue]] = ForeignKey(
|
|
"eav.EnumValue",
|
|
blank=True,
|
|
null=True,
|
|
on_delete=models.PROTECT,
|
|
related_name="eav_values",
|
|
verbose_name=_("Value enum"),
|
|
)
|
|
|
|
# Value object relationship
|
|
generic_value_id = models.IntegerField(
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_("Generic value id"),
|
|
)
|
|
|
|
generic_value_ct = ForeignKey(
|
|
ContentType,
|
|
blank=True,
|
|
null=True,
|
|
on_delete=models.PROTECT,
|
|
related_name="value_values",
|
|
verbose_name=_("Generic value content type"),
|
|
)
|
|
|
|
value_object = generic.GenericForeignKey(
|
|
ct_field="generic_value_ct",
|
|
fk_field="generic_value_id",
|
|
)
|
|
|
|
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]:
|
|
"""
|
|
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}")
|
|
|
|
def _set_value(self, new_value):
|
|
"""Set the object this value is holding."""
|
|
setattr(self, f"value_{self.attribute.datatype}", new_value)
|
|
|
|
value = property(_get_value, _set_value)
|