mirror of
https://github.com/jazzband/django-eav2.git
synced 2026-03-16 22:40:26 +00:00
216 lines
5.8 KiB
Python
216 lines
5.8 KiB
Python
|
|
# ruff: noqa: UP007
|
||
|
|
|
||
|
|
from typing import TYPE_CHECKING, 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">
|
||
|
|
"""
|
||
|
|
|
||
|
|
objects = ValueManager()
|
||
|
|
|
||
|
|
class Meta:
|
||
|
|
verbose_name = _('Value')
|
||
|
|
verbose_name_plural = _('Values')
|
||
|
|
|
||
|
|
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,
|
||
|
|
null=True,
|
||
|
|
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',
|
||
|
|
)
|
||
|
|
|
||
|
|
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 __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 _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)
|