Source code for superdjango.db.eav.models

# Imports

from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
from json import loads as json_loads
from superdjango.db.slug.utils import unique_slugify
from .constants import DATA_TYPE

# Exports

__all__ = (
    "AttributeModel",
    "ValueModel",
)

# Constants

AUTH_USER_MODEL = settings.AUTH_USER_MODEL

# Models


[docs]class AttributeModel(models.Model): """Base model for implementing extra attributes to be associated with a model.""" choices = models.TextField( _("choices"), blank=True, help_text=_("Used for the list data type. One choice per line."), null=True ) content_type = models.ForeignKey( ContentType, blank=True, help_text=_("Prompt the user to choose options from the selected content type."), null=True, on_delete=models.SET_NULL, verbose_name=_("choices from content type") ) decimal_places = models.PositiveSmallIntegerField( _("decimal places"), default=2, help_text=_("Decimal places for decimal fields.") ) DATA_TYPE_CHOICES = ( (DATA_TYPE.BOOLEAN, _('Boolean (yes/no)')), (DATA_TYPE.DATE, _('Date')), (DATA_TYPE.DATETIME, _('Date & Time')), (DATA_TYPE.DECIMAL, _('Decimal')), (DATA_TYPE.DURATION, _("Duration")), (DATA_TYPE.EMAIL, _('E-Mail Address')), (DATA_TYPE.FLOAT, _("Float")), (DATA_TYPE.INTEGER, _('Integer')), (DATA_TYPE.LIST, _('List')), (DATA_TYPE.IP_ADDRESS, _('IP Address')), (DATA_TYPE.SLUG, _('Slug')), (DATA_TYPE.TEXT, _('Text (Multi-Line)')), (DATA_TYPE.VARCHAR, _('Text (Single Line)')), (DATA_TYPE.TIME, _('Time')), (DATA_TYPE.URL, _('URL')), ) data_type = models.CharField( _("data type"), choices=DATA_TYPE_CHOICES, default=DATA_TYPE.VARCHAR, max_length=64 ) # There is currently no form support for grouping fields, but we're adding this here to (some day, optimistically) # support field groupings so that no new migrations are required. grouping = models.CharField( _("grouping"), blank=True, help_text=_("The fieldset or tab into which the field is organized."), max_length=128, null=True ) help_text = models.TextField( _("help text"), blank=True, help_text=_("Help displayed to the user regarding the attribute."), null=True ) is_enabled = models.BooleanField( _("enabled"), default=True, help_text=_("Indicates the attribute is available for use.") ) is_hidden = models.NullBooleanField( _("hidden"), help_text=_("Use this to hide options that are not configurable by the user. Be sure to specify a default.") ) is_required = models.BooleanField( _("required"), default=False, help_text=_("Make input for this attribute required. Be careful!") ) label = models.CharField( _("label"), help_text=_("A label or title of the attribute."), max_length=128 ) limit_choices_to = models.CharField( _("limit choices to"), blank=True, help_text=_("A filter statement in JSON format that may be used to limit content type choices."), max_length=128, null=True ) # 12,345,678.90 max_digits = models.PositiveSmallIntegerField( _("max digits"), default=10, help_text=_("Max number of digits for decimal fields.") ) max_length = models.PositiveSmallIntegerField( _("maximum length"), default=128, help_text=_("Max number of characters for single-line text.") ) min_length = models.PositiveSmallIntegerField( _("minimum length"), default=0, help_text=_("Minimum number of characters for single-line text.") ) # The name is for possible internal use and may not be displayed to a user-administrator. See save(). name = models.CharField( _("name"), blank=True, help_text=_("The name may be used as an identifier in reports, exports, and customizations."), max_length=256, unique=True ) # As of Wagtail 1.9 there does not appear to be a way to add and use a custom field in the ExtraAttributeAdminForm. # The default value is collected and cast to the appropriate data type. raw_default_value = models.CharField( _("default value"), blank=True, help_text=_("The default value, if any. However, it is recommended that all attributes have a default."), max_length=256, null=True ) sort_order = models.SmallIntegerField( _("sort order"), default=1 ) class Meta: abstract = True def __str__(self): return self.label @property def default_value(self): """Alias for ``get_default()``.""" return self.get_default()
[docs] def clean(self): """Make sure choices or content type has been provided for ``DATA_TYPE.LIST``. Hidden values also require a default. """ if self.data_type == DATA_TYPE.LIST: if not self.choices and not self.content_type: raise ValidationError("Either choices or content type is required for the list data type.") if self.is_hidden and not self.raw_default_value: raise ValidationError("A default is required for hidden attributes.")
[docs] def get_choices(self): """Get the valid choices for the attribute. :rtype: list """ choices = list() # noinspection PyUnresolvedReferences if self.content_type_id: # noinspection PyUnresolvedReferences ct = ContentType.objects.get_for_id(self.content_type_id) model = ct.model_class() if self.limit_choices_to: # noinspection PyTypeChecker criteria = json_loads(self.limit_choices_to) qs = model.objects.filter(**criteria) else: qs = model.objects.all() for row in qs: if hasattr(row, "get_choice_name") and callable(row.get_choice_name): label = row.get_choice_name() else: label = str(row) choices.append((row.pk, label)) else: # noinspection PyUnresolvedReferences a = self.choices.split("\n") choices = list() for i in a: choices.append((i.strip(), i.strip())) return choices
[docs] def get_default(self): """Get the default value based on ``data_type``. :rtype: int | float | str | None """ if not self.raw_default_value: return None if self.data_type == DATA_TYPE.BOOLEAN: # noinspection PyUnresolvedReferences value = self.raw_default_value.lower() if value in ("1", "true", "t", "yes", "y"): return True else: return False elif self.data_type == DATA_TYPE.FLOAT: # noinspection PyTypeChecker return float(self.raw_default_value) elif self.data_type == DATA_TYPE.INTEGER: # noinspection PyTypeChecker return int(self.raw_default_value) else: return self.raw_default_value
@property def has_choices(self): """Indicates whether choices have been defined for the value. :rtype: bool """ # noinspection PyTypeChecker,PyUnresolvedReferences if self.content_type_id: return True else: # noinspection PyTypeChecker return len(self.choices) > 1
[docs] def save(self, *args, **kwargs): """Automatically generate the ``name``.""" if not self.name: # noinspection PyTypeChecker self.name = unique_slugify(self, self.label).replace("-", "_") super().save(*args, **kwargs)
@property def value(self): """Always returns the value of ``get_default()``. Conforms to the API for :py:class:`ValueModel`.""" return self.get_default()
[docs]class ValueModel(models.Model): """Base class for specifying the value of an attribute. You *must* define the attribute foreign key as well as a foreign key to the model to which this value is added.: .. code-block:: python from superdjango.db.eav.models import AttributeModel, ValueModel class Profile(models.Model): # ... class ExtraAttribute(AttributeModel): # ... class ExtraAttributeValue(ValueModel): attribute = models.ForeignKey( ExtraAttribute, related_name="values" ) profile = models.ForeignKey( Profile, related_name="attributes" ) def get_attribute(self): return self.attribute """ raw_value = models.TextField( _("value"), blank=True, null=True ) class Meta: abstract = True def __str__(self): return self.raw_value @property def choices(self): """Alias for ``get_choices()``.""" return self.get_choices() @property def data_type(self): """Alias for the attribute's ``data_type``.""" return self.get_attribute().data_type @property def decimal_places(self): """Alias for the attribute's ``decimal_places``.""" return self.get_attribute().decimal_places
[docs] def display_value(self): """Display the human-friendly value the attribute. :rtype: float | int | str """ attribute = self.get_attribute() if attribute.data_type == DATA_TYPE.BOOLEAN: if bool(self.raw_value): return _("yes") else: return _("no") else: return self.raw_value
[docs] def get_attribute(self): """Get the attribute instance. :rtype: BaseType[AttributeModel] """ raise NotImplementedError()
[docs] def get_choices(self): """Alias for the attribute's ``get_choices()``.""" return self.get_attribute().get_choices()
[docs] def get_content_object(self): """Get the content object associated with the value when a content type is used.""" attribute = self.get_attribute() if not attribute.content_type_id: return None ct = ContentType.objects.get_for_id(attribute.content_type_id) model = ct.model_class() return model.objects.get(pk=self.raw_value)
[docs] def get_value(self): """Get the actual value of the attribute based on the ``data_type``.""" attribute = self.get_attribute() if attribute.data_type == DATA_TYPE.BOOLEAN: return bool(self.raw_value) elif attribute.data_type == DATA_TYPE.FLOAT: # noinspection PyTypeChecker return float(self.raw_value) elif attribute.data_type == DATA_TYPE.INTEGER: # noinspection PyTypeChecker return int(self.raw_value) else: return self.raw_value
@property def help_text(self): """Alias for the attribute's ``help_text``.""" return self.get_attribute().help_text @property def is_required(self): """Alias for the attribute's ``is_required``.""" return self.get_attribute().is_required @property def label(self): """Alias for the attribute's ``label``.""" return self.get_attribute().label @property def max_digits(self): """Alias for the attribute's ``max_digits``.""" return self.get_attribute().max_digits @property def max_length(self): """Alias for the attribute's ``max_length``.""" return self.get_attribute().max_length @property def min_length(self): """Alias for the attribute's ``min_length``.""" return self.get_attribute().min_length @property def name(self): """Alias for the attribute's ``name``.""" return self.get_attribute().name @property def sort_order(self): """Alias for the attribute's ``sort_order``.""" return self.get_attribute().sort_order @property def value(self): """Alias for ``get_value()``.""" return self.get_value()