mirror of
https://github.com/jazzband/django-eav2.git
synced 2026-04-25 01:14:43 +00:00
fix: error in migration files
This commit is contained in:
commit
2f1957f01a
14 changed files with 1041 additions and 72 deletions
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
|
|
|
|||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
|
@ -14,7 +14,7 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ repos:
|
|||
- id: mixed-line-ending
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.10.1
|
||||
rev: 23.12.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
# Generated by Django 4.2.6 on 2023-12-20 15:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('eav', '0010_dynamic_pk_type_for_models'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='attribute',
|
||||
name='id',
|
||||
field=models.CharField(
|
||||
editable=False, max_length=40, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='enumgroup',
|
||||
name='id',
|
||||
field=models.CharField(
|
||||
editable=False, max_length=40, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='enumvalue',
|
||||
name='id',
|
||||
field=models.CharField(
|
||||
editable=False, max_length=40, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='value',
|
||||
name='id',
|
||||
field=models.CharField(
|
||||
editable=False, max_length=40, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
]
|
||||
25
eav/models/__init__.py
Normal file
25
eav/models/__init__.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
"""
|
||||
This module defines the four concrete, non-abstract models:
|
||||
* :class:`Value`
|
||||
* :class:`Attribute`
|
||||
* :class:`EnumValue`
|
||||
* :class:`EnumGroup`.
|
||||
|
||||
Along with the :class:`Entity` helper class and :class:`EAVModelMeta`
|
||||
optional metaclass for each eav model class.
|
||||
"""
|
||||
|
||||
from .attribute import Attribute
|
||||
from .entity import EAVModelMeta, Entity
|
||||
from .enum_group import EnumGroup
|
||||
from .enum_value import EnumValue
|
||||
from .value import Value
|
||||
|
||||
__all__ = [
|
||||
"Attribute",
|
||||
"EnumGroup",
|
||||
"EnumValue",
|
||||
"Value",
|
||||
"Entity",
|
||||
"EAVModelMeta",
|
||||
]
|
||||
340
eav/models/attribute.py
Normal file
340
eav/models/attribute.py
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
# ruff: noqa: UP007
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, Tuple # noqa: UP035
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
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 EavDatatypeField
|
||||
from eav.logic.entity_pk import get_entity_pk_type
|
||||
from eav.logic.managers import AttributeManager
|
||||
from eav.logic.object_pk import get_pk_format
|
||||
from eav.logic.slug import SLUGFIELD_MAX_LENGTH, generate_slug
|
||||
from eav.settings import CHARFIELD_LENGTH
|
||||
from eav.validators import (
|
||||
validate_bool,
|
||||
validate_csv,
|
||||
validate_date,
|
||||
validate_enum,
|
||||
validate_float,
|
||||
validate_int,
|
||||
validate_json,
|
||||
validate_object,
|
||||
validate_text,
|
||||
)
|
||||
|
||||
from .enum_value import EnumValue
|
||||
from .value import Value
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .enum_group import EnumGroup
|
||||
|
||||
|
||||
class Attribute(models.Model):
|
||||
"""
|
||||
Putting the **A** in *EAV*. This holds the attributes, or concepts.
|
||||
Examples of possible *Attributes*: color, height, weight, number of
|
||||
children, number of patients, has fever?, etc...
|
||||
|
||||
Each attribute has a name, and a description, along with a slug that must
|
||||
be unique. If you don't provide a slug, a default slug (derived from
|
||||
name), will be created.
|
||||
|
||||
The *required* field is a boolean that indicates whether this EAV attribute
|
||||
is required for entities to which it applies. It defaults to *False*.
|
||||
|
||||
.. warning::
|
||||
Just like a normal model field that is required, you will not be able
|
||||
to save or create any entity object for which this attribute applies,
|
||||
without first setting this EAV attribute.
|
||||
|
||||
There are 7 possible values for datatype:
|
||||
|
||||
* int (TYPE_INT)
|
||||
* float (TYPE_FLOAT)
|
||||
* text (TYPE_TEXT)
|
||||
* date (TYPE_DATE)
|
||||
* bool (TYPE_BOOLEAN)
|
||||
* object (TYPE_OBJECT)
|
||||
* enum (TYPE_ENUM)
|
||||
* json (TYPE_JSON)
|
||||
* csv (TYPE_CSV)
|
||||
|
||||
|
||||
Examples::
|
||||
|
||||
Attribute.objects.create(name='Height', datatype=Attribute.TYPE_INT)
|
||||
# = <Attribute: Height (Integer)>
|
||||
|
||||
Attribute.objects.create(name='Color', datatype=Attribute.TYPE_TEXT)
|
||||
# = <Attribute: Color (Text)>
|
||||
|
||||
yes = EnumValue.objects.create(value='yes')
|
||||
no = EnumValue.objects.create(value='no')
|
||||
unknown = EnumValue.objects.create(value='unknown')
|
||||
ynu = EnumGroup.objects.create(name='Yes / No / Unknown')
|
||||
ynu.values.add(yes, no, unknown)
|
||||
|
||||
Attribute.objects.create(name='has fever?', datatype=Attribute.TYPE_ENUM, enum_group=ynu)
|
||||
# = <Attribute: has fever? (Multiple Choice)>
|
||||
|
||||
.. warning:: Once an Attribute has been used by an entity, you can not
|
||||
change it's datatype.
|
||||
"""
|
||||
|
||||
objects = AttributeManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name = _('Attribute')
|
||||
verbose_name_plural = _('Attributes')
|
||||
|
||||
TYPE_TEXT = 'text'
|
||||
TYPE_FLOAT = 'float'
|
||||
TYPE_INT = 'int'
|
||||
TYPE_DATE = 'date'
|
||||
TYPE_BOOLEAN = 'bool'
|
||||
TYPE_OBJECT = 'object'
|
||||
TYPE_ENUM = 'enum'
|
||||
TYPE_JSON = 'json'
|
||||
TYPE_CSV = 'csv'
|
||||
|
||||
DATATYPE_CHOICES = (
|
||||
(TYPE_TEXT, _('Text')),
|
||||
(TYPE_DATE, _('Date')),
|
||||
(TYPE_FLOAT, _('Float')),
|
||||
(TYPE_INT, _('Integer')),
|
||||
(TYPE_BOOLEAN, _('True / False')),
|
||||
(TYPE_OBJECT, _('Django Object')),
|
||||
(TYPE_ENUM, _('Multiple Choice')),
|
||||
(TYPE_JSON, _('JSON Object')),
|
||||
(TYPE_CSV, _('Comma-Separated-Value')),
|
||||
)
|
||||
|
||||
# Core attributes
|
||||
id = get_pk_format()
|
||||
|
||||
datatype = EavDatatypeField(
|
||||
choices=DATATYPE_CHOICES,
|
||||
max_length=6,
|
||||
verbose_name=_('Data Type'),
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=CHARFIELD_LENGTH,
|
||||
help_text=_('User-friendly attribute name'),
|
||||
verbose_name=_('Name'),
|
||||
)
|
||||
|
||||
"""
|
||||
Main identifer for the attribute.
|
||||
Upon creation, slug is autogenerated from the name.
|
||||
(see :meth:`~eav.fields.EavSlugField.create_slug_from_name`).
|
||||
"""
|
||||
slug = models.SlugField(
|
||||
max_length=SLUGFIELD_MAX_LENGTH,
|
||||
db_index=True,
|
||||
unique=True,
|
||||
help_text=_('Short unique attribute label'),
|
||||
verbose_name=_('Slug'),
|
||||
)
|
||||
|
||||
"""
|
||||
.. warning::
|
||||
This attribute should be used with caution. Setting this to *True*
|
||||
means that *all* entities that *can* have this attribute will
|
||||
be required to have a value for it.
|
||||
"""
|
||||
required = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Required'),
|
||||
)
|
||||
|
||||
entity_ct = models.ManyToManyField(
|
||||
ContentType,
|
||||
blank=True,
|
||||
verbose_name=_('Entity content type'),
|
||||
)
|
||||
"""
|
||||
This field allows you to specify a relationship with any number of content types.
|
||||
This would be useful, for example, if you wanted an attribute to apply only to
|
||||
a subset of entities. In that case, you could filter by content type in the
|
||||
:meth:`~eav.registry.EavConfig.get_attributes` method of that entity's config.
|
||||
"""
|
||||
|
||||
enum_group: "ForeignKey[Optional[EnumGroup]]" = ForeignKey(
|
||||
"eav.EnumGroup",
|
||||
on_delete=models.PROTECT,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Choice Group'),
|
||||
)
|
||||
|
||||
description = models.CharField(
|
||||
max_length=256,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('Short description'),
|
||||
verbose_name=_('Description'),
|
||||
)
|
||||
|
||||
# Useful meta-information
|
||||
|
||||
display_order = models.PositiveIntegerField(
|
||||
default=1,
|
||||
verbose_name=_('Display order'),
|
||||
)
|
||||
|
||||
modified = models.DateTimeField(
|
||||
auto_now=True,
|
||||
verbose_name=_('Modified'),
|
||||
)
|
||||
|
||||
created = models.DateTimeField(
|
||||
default=timezone.now,
|
||||
editable=False,
|
||||
verbose_name=_('Created'),
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.name} ({self.get_datatype_display()})'
|
||||
|
||||
def natural_key(self) -> Tuple[str, str]: # noqa: UP006
|
||||
"""
|
||||
Retrieve the natural key for the Attribute instance.
|
||||
|
||||
The natural key for an Attribute is defined by its `name` and `slug`. This method
|
||||
returns a tuple containing these two attributes of the instance.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple: A tuple containing the name and slug of the Attribute instance.
|
||||
"""
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
)
|
||||
|
||||
@property
|
||||
def help_text(self):
|
||||
return self.description
|
||||
|
||||
def get_validators(self):
|
||||
"""
|
||||
Returns the appropriate validator function from :mod:`~eav.validators`
|
||||
as a list (of length one) for the datatype.
|
||||
|
||||
.. note::
|
||||
The reason it returns it as a list, is eventually we may want this
|
||||
method to look elsewhere for additional attribute specific
|
||||
validators to return as well as the default, built-in one.
|
||||
"""
|
||||
DATATYPE_VALIDATORS = {
|
||||
'text': validate_text,
|
||||
'float': validate_float,
|
||||
'int': validate_int,
|
||||
'date': validate_date,
|
||||
'bool': validate_bool,
|
||||
'object': validate_object,
|
||||
'enum': validate_enum,
|
||||
'json': validate_json,
|
||||
'csv': validate_csv,
|
||||
}
|
||||
|
||||
return [DATATYPE_VALIDATORS[self.datatype]]
|
||||
|
||||
def validate_value(self, value):
|
||||
"""
|
||||
Check *value* against the validators returned by
|
||||
:meth:`get_validators` for this attribute.
|
||||
"""
|
||||
for validator in self.get_validators():
|
||||
validator(value)
|
||||
|
||||
if self.datatype == self.TYPE_ENUM:
|
||||
if isinstance(value, EnumValue):
|
||||
value = value.value
|
||||
if not self.enum_group.values.filter(value=value).exists():
|
||||
raise ValidationError(
|
||||
_('%(val)s is not a valid choice for %(attr)s')
|
||||
% {'val': value, 'attr': self},
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Saves the Attribute and auto-generates a slug field
|
||||
if one wasn't provided.
|
||||
"""
|
||||
if not self.slug:
|
||||
self.slug = generate_slug(self.name)
|
||||
|
||||
self.full_clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Validates the attribute. Will raise ``ValidationError`` if the
|
||||
attribute's datatype is *TYPE_ENUM* and enum_group is not set, or if
|
||||
the attribute is not *TYPE_ENUM* and the enum group is set.
|
||||
"""
|
||||
if self.datatype == self.TYPE_ENUM and not self.enum_group:
|
||||
raise ValidationError(
|
||||
_('You must set the choice group for multiple choice attributes'),
|
||||
)
|
||||
|
||||
if self.datatype != self.TYPE_ENUM and self.enum_group:
|
||||
raise ValidationError(
|
||||
_('You can only assign a choice group to multiple choice attributes'),
|
||||
)
|
||||
|
||||
def get_choices(self):
|
||||
"""
|
||||
Returns a query set of :class:`EnumValue` objects for this attribute.
|
||||
Returns None if the datatype of this attribute is not *TYPE_ENUM*.
|
||||
"""
|
||||
return (
|
||||
self.enum_group.values.all()
|
||||
if self.datatype == Attribute.TYPE_ENUM
|
||||
else None
|
||||
)
|
||||
|
||||
def save_value(self, entity, value):
|
||||
"""
|
||||
Called with *entity*, any Django object registered with eav, and
|
||||
*value*, the :class:`Value` this attribute for *entity* should
|
||||
be set to.
|
||||
|
||||
If a :class:`Value` object for this *entity* and attribute doesn't
|
||||
exist, one will be created.
|
||||
|
||||
.. note::
|
||||
If *value* is None and a :class:`Value` object exists for this
|
||||
Attribute and *entity*, it will delete that :class:`Value` object.
|
||||
"""
|
||||
ct = ContentType.objects.get_for_model(entity)
|
||||
|
||||
entity_filter = {
|
||||
'entity_ct': ct,
|
||||
'attribute': self,
|
||||
f'{get_entity_pk_type(entity)}': entity.pk,
|
||||
}
|
||||
|
||||
try:
|
||||
value_obj = self.value_set.get(**entity_filter)
|
||||
except Value.DoesNotExist:
|
||||
if value is None or value == '':
|
||||
return
|
||||
|
||||
value_obj = Value.objects.create(**entity_filter)
|
||||
|
||||
if value is None or value == '':
|
||||
value_obj.delete()
|
||||
return
|
||||
|
||||
if value != value_obj.value:
|
||||
value_obj.value = value
|
||||
value_obj.save()
|
||||
207
eav/models/entity.py
Normal file
207
eav/models/entity.py
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
from copy import copy
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models.base import ModelBase
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from eav import register
|
||||
from eav.exceptions import IllegalAssignmentException
|
||||
from eav.logic.entity_pk import get_entity_pk_type
|
||||
|
||||
from .attribute import Attribute
|
||||
from .enum_value import EnumValue
|
||||
from .value import Value
|
||||
|
||||
|
||||
class Entity:
|
||||
"""Helper class that will be attached to entities registered with eav."""
|
||||
|
||||
@staticmethod
|
||||
def pre_save_handler(sender, *args, **kwargs):
|
||||
"""
|
||||
Pre save handler attached to self.instance. Called before the
|
||||
model instance we are attached to is saved. This allows us to call
|
||||
:meth:`validate_attributes` before the entity is saved.
|
||||
"""
|
||||
instance = kwargs['instance']
|
||||
entity = getattr(kwargs['instance'], instance._eav_config_cls.eav_attr)
|
||||
entity.validate_attributes()
|
||||
|
||||
@staticmethod
|
||||
def post_save_handler(sender, *args, **kwargs):
|
||||
"""
|
||||
Post save handler attached to self.instance. Calls :meth:`save` when
|
||||
the model instance we are attached to is saved.
|
||||
"""
|
||||
instance = kwargs['instance']
|
||||
entity = getattr(instance, instance._eav_config_cls.eav_attr)
|
||||
entity.save()
|
||||
|
||||
def __init__(self, instance) -> None:
|
||||
"""
|
||||
Set self.instance equal to the instance of the model that we're attached
|
||||
to. Also, store the content type of that instance.
|
||||
"""
|
||||
self.instance = instance
|
||||
self.ct = ContentType.objects.get_for_model(instance)
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""
|
||||
Tha magic getattr helper. This is called whenever user invokes::
|
||||
|
||||
instance.<attribute>
|
||||
|
||||
Checks if *name* is a valid slug for attributes available to this
|
||||
instances. If it is, tries to lookup the :class:`Value` with that
|
||||
attribute slug. If there is one, it returns the value of the
|
||||
class:`Value` object, otherwise it hasn't been set, so it returns
|
||||
None.
|
||||
"""
|
||||
if not name.startswith('_'):
|
||||
try:
|
||||
attribute = self.get_attribute_by_slug(name)
|
||||
except Attribute.DoesNotExist:
|
||||
raise AttributeError(
|
||||
_('%(obj)s has no EAV attribute named %(attr)s')
|
||||
% {'obj': self.instance, 'attr': name},
|
||||
)
|
||||
|
||||
try:
|
||||
return self.get_value_by_attribute(attribute).value
|
||||
except Value.DoesNotExist:
|
||||
return None
|
||||
|
||||
return getattr(super(), name)
|
||||
|
||||
def get_all_attributes(self):
|
||||
"""
|
||||
Return a query set of all :class:`Attribute` objects that can be set
|
||||
for this entity.
|
||||
"""
|
||||
return self.instance._eav_config_cls.get_attributes(
|
||||
instance=self.instance,
|
||||
).order_by('display_order')
|
||||
|
||||
def _hasattr(self, attribute_slug):
|
||||
"""
|
||||
Since we override __getattr__ with a backdown to the database, this
|
||||
exists as a way of checking whether a user has set a real attribute on
|
||||
ourselves, without going to the db if not.
|
||||
"""
|
||||
return attribute_slug in self.__dict__
|
||||
|
||||
def _getattr(self, attribute_slug):
|
||||
"""
|
||||
Since we override __getattr__ with a backdown to the database, this
|
||||
exists as a way of getting the value a user set for one of our
|
||||
attributes, without going to the db to check.
|
||||
"""
|
||||
return self.__dict__[attribute_slug]
|
||||
|
||||
def save(self):
|
||||
"""Saves all the EAV values that have been set on this entity."""
|
||||
for attribute in self.get_all_attributes():
|
||||
if self._hasattr(attribute.slug):
|
||||
attribute_value = self._getattr(attribute.slug)
|
||||
if (
|
||||
attribute.datatype == Attribute.TYPE_ENUM
|
||||
and not isinstance(
|
||||
attribute_value,
|
||||
EnumValue,
|
||||
)
|
||||
and attribute_value is not None
|
||||
):
|
||||
attribute_value = EnumValue.objects.get(value=attribute_value)
|
||||
attribute.save_value(self.instance, attribute_value)
|
||||
|
||||
def validate_attributes(self):
|
||||
"""
|
||||
Called before :meth:`save`, first validate all the entity values to
|
||||
make sure they can be created / saved cleanly.
|
||||
Raises ``ValidationError`` if they can't be.
|
||||
"""
|
||||
values_dict = self.get_values_dict()
|
||||
|
||||
for attribute in self.get_all_attributes():
|
||||
value = None
|
||||
|
||||
# Value was assigned to this instance.
|
||||
if self._hasattr(attribute.slug):
|
||||
value = self._getattr(attribute.slug)
|
||||
values_dict.pop(attribute.slug, None)
|
||||
# Otherwise try pre-loaded from DB.
|
||||
else:
|
||||
value = values_dict.pop(attribute.slug, None)
|
||||
|
||||
if value is None:
|
||||
if attribute.required:
|
||||
raise ValidationError(
|
||||
_(f'{attribute.slug} EAV field cannot be blank'),
|
||||
)
|
||||
else:
|
||||
try:
|
||||
attribute.validate_value(value)
|
||||
except ValidationError as e:
|
||||
raise ValidationError(
|
||||
_('%(attr)s EAV field %(err)s')
|
||||
% {'attr': attribute.slug, 'err': e},
|
||||
)
|
||||
|
||||
illegal = values_dict or (
|
||||
self.get_object_attributes() - self.get_all_attribute_slugs()
|
||||
)
|
||||
|
||||
if illegal:
|
||||
raise IllegalAssignmentException(
|
||||
'Instance of the class {} cannot have values for attributes: {}.'.format(
|
||||
self.instance.__class__,
|
||||
', '.join(illegal),
|
||||
),
|
||||
)
|
||||
|
||||
def get_values_dict(self):
|
||||
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."""
|
||||
entity_filter = {
|
||||
'entity_ct': self.ct,
|
||||
f'{get_entity_pk_type(self.instance)}': self.instance.pk,
|
||||
}
|
||||
|
||||
return Value.objects.filter(**entity_filter).select_related()
|
||||
|
||||
def get_all_attribute_slugs(self):
|
||||
"""Returns a list of slugs for all attributes available to this entity."""
|
||||
return set(self.get_all_attributes().values_list('slug', flat=True))
|
||||
|
||||
def get_attribute_by_slug(self, slug):
|
||||
"""Returns a single :class:`Attribute` with *slug*."""
|
||||
return self.get_all_attributes().get(slug=slug)
|
||||
|
||||
def get_value_by_attribute(self, attribute):
|
||||
"""Returns a single :class:`Value` for *attribute*."""
|
||||
return self.get_values().get(attribute=attribute)
|
||||
|
||||
def get_object_attributes(self):
|
||||
"""
|
||||
Returns entity instance attributes, except for
|
||||
``instance`` and ``ct`` which are used internally.
|
||||
"""
|
||||
return set(copy(self.__dict__).keys()) - {'instance', 'ct'}
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Iterate over set eav values. This would allow you to do::
|
||||
|
||||
for i in m.eav: print(i)
|
||||
"""
|
||||
return iter(self.get_values())
|
||||
|
||||
|
||||
class EAVModelMeta(ModelBase):
|
||||
def __new__(cls, name, bases, namespace, **kwds):
|
||||
result = super().__new__(cls, name, bases, dict(namespace))
|
||||
register(result)
|
||||
return result
|
||||
61
eav/models/enum_group.py
Normal file
61
eav/models/enum_group.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
from typing import TYPE_CHECKING, Any, Tuple
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import ManyToManyField
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from eav.logic.managers import EnumGroupManager
|
||||
from eav.logic.object_pk import get_pk_format
|
||||
from eav.settings import CHARFIELD_LENGTH
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .enum_value import EnumValue
|
||||
|
||||
|
||||
class EnumGroup(models.Model):
|
||||
"""
|
||||
*EnumGroup* objects have two fields - a *name* ``CharField`` and *values*,
|
||||
a ``ManyToManyField`` to :class:`EnumValue`. :class:`Attribute` classes
|
||||
with datatype *TYPE_ENUM* have a ``ForeignKey`` field to *EnumGroup*.
|
||||
|
||||
See :class:`EnumValue` for an example.
|
||||
"""
|
||||
|
||||
objects = EnumGroupManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('EnumGroup')
|
||||
verbose_name_plural = _('EnumGroups')
|
||||
|
||||
id = get_pk_format()
|
||||
|
||||
name = models.CharField(
|
||||
unique=True,
|
||||
max_length=CHARFIELD_LENGTH,
|
||||
verbose_name=_('Name'),
|
||||
)
|
||||
values: "ManyToManyField[EnumValue, Any]" = ManyToManyField(
|
||||
"eav.EnumValue",
|
||||
verbose_name=_('Enum group'),
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of `EnumGroup` instance."""
|
||||
return str(self.name)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of `EnumGroup` object."""
|
||||
return f'<EnumGroup {self.name}>'
|
||||
|
||||
def natural_key(self) -> Tuple[str]:
|
||||
"""
|
||||
Retrieve the natural key for the EnumGroup instance.
|
||||
|
||||
The natural key for an EnumGroup is defined by its `name`. This method
|
||||
returns the name of the instance as a single-element tuple.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple: A tuple containing the name of the EnumGroup instance.
|
||||
"""
|
||||
return (self.name,)
|
||||
74
eav/models/enum_value.py
Normal file
74
eav/models/enum_value.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
from typing import Tuple
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from eav.logic.managers import EnumValueManager
|
||||
from eav.logic.object_pk import get_pk_format
|
||||
from eav.logic.slug import SLUGFIELD_MAX_LENGTH
|
||||
|
||||
|
||||
class EnumValue(models.Model):
|
||||
"""
|
||||
*EnumValue* objects are the value 'choices' to multiple choice *TYPE_ENUM*
|
||||
:class:`Attribute` objects. They have only one field, *value*, a
|
||||
``CharField`` that must be unique.
|
||||
|
||||
For example::
|
||||
|
||||
yes = EnumValue.objects.create(value='Yes') # doctest: SKIP
|
||||
no = EnumValue.objects.create(value='No')
|
||||
unknown = EnumValue.objects.create(value='Unknown')
|
||||
|
||||
ynu = EnumGroup.objects.create(name='Yes / No / Unknown')
|
||||
ynu.values.add(yes, no, unknown)
|
||||
|
||||
Attribute.objects.create(name='has fever?',
|
||||
datatype=Attribute.TYPE_ENUM, enum_group=ynu)
|
||||
# = <Attribute: has fever? (Multiple Choice)>
|
||||
|
||||
.. note::
|
||||
The same *EnumValue* objects should be reused within multiple
|
||||
*EnumGroups*. For example, if you have one *EnumGroup* called: *Yes /
|
||||
No / Unknown* and another called *Yes / No / Not applicable*, you should
|
||||
only have a total of four *EnumValues* objects, as you should have used
|
||||
the same *Yes* and *No* *EnumValues* for both *EnumGroups*.
|
||||
"""
|
||||
|
||||
objects = EnumValueManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('EnumValue')
|
||||
verbose_name_plural = _('EnumValues')
|
||||
|
||||
id = get_pk_format()
|
||||
|
||||
value = models.CharField(
|
||||
_('Value'),
|
||||
db_index=True,
|
||||
unique=True,
|
||||
max_length=SLUGFIELD_MAX_LENGTH,
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of `EnumValue` instance."""
|
||||
return str(
|
||||
self.value,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of `EnumValue` object."""
|
||||
return f'<EnumValue {self.value}>'
|
||||
|
||||
def natural_key(self) -> Tuple[str]:
|
||||
"""
|
||||
Retrieve the natural key for the EnumValue instance.
|
||||
|
||||
The natural key for an EnumValue is defined by its `value`. This method returns
|
||||
the value of the instance as a single-element tuple.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple: A tuple containing the value of the EnumValue instance.
|
||||
"""
|
||||
return (self.value,)
|
||||
213
eav/models/value.py
Normal file
213
eav/models/value.py
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
# ruff: noqa: UP007
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
|
||||
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)
|
||||
7
eav/settings.py
Normal file
7
eav/settings.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
try:
|
||||
from typing import Final
|
||||
except ImportError:
|
||||
from typing_extensions import Final # noqa: UP035
|
||||
|
||||
|
||||
CHARFIELD_LENGTH: Final = 100
|
||||
134
poetry.lock
generated
134
poetry.lock
generated
|
|
@ -1,4 +1,4 @@
|
|||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "alabaster"
|
||||
|
|
@ -424,13 +424,13 @@ tests = ["check-manifest (>=0.42)", "mock (>=1.3.0)", "pytest (==5.4.3)", "pytes
|
|||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "4.2.7"
|
||||
version = "4.2.8"
|
||||
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "Django-4.2.7-py3-none-any.whl", hash = "sha256:e1d37c51ad26186de355cbcec16613ebdabfa9689bbade9c538835205a8abbe9"},
|
||||
{file = "Django-4.2.7.tar.gz", hash = "sha256:8e0f1c2c2786b5c0e39fe1afce24c926040fad47c8ea8ad30aaf1188df29fc41"},
|
||||
{file = "Django-4.2.8-py3-none-any.whl", hash = "sha256:6cb5dcea9e3d12c47834d32156b8841f533a4493c688e2718cafd51aa430ba6d"},
|
||||
{file = "Django-4.2.8.tar.gz", hash = "sha256:d69d5e36cc5d9f4eb4872be36c622878afcdce94062716cf3e25bcedcb168b62"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -445,30 +445,31 @@ bcrypt = ["bcrypt"]
|
|||
|
||||
[[package]]
|
||||
name = "doc8"
|
||||
version = "0.11.2"
|
||||
version = "1.1.1"
|
||||
description = "Style checker for Sphinx (or other) RST documentation"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "doc8-0.11.2-py3-none-any.whl", hash = "sha256:9187da8c9f115254bbe34f74e2bbbdd3eaa1b9e92efd19ccac7461e347b5055c"},
|
||||
{file = "doc8-0.11.2.tar.gz", hash = "sha256:c35a231f88f15c204659154ed3d499fa4d402d7e63d41cba7b54cf5e646123ab"},
|
||||
{file = "doc8-1.1.1-py3-none-any.whl", hash = "sha256:e493aa3f36820197c49f407583521bb76a0fde4fffbcd0e092be946ff95931ac"},
|
||||
{file = "doc8-1.1.1.tar.gz", hash = "sha256:d97a93e8f5a2efc4713a0804657dedad83745cca4cd1d88de9186f77f9776004"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
docutils = "*"
|
||||
docutils = ">=0.19,<0.21"
|
||||
Pygments = "*"
|
||||
restructuredtext-lint = ">=0.7"
|
||||
stevedore = "*"
|
||||
tomli = {version = "*", markers = "python_version < \"3.11\""}
|
||||
|
||||
[[package]]
|
||||
name = "docutils"
|
||||
version = "0.18.1"
|
||||
version = "0.20.1"
|
||||
description = "Docutils -- Python Documentation Utilities"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"},
|
||||
{file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"},
|
||||
{file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"},
|
||||
{file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -827,17 +828,17 @@ test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mypy", "pre-commit"
|
|||
|
||||
[[package]]
|
||||
name = "hypothesis"
|
||||
version = "6.88.3"
|
||||
version = "6.92.1"
|
||||
description = "A library for property-based testing"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "hypothesis-6.88.3-py3-none-any.whl", hash = "sha256:781ce6fd35e11ca77ad132a20cebe66fd215f56678f8efd6b87013b14500151b"},
|
||||
{file = "hypothesis-6.88.3.tar.gz", hash = "sha256:5cfda253e34726c98ab04b9595fca15677ee9f4f6055146aea25a6278d71f6f1"},
|
||||
{file = "hypothesis-6.92.1-py3-none-any.whl", hash = "sha256:3cba76a7389bd7245c350fcf7234663314dc81a5be0bbef72a07d8c249bfc210"},
|
||||
{file = "hypothesis-6.92.1.tar.gz", hash = "sha256:fa755ded526e50b7e2f642cdc5d64519f88d4e4ee71d9d29ec3eb2f2fddf1274"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
attrs = ">=19.2.0"
|
||||
attrs = ">=22.2.0"
|
||||
exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||
sortedcontainers = ">=2.1.0,<3.0.0"
|
||||
|
||||
|
|
@ -1006,17 +1007,17 @@ dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils
|
|||
|
||||
[[package]]
|
||||
name = "m2r2"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3.post2"
|
||||
description = "Markdown and reStructuredText in a single file."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "m2r2-0.3.2-py3-none-any.whl", hash = "sha256:d3684086b61b4bebe2307f15189495360f05a123c9bda2a66462649b7ca236aa"},
|
||||
{file = "m2r2-0.3.2.tar.gz", hash = "sha256:ccd95b052dcd1ac7442ecb3111262b2001c10e4119b459c34c93ac7a5c2c7868"},
|
||||
{file = "m2r2-0.3.3.post2-py3-none-any.whl", hash = "sha256:86157721eb6eabcd54d4eea7195890cc58fa6188b8d0abea633383cfbb5e11e3"},
|
||||
{file = "m2r2-0.3.3.post2.tar.gz", hash = "sha256:e62bcb0e74b3ce19cda0737a0556b04cf4a43b785072fcef474558f2c1482ca8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
docutils = "*"
|
||||
docutils = ">=0.19"
|
||||
mistune = "0.8.4"
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1146,38 +1147,38 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.6.1"
|
||||
version = "1.7.1"
|
||||
description = "Optional static typing for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mypy-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c"},
|
||||
{file = "mypy-1.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb"},
|
||||
{file = "mypy-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e"},
|
||||
{file = "mypy-1.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f"},
|
||||
{file = "mypy-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c"},
|
||||
{file = "mypy-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5"},
|
||||
{file = "mypy-1.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245"},
|
||||
{file = "mypy-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183"},
|
||||
{file = "mypy-1.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0"},
|
||||
{file = "mypy-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7"},
|
||||
{file = "mypy-1.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f"},
|
||||
{file = "mypy-1.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660"},
|
||||
{file = "mypy-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7"},
|
||||
{file = "mypy-1.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71"},
|
||||
{file = "mypy-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a"},
|
||||
{file = "mypy-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169"},
|
||||
{file = "mypy-1.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143"},
|
||||
{file = "mypy-1.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46"},
|
||||
{file = "mypy-1.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85"},
|
||||
{file = "mypy-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45"},
|
||||
{file = "mypy-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208"},
|
||||
{file = "mypy-1.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd"},
|
||||
{file = "mypy-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332"},
|
||||
{file = "mypy-1.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f"},
|
||||
{file = "mypy-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30"},
|
||||
{file = "mypy-1.6.1-py3-none-any.whl", hash = "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1"},
|
||||
{file = "mypy-1.6.1.tar.gz", hash = "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1"},
|
||||
{file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"},
|
||||
{file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"},
|
||||
{file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"},
|
||||
{file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"},
|
||||
{file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"},
|
||||
{file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"},
|
||||
{file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"},
|
||||
{file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"},
|
||||
{file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"},
|
||||
{file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"},
|
||||
{file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"},
|
||||
{file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"},
|
||||
{file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"},
|
||||
{file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"},
|
||||
{file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"},
|
||||
{file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"},
|
||||
{file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"},
|
||||
{file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"},
|
||||
{file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"},
|
||||
{file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"},
|
||||
{file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"},
|
||||
{file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"},
|
||||
{file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"},
|
||||
{file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"},
|
||||
{file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"},
|
||||
{file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"},
|
||||
{file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -1188,6 +1189,7 @@ typing-extensions = ">=4.1.0"
|
|||
[package.extras]
|
||||
dmypy = ["psutil (>=4.0)"]
|
||||
install-types = ["pip"]
|
||||
mypyc = ["setuptools (>=50)"]
|
||||
reports = ["lxml"]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1447,13 +1449,13 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale
|
|||
|
||||
[[package]]
|
||||
name = "pytest-django"
|
||||
version = "4.6.0"
|
||||
version = "4.7.0"
|
||||
description = "A Django plugin for pytest."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-django-4.6.0.tar.gz", hash = "sha256:ebc12a64f822a1284a281caf434d693f96bff69a9b09c677f538ecaa2f470b37"},
|
||||
{file = "pytest_django-4.6.0-py3-none-any.whl", hash = "sha256:7e90a183dec8c715714864e5dc8da99bb219502d437a9769a3c9e524af57e43a"},
|
||||
{file = "pytest-django-4.7.0.tar.gz", hash = "sha256:92d6fd46b1d79b54fb6b060bbb39428073396cec717d5f2e122a990d4b6aa5e8"},
|
||||
{file = "pytest_django-4.7.0-py3-none-any.whl", hash = "sha256:4e1c79d5261ade2dd58d91208017cd8f62cb4710b56e012ecd361d15d5d662a2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -1803,13 +1805,13 @@ test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"]
|
|||
|
||||
[[package]]
|
||||
name = "sphinx-autodoc-typehints"
|
||||
version = "1.24.1"
|
||||
version = "1.25.2"
|
||||
description = "Type hints (PEP 484) support for the Sphinx autodoc extension"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "sphinx_autodoc_typehints-1.24.1-py3-none-any.whl", hash = "sha256:4cc16c5545f2bf896ca52a854babefe3d8baeaaa033d13a7f179ac1d9feb02d5"},
|
||||
{file = "sphinx_autodoc_typehints-1.24.1.tar.gz", hash = "sha256:06683a2b76c3c7b1931b75e40e0211866fbb50ba4c4e802d0901d9b4e849add2"},
|
||||
{file = "sphinx_autodoc_typehints-1.25.2-py3-none-any.whl", hash = "sha256:5ed05017d23ad4b937eab3bee9fae9ab0dd63f0b42aa360031f1fad47e47f673"},
|
||||
{file = "sphinx_autodoc_typehints-1.25.2.tar.gz", hash = "sha256:3cabc2537e17989b2f92e64a399425c4c8bf561ed73f087bc7414a5003616a50"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -1822,18 +1824,18 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pyt
|
|||
|
||||
[[package]]
|
||||
name = "sphinx-rtd-theme"
|
||||
version = "1.3.0"
|
||||
version = "2.0.0"
|
||||
description = "Read the Docs theme for Sphinx"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0"},
|
||||
{file = "sphinx_rtd_theme-1.3.0.tar.gz", hash = "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931"},
|
||||
{file = "sphinx_rtd_theme-2.0.0-py2.py3-none-any.whl", hash = "sha256:ec93d0856dc280cf3aee9a4c9807c60e027c7f7b461b77aeffed682e68f0e586"},
|
||||
{file = "sphinx_rtd_theme-2.0.0.tar.gz", hash = "sha256:bd5d7b80622406762073a04ef8fadc5f9151261563d47027de09910ce03afe6b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
docutils = "<0.19"
|
||||
sphinx = ">=1.6,<8"
|
||||
docutils = "<0.21"
|
||||
sphinx = ">=5,<8"
|
||||
sphinxcontrib-jquery = ">=4,<5"
|
||||
|
||||
[package.extras]
|
||||
|
|
@ -2023,13 +2025,13 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "tomlkit"
|
||||
version = "0.12.2"
|
||||
version = "0.12.3"
|
||||
description = "Style preserving TOML library"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tomlkit-0.12.2-py3-none-any.whl", hash = "sha256:eeea7ac7563faeab0a1ed8fe12c2e5a51c61f933f2502f7e9db0241a65163ad0"},
|
||||
{file = "tomlkit-0.12.2.tar.gz", hash = "sha256:df32fab589a81f0d7dc525a4267b6d7a64ee99619cbd1eeb0fae32c1dd426977"},
|
||||
{file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"},
|
||||
{file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2148,4 +2150,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "8b5aebd16a0dc06acbcafc6bb95be6ed9d5866cc266924280cd9fe5a8f0fa0cc"
|
||||
content-hash = "032fb6720d5f7983f711f8bfaaa1360e6dea80f224372ffd7105327f7b564736"
|
||||
|
|
|
|||
|
|
@ -88,14 +88,14 @@ pytest-randomly = "^3.15"
|
|||
pytest-django = "^4.5.2"
|
||||
hypothesis = "^6.87.1"
|
||||
|
||||
doc8 = "^0.11.2"
|
||||
doc8 = ">=0.11.2,<1.2.0"
|
||||
|
||||
[tool.poetry.group.docs]
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
sphinx = ">=5.0,<8.0"
|
||||
sphinx-rtd-theme = "^1.3.0"
|
||||
sphinx-rtd-theme = ">=1.3,<3.0"
|
||||
sphinx-autodoc-typehints = "^1.19.5"
|
||||
m2r2 = "^0.3"
|
||||
tomlkit = ">=0.11,<0.13"
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ DATABASES = {
|
|||
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
EAV2_PRIMARY_KEY_FIELD = 'django.db.models.AutoField'
|
||||
EAV2_PRIMARY_KEY_FIELD = 'django.db.models.CharField'
|
||||
|
||||
|
||||
# Password validation
|
||||
|
|
|
|||
Loading…
Reference in a new issue