mirror of
https://github.com/jazzband/django-eav2.git
synced 2026-03-16 14:30:24 +00:00
refactor: apply ruff linter rules and standardize code style
Replace flake8 with ruff and apply consistent linting rules across the entire codebase. Update type annotations, quotation marks, and other style-related changes to comply with the new standards.
This commit is contained in:
parent
5e1a7d2803
commit
b5b576aca5
31 changed files with 894 additions and 787 deletions
|
|
@ -2,30 +2,35 @@
|
|||
#
|
||||
# More information on the configuration options is available at:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict
|
||||
from pathlib import Path
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
from sphinx.ext.autodoc import between
|
||||
|
||||
sys.path.insert(0, os.path.abspath('.'))
|
||||
sys.path.insert(0, os.path.abspath('../../'))
|
||||
# For discovery of Python modules
|
||||
sys.path.insert(0, str(Path().cwd()))
|
||||
|
||||
# For finding the django_settings.py file
|
||||
sys.path.insert(0, str(Path("../../").resolve()))
|
||||
|
||||
|
||||
# Pass settings into configure.
|
||||
settings.configure(
|
||||
INSTALLED_APPS=[
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'eav',
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"eav",
|
||||
],
|
||||
SECRET_KEY=os.environ.get('DJANGO_SECRET_KEY', 'this-is-not-s3cur3'),
|
||||
SECRET_KEY=os.environ.get("DJANGO_SECRET_KEY", "this-is-not-s3cur3"),
|
||||
EAV2_PRIMARY_KEY_FIELD="django.db.models.BigAutoField",
|
||||
)
|
||||
|
||||
|
|
@ -34,22 +39,22 @@ django.setup()
|
|||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'Django EAV 2'
|
||||
copyright = '2018, Iwo Herka and team at MAKIMO'
|
||||
author = '-'
|
||||
project = "Django EAV 2"
|
||||
copyright = "2018, Iwo Herka and team at MAKIMO" # noqa: A001
|
||||
author = "-"
|
||||
|
||||
# The short X.Y version
|
||||
version = ''
|
||||
version = ""
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = '0.10.0'
|
||||
release = "0.10.0"
|
||||
|
||||
|
||||
def setup(app):
|
||||
"""Use the configuration file itself as an extension."""
|
||||
app.connect(
|
||||
'autodoc-process-docstring',
|
||||
"autodoc-process-docstring",
|
||||
between(
|
||||
'^.*IGNORE.*$',
|
||||
"^.*IGNORE.*$",
|
||||
exclude=True,
|
||||
),
|
||||
)
|
||||
|
|
@ -59,57 +64,57 @@ def setup(app):
|
|||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
extensions = [
|
||||
'sphinx.ext.napoleon',
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx_rtd_theme',
|
||||
"sphinx.ext.napoleon",
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.coverage",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx_rtd_theme",
|
||||
]
|
||||
|
||||
templates_path = ['_templates']
|
||||
templates_path = ["_templates"]
|
||||
|
||||
source_suffix = '.rst'
|
||||
source_suffix = ".rst"
|
||||
|
||||
master_doc = 'index'
|
||||
master_doc = "index"
|
||||
|
||||
language = 'en'
|
||||
language = "en"
|
||||
|
||||
exclude_patterns = ['build']
|
||||
exclude_patterns = ["build"]
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
pygments_style = "sphinx"
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
|
||||
html_static_path = ['_static']
|
||||
html_static_path = ["_static"]
|
||||
|
||||
html_sidebars = {
|
||||
'index': ['sidebarintro.html', 'localtoc.html'],
|
||||
'**': [
|
||||
'sidebarintro.html',
|
||||
'localtoc.html',
|
||||
'relations.html',
|
||||
'searchbox.html',
|
||||
"index": ["sidebarintro.html", "localtoc.html"],
|
||||
"**": [
|
||||
"sidebarintro.html",
|
||||
"localtoc.html",
|
||||
"relations.html",
|
||||
"searchbox.html",
|
||||
],
|
||||
}
|
||||
|
||||
htmlhelp_basename = 'DjangoEAV2doc'
|
||||
htmlhelp_basename = "DjangoEAV2doc"
|
||||
|
||||
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
|
||||
latex_elements: Dict[str, str] = {}
|
||||
latex_elements: dict[str, str] = {}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'DjangoEAV2.tex', 'Django EAV 2 Documentation', '-', 'manual'),
|
||||
(master_doc, "DjangoEAV2.tex", "Django EAV 2 Documentation", "-", "manual"),
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -120,8 +125,8 @@ latex_documents = [
|
|||
man_pages = [
|
||||
(
|
||||
master_doc,
|
||||
'djangoeav2',
|
||||
'Django EAV 2 Documentation',
|
||||
"djangoeav2",
|
||||
"Django EAV 2 Documentation",
|
||||
[author],
|
||||
1,
|
||||
),
|
||||
|
|
@ -136,12 +141,12 @@ man_pages = [
|
|||
texinfo_documents = [
|
||||
(
|
||||
master_doc,
|
||||
'DjangoEAV2',
|
||||
'Django EAV 2 Documentation',
|
||||
"DjangoEAV2",
|
||||
"Django EAV 2 Documentation",
|
||||
author,
|
||||
'DjangoEAV2',
|
||||
'One line description of project.',
|
||||
'Miscellaneous',
|
||||
"DjangoEAV2",
|
||||
"One line description of project.",
|
||||
"Miscellaneous",
|
||||
),
|
||||
]
|
||||
|
||||
|
|
@ -150,7 +155,7 @@ texinfo_documents = [
|
|||
# -- Options for intersphinx extension ---------------------------------------
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
|
||||
intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
|
||||
|
||||
# -- Autodoc configuration ---------------------------------------------------
|
||||
|
||||
|
|
|
|||
46
eav/admin.py
46
eav/admin.py
|
|
@ -1,6 +1,8 @@
|
|||
"""This module contains classes used for admin integration."""
|
||||
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, ClassVar, Dict, List, Sequence, Union
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.options import InlineModelAdmin, ModelAdmin
|
||||
|
|
@ -11,6 +13,8 @@ from eav.models import Attribute, EnumGroup, EnumValue, Value
|
|||
|
||||
_FIELDSET_TYPE = List[Union[str, Dict[str, Any]]] # type: ignore[misc]
|
||||
|
||||
some_attribute = ClassVar[dict[str, str]]
|
||||
|
||||
|
||||
class BaseEntityAdmin(ModelAdmin):
|
||||
"""Custom admin model to support dynamic EAV fieldsets.
|
||||
|
|
@ -26,7 +30,7 @@ class BaseEntityAdmin(ModelAdmin):
|
|||
"""
|
||||
|
||||
eav_fieldset_title: str = "EAV Attributes"
|
||||
eav_fieldset_description: Optional[str] = None
|
||||
eav_fieldset_description: str | None = None
|
||||
|
||||
def render_change_form(self, request, context, *args, **kwargs):
|
||||
"""Dynamically modifies the admin form to include EAV fields.
|
||||
|
|
@ -45,7 +49,7 @@ class BaseEntityAdmin(ModelAdmin):
|
|||
Returns:
|
||||
HttpResponse object representing the rendered change form.
|
||||
"""
|
||||
form = context['adminform'].form
|
||||
form = context["adminform"].form
|
||||
|
||||
# Identify EAV fields based on the form instance's configuration.
|
||||
eav_fields = self._get_eav_fields(form.instance)
|
||||
|
|
@ -55,7 +59,7 @@ class BaseEntityAdmin(ModelAdmin):
|
|||
return super().render_change_form(request, context, *args, **kwargs)
|
||||
|
||||
# Get the non-EAV fieldsets and then append our own
|
||||
fieldsets = list(self.get_fieldsets(request, kwargs['obj']))
|
||||
fieldsets = list(self.get_fieldsets(request, kwargs["obj"]))
|
||||
fieldsets.append(self._get_eav_fieldset(eav_fields))
|
||||
|
||||
# Reconstruct the admin form with updated fieldsets.
|
||||
|
|
@ -65,18 +69,18 @@ class BaseEntityAdmin(ModelAdmin):
|
|||
# Clear prepopulated fields on a view-only form to avoid a crash.
|
||||
(
|
||||
self.prepopulated_fields
|
||||
if self.has_change_permission(request, kwargs['obj'])
|
||||
if self.has_change_permission(request, kwargs["obj"])
|
||||
else {}
|
||||
),
|
||||
readonly_fields=self.readonly_fields,
|
||||
model_admin=self,
|
||||
)
|
||||
media = mark_safe(context['media'] + adminform.media)
|
||||
media = mark_safe(context["media"] + adminform.media) # noqa: S308
|
||||
context.update(adminform=adminform, media=media)
|
||||
|
||||
return super().render_change_form(request, context, *args, **kwargs)
|
||||
|
||||
def _get_eav_fields(self, instance) -> List[str]:
|
||||
def _get_eav_fields(self, instance) -> list[str]:
|
||||
"""Retrieves a list of EAV field slugs for the given instance.
|
||||
|
||||
Args:
|
||||
|
|
@ -85,24 +89,24 @@ class BaseEntityAdmin(ModelAdmin):
|
|||
Returns:
|
||||
A list of strings representing the slugs of EAV fields.
|
||||
"""
|
||||
entity = getattr(instance, instance._eav_config_cls.eav_attr)
|
||||
return list(entity.get_all_attributes().values_list('slug', flat=True))
|
||||
entity = getattr(instance, instance._eav_config_cls.eav_attr) # noqa: SLF001
|
||||
return list(entity.get_all_attributes().values_list("slug", flat=True))
|
||||
|
||||
def _get_eav_fieldset(self, eav_fields) -> _FIELDSET_TYPE:
|
||||
"""Constructs an EAV Attributes fieldset for inclusion in admin form fieldsets.
|
||||
|
||||
Generates a list representing a fieldset specifically for Entity-Attribute-Value (EAV) fields,
|
||||
intended to be appended to the admin form's fieldsets configuration. This facilitates the
|
||||
dynamic inclusion of EAV fields within the Django admin interface by creating a designated
|
||||
section for these attributes.
|
||||
Generates a list representing a fieldset specifically for Entity-Attribute-Value
|
||||
(EAV) fields, intended to be appended to the admin form's fieldsets
|
||||
configuration. This facilitates the dynamic inclusion of EAV fields within the
|
||||
Django admin interface by creating a designated section for these attributes.
|
||||
|
||||
Args:
|
||||
eav_fields (List[str]): A list of slugs representing the EAV fields to be included
|
||||
in the EAV Attributes fieldset.
|
||||
eav_fields (List[str]): A list of slugs representing the EAV fields to be
|
||||
included in the EAV Attributes fieldset.
|
||||
"""
|
||||
return [
|
||||
self.eav_fieldset_title,
|
||||
{'fields': eav_fields, 'description': self.eav_fieldset_description},
|
||||
{"fields": eav_fields, "description": self.eav_fieldset_description},
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -114,9 +118,9 @@ class BaseEntityInlineFormSet(BaseInlineFormSet):
|
|||
def add_fields(self, form, index):
|
||||
if self.instance:
|
||||
setattr(form.instance, self.fk.name, self.instance)
|
||||
form._build_dynamic_fields()
|
||||
form._build_dynamic_fields() # noqa: SLF001
|
||||
|
||||
super(BaseEntityInlineFormSet, self).add_fields(form, index)
|
||||
super().add_fields(form, index)
|
||||
|
||||
|
||||
class BaseEntityInline(InlineModelAdmin):
|
||||
|
|
@ -147,12 +151,12 @@ class BaseEntityInline(InlineModelAdmin):
|
|||
instance = self.model(**kw)
|
||||
form = formset.form(request.POST, instance=instance)
|
||||
|
||||
return [(None, {'fields': form.fields.keys()})]
|
||||
return [(None, {"fields": form.fields.keys()})]
|
||||
|
||||
|
||||
class AttributeAdmin(ModelAdmin):
|
||||
list_display = ('name', 'slug', 'datatype', 'description')
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
list_display = ("name", "slug", "datatype", "description")
|
||||
prepopulated_fields: ClassVar[dict[str, Sequence[str]]] = {"slug": ("name",)}
|
||||
|
||||
|
||||
admin.site.register(Attribute, AttributeAdmin)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ def register_eav(**kwargs):
|
|||
|
||||
def _model_eav_wrapper(model_class):
|
||||
if not issubclass(model_class, Model):
|
||||
raise ValueError('Wrapped class must subclass Model.')
|
||||
raise TypeError("Wrapped class must subclass Model.")
|
||||
register(model_class, **kwargs)
|
||||
return model_class
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
class IllegalAssignmentException(Exception):
|
||||
class IllegalAssignmentException(Exception): # noqa: N818
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class EavDatatypeField(models.CharField):
|
|||
:class:`~eav.models.Attribute` that is already used by
|
||||
:class:`~eav.models.Value` objects.
|
||||
"""
|
||||
super(EavDatatypeField, self).validate(value, instance)
|
||||
super().validate(value, instance)
|
||||
|
||||
if not instance.pk:
|
||||
return
|
||||
|
|
@ -31,8 +31,9 @@ class EavDatatypeField(models.CharField):
|
|||
if instance.value_set.count():
|
||||
raise ValidationError(
|
||||
_(
|
||||
'You cannot change the datatype of an attribute that is already in use.'
|
||||
)
|
||||
"You cannot change the datatype of an "
|
||||
+ "attribute that is already in use.",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -42,21 +43,21 @@ class CSVField(models.TextField): # (models.Field):
|
|||
|
||||
def __init__(self, separator=";", *args, **kwargs):
|
||||
self.separator = separator
|
||||
kwargs.setdefault('default', "")
|
||||
kwargs.setdefault("default", "")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
if self.separator != self.default_separator:
|
||||
kwargs['separator'] = self.separator
|
||||
kwargs["separator"] = self.separator
|
||||
return name, path, args, kwargs
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': CSVFormField}
|
||||
defaults = {"form_class": CSVFormField}
|
||||
defaults.update(kwargs)
|
||||
return super().formfield(**defaults)
|
||||
|
||||
def from_db_value(self, value, expression, connection, context=None):
|
||||
def from_db_value(self, value, expression, connection):
|
||||
if value is None:
|
||||
return []
|
||||
return value.split(self.separator)
|
||||
|
|
@ -73,8 +74,9 @@ class CSVField(models.TextField): # (models.Field):
|
|||
return ""
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
elif isinstance(value, list):
|
||||
if isinstance(value, list):
|
||||
return self.separator.join(value)
|
||||
return value
|
||||
|
||||
def value_to_string(self, obj):
|
||||
value = self.value_from_object(obj)
|
||||
|
|
|
|||
73
eav/forms.py
73
eav/forms.py
|
|
@ -1,6 +1,9 @@
|
|||
"""This module contains forms used for admin integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import ClassVar
|
||||
|
||||
from django.contrib.admin.widgets import AdminSplitDateTime
|
||||
from django.core.exceptions import ValidationError
|
||||
|
|
@ -21,14 +24,14 @@ from eav.widgets import CSVWidget
|
|||
|
||||
|
||||
class CSVFormField(Field):
|
||||
message = _('Enter comma-separated-values. eg: one;two;three.')
|
||||
code = 'invalid'
|
||||
message = _("Enter comma-separated-values. eg: one;two;three.")
|
||||
code = "invalid"
|
||||
widget = CSVWidget
|
||||
default_separator = ";"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.pop('max_length', None)
|
||||
self.separator = kwargs.pop('separator', self.default_separator)
|
||||
kwargs.pop("max_length", None)
|
||||
self.separator = kwargs.pop("separator", self.default_separator)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_python(self, value):
|
||||
|
|
@ -38,9 +41,8 @@ class CSVFormField(Field):
|
|||
|
||||
def validate(self, field_value):
|
||||
super().validate(field_value)
|
||||
try:
|
||||
isinstance(field_value, list)
|
||||
except ValidationError:
|
||||
|
||||
if not isinstance(field_value, list):
|
||||
raise ValidationError(self.message, code=self.code)
|
||||
|
||||
|
||||
|
|
@ -70,20 +72,20 @@ class BaseDynamicEntityForm(ModelForm):
|
|||
===== =============
|
||||
"""
|
||||
|
||||
FIELD_CLASSES = {
|
||||
'text': CharField,
|
||||
'float': FloatField,
|
||||
'int': IntegerField,
|
||||
'date': SplitDateTimeField,
|
||||
'bool': BooleanField,
|
||||
'enum': ChoiceField,
|
||||
'json': JSONField,
|
||||
'csv': CSVFormField,
|
||||
FIELD_CLASSES: ClassVar[dict[str, Field]] = {
|
||||
"text": CharField,
|
||||
"float": FloatField,
|
||||
"int": IntegerField,
|
||||
"date": SplitDateTimeField,
|
||||
"bool": BooleanField,
|
||||
"enum": ChoiceField,
|
||||
"json": JSONField,
|
||||
"csv": CSVFormField,
|
||||
}
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super(BaseDynamicEntityForm, self).__init__(data, *args, **kwargs)
|
||||
config_cls = self.instance._eav_config_cls
|
||||
super().__init__(data, *args, **kwargs)
|
||||
config_cls = self.instance._eav_config_cls # noqa: SLF001
|
||||
self.entity = getattr(self.instance, config_cls.eav_attr)
|
||||
self._build_dynamic_fields()
|
||||
|
||||
|
|
@ -95,35 +97,35 @@ class BaseDynamicEntityForm(ModelForm):
|
|||
value = getattr(self.entity, attribute.slug)
|
||||
|
||||
defaults = {
|
||||
'label': attribute.name.capitalize(),
|
||||
'required': attribute.required,
|
||||
'help_text': attribute.help_text,
|
||||
'validators': attribute.get_validators(),
|
||||
"label": attribute.name.capitalize(),
|
||||
"required": attribute.required,
|
||||
"help_text": attribute.help_text,
|
||||
"validators": attribute.get_validators(),
|
||||
}
|
||||
|
||||
datatype = attribute.datatype
|
||||
|
||||
if datatype == attribute.TYPE_ENUM:
|
||||
values = attribute.get_choices().values_list('id', 'value')
|
||||
choices = [('', '-----')] + list(values)
|
||||
defaults.update({'choices': choices})
|
||||
values = attribute.get_choices().values_list("id", "value")
|
||||
choices = ["", "-----", *list(values)]
|
||||
defaults.update({"choices": choices})
|
||||
|
||||
if value:
|
||||
defaults.update({'initial': value.pk})
|
||||
defaults.update({"initial": value.pk})
|
||||
|
||||
elif datatype == attribute.TYPE_DATE:
|
||||
defaults.update({'widget': AdminSplitDateTime})
|
||||
defaults.update({"widget": AdminSplitDateTime})
|
||||
elif datatype == attribute.TYPE_OBJECT:
|
||||
continue
|
||||
|
||||
MappedField = self.FIELD_CLASSES[datatype]
|
||||
MappedField = self.FIELD_CLASSES[datatype] # noqa: N806
|
||||
self.fields[attribute.slug] = MappedField(**defaults)
|
||||
|
||||
# Fill initial data (if attribute was already defined).
|
||||
if value and not datatype == attribute.TYPE_ENUM:
|
||||
if value and datatype != attribute.TYPE_ENUM:
|
||||
self.initial[attribute.slug] = value
|
||||
|
||||
def save(self, commit=True):
|
||||
def save(self, *, commit=True):
|
||||
"""
|
||||
Saves this ``form``'s cleaned_data into model instance
|
||||
``self.instance`` and related EAV attributes. Returns ``instance``.
|
||||
|
|
@ -131,23 +133,20 @@ class BaseDynamicEntityForm(ModelForm):
|
|||
if self.errors:
|
||||
raise ValueError(
|
||||
_(
|
||||
'The %s could not be saved because the data'
|
||||
'didn\'t validate.' % self.instance._meta.object_name
|
||||
"The %s could not be saved because the data didn't validate.",
|
||||
)
|
||||
% self.instance._meta.object_name, # noqa: SLF001
|
||||
)
|
||||
|
||||
# Create entity instance, don't save yet.
|
||||
instance = super(BaseDynamicEntityForm, self).save(commit=False)
|
||||
instance = super().save(commit=False)
|
||||
|
||||
# Assign attributes.
|
||||
for attribute in self.entity.get_all_attributes():
|
||||
value = self.cleaned_data.get(attribute.slug)
|
||||
|
||||
if attribute.datatype == attribute.TYPE_ENUM:
|
||||
if value:
|
||||
value = attribute.enum_group.values.get(pk=value)
|
||||
else:
|
||||
value = None
|
||||
value = attribute.enum_group.values.get(pk=value) if value else None
|
||||
|
||||
setattr(self.entity, attribute.slug, value)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,6 @@ def get_entity_pk_type(entity_cls) -> str:
|
|||
These values map to `models.Value` as potential fields to use to relate
|
||||
to the proper entity via the correct PK type.
|
||||
"""
|
||||
if isinstance(entity_cls._meta.pk, UUIDField):
|
||||
return 'entity_uuid'
|
||||
return 'entity_id'
|
||||
if isinstance(entity_cls._meta.pk, UUIDField): # noqa: SLF001
|
||||
return "entity_uuid"
|
||||
return "entity_id"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import uuid
|
||||
from functools import partial
|
||||
from typing import Type
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
|
@ -24,7 +23,7 @@ _FIELD_MAPPING = {
|
|||
}
|
||||
|
||||
|
||||
def get_pk_format() -> Type[models.Field]:
|
||||
def get_pk_format() -> models.Field:
|
||||
"""
|
||||
Get the primary key field format based on the Django settings.
|
||||
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@ class EntityManager(models.Manager):
|
|||
Parse eav attributes out of *kwargs*, then try to create and save
|
||||
the object, then assign and save it's eav attributes.
|
||||
"""
|
||||
config_cls = getattr(self.model, '_eav_config_cls', None)
|
||||
config_cls = getattr(self.model, "_eav_config_cls", None)
|
||||
|
||||
if not config_cls or config_cls.manager_only:
|
||||
return super(EntityManager, self).create(**kwargs)
|
||||
return super().create(**kwargs)
|
||||
|
||||
prefix = '%s__' % config_cls.eav_attr
|
||||
prefix = f"{config_cls.eav_attr}__"
|
||||
new_kwargs = {}
|
||||
eav_kwargs = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,215 +8,220 @@ import eav.fields
|
|||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
"""Initial migration that creates the Attribute, EnumGroup, EnumValue, and Value models."""
|
||||
"""Initial migration for the Attribute, EnumGroup, EnumValue, and Value models."""
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Attribute',
|
||||
name="Attribute",
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID',
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
'name',
|
||||
"name",
|
||||
models.CharField(
|
||||
help_text='User-friendly attribute name',
|
||||
help_text="User-friendly attribute name",
|
||||
max_length=100,
|
||||
verbose_name='Name',
|
||||
verbose_name="Name",
|
||||
),
|
||||
),
|
||||
(
|
||||
'slug',
|
||||
"slug",
|
||||
models.SlugField(
|
||||
help_text='Short unique attribute label',
|
||||
help_text="Short unique attribute label",
|
||||
unique=True,
|
||||
verbose_name='Slug',
|
||||
verbose_name="Slug",
|
||||
),
|
||||
),
|
||||
(
|
||||
'description',
|
||||
"description",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text='Short description',
|
||||
help_text="Short description",
|
||||
max_length=256,
|
||||
null=True,
|
||||
verbose_name='Description',
|
||||
verbose_name="Description",
|
||||
),
|
||||
),
|
||||
(
|
||||
'datatype',
|
||||
"datatype",
|
||||
eav.fields.EavDatatypeField(
|
||||
choices=[
|
||||
('text', 'Text'),
|
||||
('date', 'Date'),
|
||||
('float', 'Float'),
|
||||
('int', 'Integer'),
|
||||
('bool', 'True / False'),
|
||||
('object', 'Django Object'),
|
||||
('enum', 'Multiple Choice'),
|
||||
("text", "Text"),
|
||||
("date", "Date"),
|
||||
("float", "Float"),
|
||||
("int", "Integer"),
|
||||
("bool", "True / False"),
|
||||
("object", "Django Object"),
|
||||
("enum", "Multiple Choice"),
|
||||
],
|
||||
max_length=6,
|
||||
verbose_name='Data Type',
|
||||
verbose_name="Data Type",
|
||||
),
|
||||
),
|
||||
(
|
||||
'created',
|
||||
"created",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name='Created',
|
||||
verbose_name="Created",
|
||||
),
|
||||
),
|
||||
(
|
||||
'modified',
|
||||
models.DateTimeField(auto_now=True, verbose_name='Modified'),
|
||||
"modified",
|
||||
models.DateTimeField(auto_now=True, verbose_name="Modified"),
|
||||
),
|
||||
(
|
||||
'required',
|
||||
models.BooleanField(default=False, verbose_name='Required'),
|
||||
"required",
|
||||
models.BooleanField(default=False, verbose_name="Required"),
|
||||
),
|
||||
(
|
||||
'display_order',
|
||||
"display_order",
|
||||
models.PositiveIntegerField(
|
||||
default=1, verbose_name='Display order'
|
||||
default=1,
|
||||
verbose_name="Display order",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
"ordering": ["name"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EnumGroup',
|
||||
name="EnumGroup",
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID',
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
'name',
|
||||
models.CharField(max_length=100, unique=True, verbose_name='Name'),
|
||||
"name",
|
||||
models.CharField(max_length=100, unique=True, verbose_name="Name"),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EnumValue',
|
||||
name="EnumValue",
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID',
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
'value',
|
||||
"value",
|
||||
models.CharField(
|
||||
db_index=True, max_length=50, unique=True, verbose_name='Value'
|
||||
db_index=True,
|
||||
max_length=50,
|
||||
unique=True,
|
||||
verbose_name="Value",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Value',
|
||||
name="Value",
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID',
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
('entity_id', models.IntegerField()),
|
||||
('value_text', models.TextField(blank=True, null=True)),
|
||||
('value_float', models.FloatField(blank=True, null=True)),
|
||||
('value_int', models.IntegerField(blank=True, null=True)),
|
||||
('value_date', models.DateTimeField(blank=True, null=True)),
|
||||
('value_bool', models.NullBooleanField()),
|
||||
('generic_value_id', models.IntegerField(blank=True, null=True)),
|
||||
("entity_id", models.IntegerField()),
|
||||
("value_text", models.TextField(blank=True, null=True)),
|
||||
("value_float", models.FloatField(blank=True, null=True)),
|
||||
("value_int", models.IntegerField(blank=True, null=True)),
|
||||
("value_date", models.DateTimeField(blank=True, null=True)),
|
||||
("value_bool", models.NullBooleanField()),
|
||||
("generic_value_id", models.IntegerField(blank=True, null=True)),
|
||||
(
|
||||
'created',
|
||||
"created",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name='Created'
|
||||
default=django.utils.timezone.now,
|
||||
verbose_name="Created",
|
||||
),
|
||||
),
|
||||
(
|
||||
'modified',
|
||||
models.DateTimeField(auto_now=True, verbose_name='Modified'),
|
||||
"modified",
|
||||
models.DateTimeField(auto_now=True, verbose_name="Modified"),
|
||||
),
|
||||
(
|
||||
'attribute',
|
||||
"attribute",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to='eav.Attribute',
|
||||
verbose_name='Attribute',
|
||||
to="eav.Attribute",
|
||||
verbose_name="Attribute",
|
||||
),
|
||||
),
|
||||
(
|
||||
'entity_ct',
|
||||
"entity_ct",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='value_entities',
|
||||
to='contenttypes.ContentType',
|
||||
related_name="value_entities",
|
||||
to="contenttypes.ContentType",
|
||||
),
|
||||
),
|
||||
(
|
||||
'generic_value_ct',
|
||||
"generic_value_ct",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='value_values',
|
||||
to='contenttypes.ContentType',
|
||||
related_name="value_values",
|
||||
to="contenttypes.ContentType",
|
||||
),
|
||||
),
|
||||
(
|
||||
'value_enum',
|
||||
"value_enum",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='eav_values',
|
||||
to='eav.EnumValue',
|
||||
related_name="eav_values",
|
||||
to="eav.EnumValue",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='enumgroup',
|
||||
name='values',
|
||||
field=models.ManyToManyField(to='eav.EnumValue', verbose_name='Enum group'),
|
||||
model_name="enumgroup",
|
||||
name="values",
|
||||
field=models.ManyToManyField(to="eav.EnumValue", verbose_name="Enum group"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='attribute',
|
||||
name='enum_group',
|
||||
model_name="attribute",
|
||||
name="enum_group",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to='eav.EnumGroup',
|
||||
verbose_name='Choice Group',
|
||||
to="eav.EnumGroup",
|
||||
verbose_name="Choice Group",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# ruff: noqa: UP007
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, Tuple # noqa: UP035
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
|
|
@ -79,40 +81,36 @@ class Attribute(models.Model):
|
|||
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.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'
|
||||
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')),
|
||||
(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
|
||||
|
|
@ -121,13 +119,13 @@ class Attribute(models.Model):
|
|||
datatype = EavDatatypeField(
|
||||
choices=DATATYPE_CHOICES,
|
||||
max_length=6,
|
||||
verbose_name=_('Data Type'),
|
||||
verbose_name=_("Data Type"),
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=CHARFIELD_LENGTH,
|
||||
help_text=_('User-friendly attribute name'),
|
||||
verbose_name=_('Name'),
|
||||
help_text=_("User-friendly attribute name"),
|
||||
verbose_name=_("Name"),
|
||||
)
|
||||
|
||||
"""
|
||||
|
|
@ -139,8 +137,8 @@ class Attribute(models.Model):
|
|||
max_length=SLUGFIELD_MAX_LENGTH,
|
||||
db_index=True,
|
||||
unique=True,
|
||||
help_text=_('Short unique attribute label'),
|
||||
verbose_name=_('Slug'),
|
||||
help_text=_("Short unique attribute label"),
|
||||
verbose_name=_("Slug"),
|
||||
)
|
||||
|
||||
"""
|
||||
|
|
@ -151,13 +149,13 @@ class Attribute(models.Model):
|
|||
"""
|
||||
required = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Required'),
|
||||
verbose_name=_("Required"),
|
||||
)
|
||||
|
||||
entity_ct = models.ManyToManyField(
|
||||
ContentType,
|
||||
blank=True,
|
||||
verbose_name=_('Entity content type'),
|
||||
verbose_name=_("Entity content type"),
|
||||
)
|
||||
"""
|
||||
This field allows you to specify a relationship with any number of content types.
|
||||
|
|
@ -166,49 +164,67 @@ class Attribute(models.Model):
|
|||
:meth:`~eav.registry.EavConfig.get_attributes` method of that entity's config.
|
||||
"""
|
||||
|
||||
enum_group: "ForeignKey[Optional[EnumGroup]]" = ForeignKey(
|
||||
enum_group: ForeignKey[Optional[EnumGroup]] = ForeignKey(
|
||||
"eav.EnumGroup",
|
||||
on_delete=models.PROTECT,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Choice Group'),
|
||||
verbose_name=_("Choice Group"),
|
||||
)
|
||||
|
||||
description = models.CharField(
|
||||
max_length=256,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('Short description'),
|
||||
verbose_name=_('Description'),
|
||||
default="",
|
||||
help_text=_("Short description"),
|
||||
verbose_name=_("Description"),
|
||||
)
|
||||
|
||||
# Useful meta-information
|
||||
|
||||
display_order = models.PositiveIntegerField(
|
||||
default=1,
|
||||
verbose_name=_('Display order'),
|
||||
verbose_name=_("Display order"),
|
||||
)
|
||||
|
||||
modified = models.DateTimeField(
|
||||
auto_now=True,
|
||||
verbose_name=_('Modified'),
|
||||
verbose_name=_("Modified"),
|
||||
)
|
||||
|
||||
created = models.DateTimeField(
|
||||
default=timezone.now,
|
||||
editable=False,
|
||||
verbose_name=_('Created'),
|
||||
verbose_name=_("Created"),
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.name} ({self.get_datatype_display()})'
|
||||
objects = AttributeManager()
|
||||
|
||||
def natural_key(self) -> Tuple[str, str]: # noqa: UP006
|
||||
class Meta:
|
||||
ordering = ("name",)
|
||||
verbose_name = _("Attribute")
|
||||
verbose_name_plural = _("Attributes")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name} ({self.get_datatype_display()})"
|
||||
|
||||
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 natural_key(self) -> tuple[str, str]:
|
||||
"""
|
||||
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.
|
||||
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
|
||||
-------
|
||||
|
|
@ -233,19 +249,19 @@ class Attribute(models.Model):
|
|||
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,
|
||||
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]]
|
||||
return [datatype_validators[self.datatype]]
|
||||
|
||||
def validate_value(self, value):
|
||||
"""
|
||||
|
|
@ -260,21 +276,10 @@ class Attribute(models.Model):
|
|||
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},
|
||||
_("%(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
|
||||
|
|
@ -283,12 +288,12 @@ class Attribute(models.Model):
|
|||
"""
|
||||
if self.datatype == self.TYPE_ENUM and not self.enum_group:
|
||||
raise ValidationError(
|
||||
_('You must set the choice group for multiple choice attributes'),
|
||||
_("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'),
|
||||
_("You can only assign a choice group to multiple choice attributes"),
|
||||
)
|
||||
|
||||
def clean_fields(self, exclude=None):
|
||||
|
|
@ -308,11 +313,11 @@ class Attribute(models.Model):
|
|||
if not self.slug.isidentifier():
|
||||
raise ValidationError(
|
||||
{
|
||||
'slug': _(
|
||||
"slug": _(
|
||||
"Slug must be a valid Python identifier (no spaces, "
|
||||
"special characters, or leading digits)."
|
||||
)
|
||||
}
|
||||
+ "special characters, or leading digits).",
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
def get_choices(self):
|
||||
|
|
@ -342,20 +347,20 @@ class Attribute(models.Model):
|
|||
ct = ContentType.objects.get_for_model(entity)
|
||||
|
||||
entity_filter = {
|
||||
'entity_ct': ct,
|
||||
'attribute': self,
|
||||
f'{get_entity_pk_type(entity)}': entity.pk,
|
||||
"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 == '':
|
||||
if value is None or value == "":
|
||||
return
|
||||
|
||||
value_obj = Value.objects.create(**entity_filter)
|
||||
|
||||
if value is None or value == '':
|
||||
if value is None or value == "":
|
||||
value_obj.delete()
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ class Entity:
|
|||
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)
|
||||
instance = kwargs["instance"]
|
||||
entity = getattr(kwargs["instance"], instance._eav_config_cls.eav_attr) # noqa: SLF001
|
||||
entity.validate_attributes()
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -34,8 +34,8 @@ class Entity:
|
|||
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)
|
||||
instance = kwargs["instance"]
|
||||
entity = getattr(instance, instance._eav_config_cls.eav_attr) # noqa: SLF001
|
||||
entity.save()
|
||||
|
||||
def __init__(self, instance) -> None:
|
||||
|
|
@ -58,14 +58,14 @@ class Entity:
|
|||
class:`Value` object, otherwise it hasn't been set, so it returns
|
||||
None.
|
||||
"""
|
||||
if not name.startswith('_'):
|
||||
if not name.startswith("_"):
|
||||
try:
|
||||
attribute = self.get_attribute_by_slug(name)
|
||||
except Attribute.DoesNotExist:
|
||||
except Attribute.DoesNotExist as err:
|
||||
raise AttributeError(
|
||||
_('%(obj)s has no EAV attribute named %(attr)s')
|
||||
% {'obj': self.instance, 'attr': name},
|
||||
)
|
||||
_("%(obj)s has no EAV attribute named %(attr)s")
|
||||
% {"obj": self.instance, "attr": name},
|
||||
) from err
|
||||
|
||||
try:
|
||||
return self.get_value_by_attribute(attribute).value
|
||||
|
|
@ -79,9 +79,9 @@ class Entity:
|
|||
Return a query set of all :class:`Attribute` objects that can be set
|
||||
for this entity.
|
||||
"""
|
||||
return self.instance._eav_config_cls.get_attributes(
|
||||
return self.instance._eav_config_cls.get_attributes( # noqa: SLF001
|
||||
instance=self.instance,
|
||||
).order_by('display_order')
|
||||
).order_by("display_order")
|
||||
|
||||
def _hasattr(self, attribute_slug):
|
||||
"""
|
||||
|
|
@ -137,28 +137,29 @@ class Entity:
|
|||
if value is None:
|
||||
if attribute.required:
|
||||
raise ValidationError(
|
||||
_(f'{attribute.slug} EAV field cannot be blank'),
|
||||
_("%s EAV field cannot be blank") % attribute.slug,
|
||||
)
|
||||
else:
|
||||
try:
|
||||
attribute.validate_value(value)
|
||||
except ValidationError as e:
|
||||
except ValidationError as err:
|
||||
raise ValidationError(
|
||||
_('%(attr)s EAV field %(err)s')
|
||||
% {'attr': attribute.slug, 'err': e},
|
||||
)
|
||||
_("%(attr)s EAV field %(err)s")
|
||||
% {"attr": attribute.slug, "err": err},
|
||||
) from err
|
||||
|
||||
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),
|
||||
),
|
||||
message = (
|
||||
"Instance of the class {} cannot have values for attributes: {}."
|
||||
).format(
|
||||
self.instance.__class__,
|
||||
", ".join(illegal),
|
||||
)
|
||||
raise IllegalAssignmentException(message)
|
||||
|
||||
def get_values_dict(self):
|
||||
return {v.attribute.slug: v.value for v in self.get_values()}
|
||||
|
|
@ -166,15 +167,15 @@ class Entity:
|
|||
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,
|
||||
"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))
|
||||
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*."""
|
||||
|
|
@ -189,7 +190,7 @@ class Entity:
|
|||
Returns entity instance attributes, except for
|
||||
``instance`` and ``ct`` which are used internally.
|
||||
"""
|
||||
return set(copy(self.__dict__).keys()) - {'instance', 'ct'}
|
||||
return set(copy(self.__dict__).keys()) - {"instance", "ct"}
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
from typing import TYPE_CHECKING, Any, Tuple
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import ManyToManyField
|
||||
|
|
@ -21,33 +23,33 @@ class EnumGroup(models.Model):
|
|||
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'),
|
||||
verbose_name=_("Name"),
|
||||
)
|
||||
values: "ManyToManyField[EnumValue, Any]" = ManyToManyField(
|
||||
values: ManyToManyField[EnumValue, Any] = ManyToManyField(
|
||||
"eav.EnumValue",
|
||||
verbose_name=_('Enum group'),
|
||||
verbose_name=_("Enum group"),
|
||||
)
|
||||
|
||||
objects = EnumGroupManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("EnumGroup")
|
||||
verbose_name_plural = _("EnumGroups")
|
||||
|
||||
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}>'
|
||||
return f"<EnumGroup {self.name}>"
|
||||
|
||||
def natural_key(self) -> Tuple[str]:
|
||||
def natural_key(self) -> tuple[str]:
|
||||
"""
|
||||
Retrieve the natural key for the EnumGroup instance.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Tuple
|
||||
from __future__ import annotations
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
|
@ -35,21 +35,21 @@ class EnumValue(models.Model):
|
|||
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'),
|
||||
_("Value"),
|
||||
db_index=True,
|
||||
unique=True,
|
||||
max_length=SLUGFIELD_MAX_LENGTH,
|
||||
)
|
||||
|
||||
objects = EnumValueManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("EnumValue")
|
||||
verbose_name_plural = _("EnumValues")
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of `EnumValue` instance."""
|
||||
return str(
|
||||
|
|
@ -58,9 +58,9 @@ class EnumValue(models.Model):
|
|||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of `EnumValue` object."""
|
||||
return f'<EnumValue {self.value}>'
|
||||
return f"<EnumValue {self.value}>"
|
||||
|
||||
def natural_key(self) -> Tuple[str]:
|
||||
def natural_key(self) -> tuple[str]:
|
||||
"""
|
||||
Retrieve the natural key for the EnumValue instance.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# ruff: noqa: UP007
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from django.contrib.contenttypes import fields as generic
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
|
@ -43,20 +44,14 @@ class Value(models.Model):
|
|||
# = <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(
|
||||
attribute: ForeignKey[Attribute] = ForeignKey(
|
||||
"eav.Attribute",
|
||||
db_index=True,
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_('Attribute'),
|
||||
verbose_name=_("Attribute"),
|
||||
)
|
||||
|
||||
# Entity generic relationships. Rather than rely on database casting,
|
||||
|
|
@ -65,73 +60,73 @@ class Value(models.Model):
|
|||
entity_id = models.IntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Entity id'),
|
||||
verbose_name=_("Entity id"),
|
||||
)
|
||||
|
||||
entity_uuid = models.UUIDField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Entity uuid'),
|
||||
verbose_name=_("Entity uuid"),
|
||||
)
|
||||
|
||||
entity_ct = ForeignKey(
|
||||
ContentType,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='value_entities',
|
||||
verbose_name=_('Entity ct'),
|
||||
related_name="value_entities",
|
||||
verbose_name=_("Entity ct"),
|
||||
)
|
||||
|
||||
entity_pk_int = generic.GenericForeignKey(
|
||||
ct_field='entity_ct',
|
||||
fk_field='entity_id',
|
||||
ct_field="entity_ct",
|
||||
fk_field="entity_id",
|
||||
)
|
||||
|
||||
entity_pk_uuid = generic.GenericForeignKey(
|
||||
ct_field='entity_ct',
|
||||
fk_field='entity_uuid',
|
||||
ct_field="entity_ct",
|
||||
fk_field="entity_uuid",
|
||||
)
|
||||
|
||||
# Model attributes
|
||||
created = models.DateTimeField(
|
||||
default=timezone.now,
|
||||
verbose_name=_('Created'),
|
||||
verbose_name=_("Created"),
|
||||
)
|
||||
|
||||
modified = models.DateTimeField(
|
||||
auto_now=True,
|
||||
verbose_name=_('Modified'),
|
||||
verbose_name=_("Modified"),
|
||||
)
|
||||
|
||||
# Value attributes
|
||||
value_bool = models.BooleanField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Value bool'),
|
||||
verbose_name=_("Value bool"),
|
||||
)
|
||||
value_csv = CSVField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Value CSV'),
|
||||
verbose_name=_("Value CSV"),
|
||||
)
|
||||
value_date = models.DateTimeField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Value date'),
|
||||
verbose_name=_("Value date"),
|
||||
)
|
||||
value_float = models.FloatField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Value float'),
|
||||
verbose_name=_("Value float"),
|
||||
)
|
||||
value_int = models.BigIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Value int'),
|
||||
verbose_name=_("Value int"),
|
||||
)
|
||||
value_text = models.TextField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Value text'),
|
||||
default="",
|
||||
verbose_name=_("Value text"),
|
||||
)
|
||||
|
||||
value_json = models.JSONField(
|
||||
|
|
@ -139,23 +134,23 @@ class Value(models.Model):
|
|||
encoder=DjangoJSONEncoder,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Value JSON'),
|
||||
verbose_name=_("Value JSON"),
|
||||
)
|
||||
|
||||
value_enum: "ForeignKey[Optional[EnumValue]]" = ForeignKey(
|
||||
value_enum: ForeignKey[Optional[EnumValue]] = ForeignKey(
|
||||
"eav.EnumValue",
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='eav_values',
|
||||
verbose_name=_('Value enum'),
|
||||
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'),
|
||||
verbose_name=_("Generic value id"),
|
||||
)
|
||||
|
||||
generic_value_ct = ForeignKey(
|
||||
|
|
@ -163,16 +158,37 @@ class Value(models.Model):
|
|||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='value_values',
|
||||
verbose_name=_('Generic value content type'),
|
||||
related_name="value_values",
|
||||
verbose_name=_("Generic value content type"),
|
||||
)
|
||||
|
||||
value_object = generic.GenericForeignKey(
|
||||
ct_field='generic_value_ct',
|
||||
fk_field='generic_value_id',
|
||||
ct_field="generic_value_ct",
|
||||
fk_field="generic_value_id",
|
||||
)
|
||||
|
||||
def natural_key(self) -> Tuple[Tuple[str, str], int, str]:
|
||||
objects = ValueManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Value")
|
||||
verbose_name_plural = _("Values")
|
||||
|
||||
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 save(self, *args, **kwargs):
|
||||
"""Validate and save this value."""
|
||||
self.full_clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
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 natural_key(self) -> tuple[tuple[str, str], int, str]:
|
||||
"""
|
||||
Retrieve the natural key for the Value instance.
|
||||
|
||||
|
|
@ -187,27 +203,12 @@ class Value(models.Model):
|
|||
"""
|
||||
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}')
|
||||
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)
|
||||
setattr(self, f"value_{self.attribute.datatype}", new_value)
|
||||
|
||||
value = property(_get_value, _set_value)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module contains custom :class:`EavQuerySet` class used for overriding
|
||||
relational operators and pure functions for rewriting Q-expressions.
|
||||
|
|
@ -19,14 +18,14 @@ Q-expressions need to be rewritten for two reasons:
|
|||
2. To ensure that Q-expression tree is compiled to valid SQL.
|
||||
For details see: :func:`rewrite_q_expr`.
|
||||
"""
|
||||
|
||||
from functools import wraps
|
||||
from itertools import count
|
||||
|
||||
from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models import Case, IntegerField, Q, When
|
||||
from django.db.models.query import QuerySet
|
||||
from django.db.utils import NotSupportedError
|
||||
from django.db.models import Subquery
|
||||
|
||||
from eav.models import Attribute, EnumValue, Value
|
||||
|
||||
|
|
@ -43,9 +42,9 @@ def is_eav_and_leaf(expr, gr_name):
|
|||
bool
|
||||
"""
|
||||
return (
|
||||
getattr(expr, 'connector', None) == 'AND'
|
||||
getattr(expr, "connector", None) == "AND"
|
||||
and len(expr.children) == 1
|
||||
and expr.children[0][0] in ['pk__in', '{}__in'.format(gr_name)]
|
||||
and expr.children[0][0] in ["pk__in", f"{gr_name}__in"]
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -98,7 +97,7 @@ def rewrite_q_expr(model_cls, expr):
|
|||
# We are only interested in Qs.
|
||||
|
||||
if isinstance(expr, Q):
|
||||
config_cls = getattr(model_cls, '_eav_config_cls', None)
|
||||
config_cls = getattr(model_cls, "_eav_config_cls", None)
|
||||
gr_name = config_cls.generic_relation_attr
|
||||
|
||||
# Recursively check child nodes.
|
||||
|
|
@ -112,18 +111,18 @@ def rewrite_q_expr(model_cls, expr):
|
|||
if len(rewritable) > 1:
|
||||
q = None
|
||||
# Save nodes which shouldn't be merged (non-EAV).
|
||||
other = [c for c in expr.children if not c in rewritable]
|
||||
other = [c for c in expr.children if c not in rewritable]
|
||||
|
||||
for child in rewritable:
|
||||
if not (child.children and len(child.children) == 1):
|
||||
raise AssertionError('Child must have exactly one descendant')
|
||||
raise AssertionError("Child must have exactly one descendant")
|
||||
# Child to be merged is always a terminal Q node,
|
||||
# i.e. it's an AND expression with attribute-value tuple child.
|
||||
attrval = child.children[0]
|
||||
if not isinstance(attrval, tuple):
|
||||
raise AssertionError('Attribute-value must be a tuple')
|
||||
raise TypeError("Attribute-value must be a tuple")
|
||||
|
||||
fname = '{}__in'.format(gr_name)
|
||||
fname = f"{gr_name}__in"
|
||||
|
||||
# Child can be either a 'eav_values__in' or 'pk__in' query.
|
||||
# If it's the former then transform it into the latter.
|
||||
|
|
@ -131,7 +130,7 @@ def rewrite_q_expr(model_cls, expr):
|
|||
# If so, reverse it back to QuerySet so that set operators
|
||||
# can be applied.
|
||||
|
||||
if attrval[0] == fname or hasattr(attrval[1], '__contains__'):
|
||||
if attrval[0] == fname or hasattr(attrval[1], "__contains__"):
|
||||
# Create model queryset.
|
||||
_q = model_cls.objects.filter(**{fname: attrval[1]})
|
||||
else:
|
||||
|
|
@ -140,17 +139,17 @@ def rewrite_q_expr(model_cls, expr):
|
|||
|
||||
# Explicitly check for None. 'or' doesn't work here
|
||||
# as empty QuerySet, which is valid, is falsy.
|
||||
q = q if q != None else _q
|
||||
q = q if q is not None else _q
|
||||
|
||||
if expr.connector == 'AND':
|
||||
if expr.connector == "AND":
|
||||
q &= _q
|
||||
else:
|
||||
q |= _q
|
||||
|
||||
# If any two children were merged,
|
||||
# update parent expression.
|
||||
if q != None:
|
||||
expr.children = other + [('pk__in', q)]
|
||||
if q is not None:
|
||||
expr.children = [*other, ("pk__in", q)]
|
||||
|
||||
return expr
|
||||
|
||||
|
|
@ -170,9 +169,9 @@ def eav_filter(func):
|
|||
for arg in args:
|
||||
if isinstance(arg, Q):
|
||||
# Modify Q objects (warning: recursion ahead).
|
||||
arg = expand_q_filters(arg, self.model)
|
||||
arg = expand_q_filters(arg, self.model) # noqa: PLW2901
|
||||
# Rewrite Q-expression to safeform.
|
||||
arg = rewrite_q_expr(self.model, arg)
|
||||
arg = rewrite_q_expr(self.model, arg) # noqa: PLW2901
|
||||
nargs.append(arg)
|
||||
|
||||
for key, value in kwargs.items():
|
||||
|
|
@ -180,9 +179,10 @@ def eav_filter(func):
|
|||
nkey, nval = expand_eav_filter(self.model, key, value)
|
||||
|
||||
if nkey in nkwargs:
|
||||
# Add filter to check if matching entity_id is in the previous queryset with same nkey
|
||||
# Add filter to check if matching entity_id is
|
||||
# in the previous queryset with same nkey
|
||||
nkwargs[nkey] = nval.filter(
|
||||
entity_id__in=nkwargs[nkey].values_list('entity_id', flat=True)
|
||||
entity_id__in=nkwargs[nkey].values_list("entity_id", flat=True),
|
||||
).distinct()
|
||||
else:
|
||||
nkwargs.update({nkey: nval})
|
||||
|
|
@ -229,27 +229,27 @@ def expand_eav_filter(model_cls, key, value):
|
|||
key = 'eav_values__in'
|
||||
value = Values.objects.filter(value_int=5, attribute__slug='height')
|
||||
"""
|
||||
fields = key.split('__')
|
||||
config_cls = getattr(model_cls, '_eav_config_cls', None)
|
||||
fields = key.split("__")
|
||||
config_cls = getattr(model_cls, "_eav_config_cls", None)
|
||||
|
||||
if len(fields) > 1 and config_cls and fields[0] == config_cls.eav_attr:
|
||||
slug = fields[1]
|
||||
gr_name = config_cls.generic_relation_attr
|
||||
datatype = Attribute.objects.get(slug=slug).datatype
|
||||
|
||||
value_key = ''
|
||||
value_key = ""
|
||||
if datatype == Attribute.TYPE_ENUM and not isinstance(value, EnumValue):
|
||||
lookup = '__value__{}'.format(fields[2]) if len(fields) > 2 else '__value'
|
||||
value_key = 'value_{}{}'.format(datatype, lookup)
|
||||
lookup = f"__value__{fields[2]}" if len(fields) > 2 else "__value" # noqa: PLR2004
|
||||
value_key = f"value_{datatype}{lookup}"
|
||||
elif datatype == Attribute.TYPE_OBJECT:
|
||||
value_key = 'generic_value_id'
|
||||
value_key = "generic_value_id"
|
||||
else:
|
||||
lookup = '__{}'.format(fields[2]) if len(fields) > 2 else ''
|
||||
value_key = 'value_{}{}'.format(datatype, lookup)
|
||||
kwargs = {value_key: value, 'attribute__slug': slug}
|
||||
lookup = f"__{fields[2]}" if len(fields) > 2 else "" # noqa: PLR2004
|
||||
value_key = f"value_{datatype}{lookup}"
|
||||
kwargs = {value_key: value, "attribute__slug": slug}
|
||||
value = Value.objects.filter(**kwargs)
|
||||
|
||||
return '%s__in' % gr_name, value
|
||||
return f"{gr_name}__in", value
|
||||
|
||||
# Not an eav field, so keep as is
|
||||
return key, value
|
||||
|
|
@ -266,7 +266,7 @@ class EavQuerySet(QuerySet):
|
|||
Pass *args* and *kwargs* through :func:`eav_filter`, then pass to
|
||||
the ``Manager`` filter method.
|
||||
"""
|
||||
return super(EavQuerySet, self).filter(*args, **kwargs)
|
||||
return super().filter(*args, **kwargs)
|
||||
|
||||
@eav_filter
|
||||
def exclude(self, *args, **kwargs):
|
||||
|
|
@ -274,7 +274,7 @@ class EavQuerySet(QuerySet):
|
|||
Pass *args* and *kwargs* through :func:`eav_filter`, then pass to
|
||||
the ``Manager`` exclude method.
|
||||
"""
|
||||
return super(EavQuerySet, self).exclude(*args, **kwargs)
|
||||
return super().exclude(*args, **kwargs)
|
||||
|
||||
@eav_filter
|
||||
def get(self, *args, **kwargs):
|
||||
|
|
@ -282,7 +282,7 @@ class EavQuerySet(QuerySet):
|
|||
Pass *args* and *kwargs* through :func:`eav_filter`, then pass to
|
||||
the ``Manager`` get method.
|
||||
"""
|
||||
return super(EavQuerySet, self).get(*args, **kwargs)
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
def order_by(self, *fields):
|
||||
# Django only allows to order querysets by direct fields and
|
||||
|
|
@ -292,20 +292,20 @@ class EavQuerySet(QuerySet):
|
|||
# This will be slow, of course.
|
||||
order_clauses = []
|
||||
query_clause = self
|
||||
config_cls = self.model._eav_config_cls
|
||||
config_cls = self.model._eav_config_cls # noqa: SLF001
|
||||
|
||||
for term in [t.split('__') for t in fields]:
|
||||
for term in [t.split("__") for t in fields]:
|
||||
# Continue only for EAV attributes.
|
||||
if len(term) == 2 and term[0] == config_cls.eav_attr:
|
||||
if len(term) == 2 and term[0] == config_cls.eav_attr: # noqa: PLR2004
|
||||
# Retrieve Attribute over which the ordering is performed.
|
||||
try:
|
||||
attr = Attribute.objects.get(slug=term[1])
|
||||
except ObjectDoesNotExist:
|
||||
except ObjectDoesNotExist as err:
|
||||
raise ObjectDoesNotExist(
|
||||
'Cannot find EAV attribute "{}"'.format(term[1])
|
||||
)
|
||||
f'Cannot find EAV attribute "{term[1]}"',
|
||||
) from err
|
||||
|
||||
field_name = 'value_%s' % attr.datatype
|
||||
field_name = f"value_{attr.datatype}"
|
||||
|
||||
pks_values = (
|
||||
Value.objects.filter(
|
||||
|
|
@ -318,12 +318,12 @@ class EavQuerySet(QuerySet):
|
|||
.order_by(
|
||||
# Order values by their value-field of
|
||||
# appropriate attribute data-type.
|
||||
field_name
|
||||
field_name,
|
||||
)
|
||||
.values_list(
|
||||
# Retrieve only primary-keys of the entities
|
||||
# in the current queryset.
|
||||
'entity_id',
|
||||
"entity_id",
|
||||
field_name,
|
||||
)
|
||||
)
|
||||
|
|
@ -352,16 +352,16 @@ class EavQuerySet(QuerySet):
|
|||
|
||||
order_clause = Case(*when_clauses, output_field=IntegerField())
|
||||
|
||||
clause_name = '__'.join(term)
|
||||
clause_name = "__".join(term)
|
||||
# Use when-clause to construct
|
||||
# custom order-by clause.
|
||||
query_clause = query_clause.annotate(**{clause_name: order_clause})
|
||||
|
||||
order_clauses.append(clause_name)
|
||||
|
||||
elif len(term) >= 2 and term[0] == config_cls.eav_attr:
|
||||
elif len(term) >= 2 and term[0] == config_cls.eav_attr: # noqa: PLR2004
|
||||
raise NotSupportedError(
|
||||
'EAV does not support ordering through ' 'foreign-key chains'
|
||||
"EAV does not support ordering through foreign-key chains",
|
||||
)
|
||||
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -3,13 +3,12 @@
|
|||
from django.contrib.contenttypes import fields as generic
|
||||
from django.db.models.signals import post_init, post_save, pre_save
|
||||
|
||||
from eav.logic.entity_pk import get_entity_pk_type
|
||||
from eav.managers import EntityManager
|
||||
from eav.models import Attribute, Entity, Value
|
||||
|
||||
from eav.logic.entity_pk import get_entity_pk_type
|
||||
|
||||
|
||||
class EavConfig(object):
|
||||
class EavConfig:
|
||||
"""
|
||||
The default ``EavConfig`` class used if it is not overridden on registration.
|
||||
This is where all the default eav attribute names are defined.
|
||||
|
|
@ -29,10 +28,10 @@ class EavConfig(object):
|
|||
if not overridden, it is not possible to query Values by Entities.
|
||||
"""
|
||||
|
||||
manager_attr = 'objects'
|
||||
manager_attr = "objects"
|
||||
manager_only = False
|
||||
eav_attr = 'eav'
|
||||
generic_relation_attr = 'eav_values'
|
||||
eav_attr = "eav"
|
||||
generic_relation_attr = "eav_values"
|
||||
generic_relation_related_name = None
|
||||
|
||||
@classmethod
|
||||
|
|
@ -44,7 +43,7 @@ class EavConfig(object):
|
|||
return Attribute.objects.all()
|
||||
|
||||
|
||||
class Registry(object):
|
||||
class Registry:
|
||||
"""
|
||||
Handles registration through the
|
||||
:meth:`register` and :meth:`unregister` methods.
|
||||
|
|
@ -59,14 +58,14 @@ class Registry(object):
|
|||
.. note::
|
||||
Multiple registrations for the same entity are harmlessly ignored.
|
||||
"""
|
||||
if hasattr(model_cls, '_eav_config_cls'):
|
||||
if hasattr(model_cls, "_eav_config_cls"):
|
||||
return
|
||||
|
||||
if config_cls is EavConfig or config_cls is None:
|
||||
config_cls = type("%sConfig" % model_cls.__name__, (EavConfig,), {})
|
||||
config_cls = type(f"{model_cls.__name__}Config", (EavConfig,), {})
|
||||
|
||||
# set _eav_config_cls on the model so we can access it there
|
||||
setattr(model_cls, '_eav_config_cls', config_cls)
|
||||
model_cls._eav_config_cls = config_cls
|
||||
|
||||
reg = Registry(model_cls)
|
||||
reg._register_self()
|
||||
|
|
@ -79,19 +78,19 @@ class Registry(object):
|
|||
.. note::
|
||||
Unregistering a class not already registered is harmlessly ignored.
|
||||
"""
|
||||
if not getattr(model_cls, '_eav_config_cls', None):
|
||||
if not getattr(model_cls, "_eav_config_cls", None):
|
||||
return
|
||||
reg = Registry(model_cls)
|
||||
reg._unregister_self()
|
||||
|
||||
delattr(model_cls, '_eav_config_cls')
|
||||
delattr(model_cls, "_eav_config_cls")
|
||||
|
||||
@staticmethod
|
||||
def attach_eav_attr(sender, *args, **kwargs):
|
||||
"""
|
||||
Attach EAV Entity toolkit to an instance after init.
|
||||
"""
|
||||
instance = kwargs['instance']
|
||||
instance = kwargs["instance"]
|
||||
config_cls = instance.__class__._eav_config_cls
|
||||
setattr(instance, config_cls.eav_attr, Entity(instance))
|
||||
|
||||
|
|
@ -147,9 +146,10 @@ class Registry(object):
|
|||
self.model_cls._meta._expire_cache()
|
||||
delattr(self.model_cls, self.config_cls.manager_attr)
|
||||
|
||||
if hasattr(self.config_cls, 'old_mgr'):
|
||||
if hasattr(self.config_cls, "old_mgr"):
|
||||
self.config_cls.old_mgr.contribute_to_class(
|
||||
self.model_cls, self.config_cls.manager_attr
|
||||
self.model_cls,
|
||||
self.config_cls.manager_attr,
|
||||
)
|
||||
|
||||
def _attach_signals(self):
|
||||
|
|
@ -181,7 +181,7 @@ class Registry(object):
|
|||
generic_relation = generic.GenericRelation(
|
||||
Value,
|
||||
object_id_field=get_entity_pk_type(self.model_cls),
|
||||
content_type_field='entity_ct',
|
||||
content_type_field="entity_ct",
|
||||
related_query_name=rel_name,
|
||||
)
|
||||
generic_relation.contribute_to_class(self.model_cls, gr_name)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ def validate_text(value):
|
|||
Raises ``ValidationError`` unless *value* type is ``str`` or ``unicode``
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
raise ValidationError(_(u"Must be str or unicode"))
|
||||
raise ValidationError(_("Must be str or unicode"))
|
||||
|
||||
|
||||
def validate_float(value):
|
||||
|
|
@ -32,8 +32,8 @@ def validate_float(value):
|
|||
"""
|
||||
try:
|
||||
float(value)
|
||||
except ValueError:
|
||||
raise ValidationError(_(u"Must be a float"))
|
||||
except ValueError as err:
|
||||
raise ValidationError(_("Must be a float")) from err
|
||||
|
||||
|
||||
def validate_int(value):
|
||||
|
|
@ -42,8 +42,8 @@ def validate_int(value):
|
|||
"""
|
||||
try:
|
||||
int(value)
|
||||
except ValueError:
|
||||
raise ValidationError(_(u"Must be an integer"))
|
||||
except ValueError as err:
|
||||
raise ValidationError(_("Must be an integer")) from err
|
||||
|
||||
|
||||
def validate_date(value):
|
||||
|
|
@ -52,9 +52,10 @@ def validate_date(value):
|
|||
or ``date``
|
||||
"""
|
||||
if not isinstance(value, datetime.datetime) and not isinstance(
|
||||
value, datetime.date
|
||||
value,
|
||||
datetime.date,
|
||||
):
|
||||
raise ValidationError(_(u"Must be a date or datetime"))
|
||||
raise ValidationError(_("Must be a date or datetime"))
|
||||
|
||||
|
||||
def validate_bool(value):
|
||||
|
|
@ -62,7 +63,7 @@ def validate_bool(value):
|
|||
Raises ``ValidationError`` unless *value* type is ``bool``
|
||||
"""
|
||||
if not isinstance(value, bool):
|
||||
raise ValidationError(_(u"Must be a boolean"))
|
||||
raise ValidationError(_("Must be a boolean"))
|
||||
|
||||
|
||||
def validate_object(value):
|
||||
|
|
@ -71,10 +72,10 @@ def validate_object(value):
|
|||
django model instance.
|
||||
"""
|
||||
if not isinstance(value, models.Model):
|
||||
raise ValidationError(_(u"Must be a django model object instance"))
|
||||
raise ValidationError(_("Must be a django model object instance"))
|
||||
|
||||
if not value.pk:
|
||||
raise ValidationError(_(u"Model has not been saved yet"))
|
||||
raise ValidationError(_("Model has not been saved yet"))
|
||||
|
||||
|
||||
def validate_enum(value):
|
||||
|
|
@ -85,7 +86,7 @@ def validate_enum(value):
|
|||
from eav.models import EnumValue
|
||||
|
||||
if isinstance(value, EnumValue) and not value.pk:
|
||||
raise ValidationError(_(u"EnumValue has not been saved yet"))
|
||||
raise ValidationError(_("EnumValue has not been saved yet"))
|
||||
|
||||
|
||||
def validate_json(value):
|
||||
|
|
@ -96,9 +97,9 @@ def validate_json(value):
|
|||
if isinstance(value, str):
|
||||
value = json.loads(value)
|
||||
if not isinstance(value, dict):
|
||||
raise ValidationError(_(u"Must be a JSON Serializable object"))
|
||||
except ValueError:
|
||||
raise ValidationError(_(u"Must be a JSON Serializable object"))
|
||||
raise ValidationError(_("Must be a JSON Serializable object"))
|
||||
except ValueError as err:
|
||||
raise ValidationError(_("Must be a JSON Serializable object")) from err
|
||||
|
||||
|
||||
def validate_csv(value):
|
||||
|
|
@ -108,4 +109,4 @@ def validate_csv(value):
|
|||
if isinstance(value, str):
|
||||
value = value.split(";")
|
||||
if not isinstance(value, list):
|
||||
raise ValidationError(_(u"Must be Comma-Separated-Value."))
|
||||
raise ValidationError(_("Must be Comma-Separated-Value."))
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from django.core import validators
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.forms.widgets import Textarea
|
||||
|
||||
EMPTY_VALUES = validators.EMPTY_VALUES + ('[]',)
|
||||
EMPTY_VALUES = (*validators.EMPTY_VALUES, "[]")
|
||||
|
||||
|
||||
class CSVWidget(Textarea):
|
||||
|
|
@ -12,11 +12,11 @@ class CSVWidget(Textarea):
|
|||
"""Prepare value before effectively render widget"""
|
||||
if value in EMPTY_VALUES:
|
||||
return ""
|
||||
elif isinstance(value, str):
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
elif isinstance(value, list):
|
||||
if isinstance(value, list):
|
||||
return ";".join(value)
|
||||
raise ValidationError('Invalid format.')
|
||||
raise ValidationError("Invalid format.")
|
||||
|
||||
def render(self, name, value, **kwargs):
|
||||
value = self.prep_value(value)
|
||||
|
|
@ -31,11 +31,9 @@ class CSVWidget(Textarea):
|
|||
key, we need to loop through each field checking if the eav attribute
|
||||
exists with the given 'name'.
|
||||
"""
|
||||
widget_value = None
|
||||
for data_value in data:
|
||||
try:
|
||||
widget_value = getattr(data.get(data_value), name)
|
||||
except AttributeError:
|
||||
pass # noqa: WPS420
|
||||
for data_value in data.values():
|
||||
widget_value = getattr(data_value, name, None)
|
||||
if widget_value is not None:
|
||||
return widget_value
|
||||
|
||||
return widget_value
|
||||
return None
|
||||
|
|
|
|||
14
manage.py
14
manage.py
|
|
@ -13,19 +13,19 @@ def main() -> None:
|
|||
2. Warns if Django is not installed
|
||||
3. Executes any given command
|
||||
"""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_project.settings')
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings")
|
||||
|
||||
try:
|
||||
from django.core import management # noqa: WPS433
|
||||
except ImportError:
|
||||
from django.core import management
|
||||
except ImportError as err:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
+ 'available on your PYTHONPATH environment variable? Did you '
|
||||
+ 'forget to activate a virtual environment?',
|
||||
)
|
||||
+ "available on your PYTHONPATH environment variable? Did you "
|
||||
+ "forget to activate a virtual environment?",
|
||||
) from err
|
||||
|
||||
management.execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -1,16 +1,11 @@
|
|||
import sys
|
||||
import uuid
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Final, final
|
||||
else:
|
||||
from typing_extensions import Final, final
|
||||
from typing import Final, final
|
||||
|
||||
from django.db import models
|
||||
|
||||
from eav.decorators import register_eav
|
||||
from eav.models import EAVModelMeta
|
||||
from eav.managers import EntityManager
|
||||
from eav.models import EAVModelMeta
|
||||
|
||||
#: Constants
|
||||
MAX_CHARFIELD_LEN: Final = 254
|
||||
|
|
@ -19,10 +14,10 @@ MAX_CHARFIELD_LEN: Final = 254
|
|||
class TestBase(models.Model):
|
||||
"""Base class for test models."""
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
"""Define common options."""
|
||||
|
||||
app_label = 'test_project'
|
||||
app_label = "test_project"
|
||||
abstract = True
|
||||
|
||||
|
||||
|
|
@ -62,7 +57,8 @@ class DoctorSubstringManager(models.Manager):
|
|||
substring (str): The substring to search for in the doctor's name.
|
||||
|
||||
Returns:
|
||||
models.QuerySet: A QuerySet of doctors whose names contain the specified substring.
|
||||
models.QuerySet: A QuerySet of doctors whose names contain the
|
||||
specified substring.
|
||||
"""
|
||||
return self.filter(name__icontains=substring)
|
||||
|
||||
|
|
@ -78,13 +74,16 @@ class Doctor(TestBase):
|
|||
objects = DoctorManager()
|
||||
substrings = DoctorSubstringManager()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
@final
|
||||
class Patient(TestBase):
|
||||
name = models.CharField(max_length=MAX_CHARFIELD_LEN)
|
||||
email = models.EmailField(max_length=MAX_CHARFIELD_LEN, blank=True)
|
||||
example = models.ForeignKey(
|
||||
'ExampleModel',
|
||||
"ExampleModel",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.PROTECT,
|
||||
|
|
@ -102,7 +101,7 @@ class Encounter(TestBase):
|
|||
patient = models.ForeignKey(Patient, on_delete=models.PROTECT)
|
||||
|
||||
def __str__(self):
|
||||
return '%s: encounter num %d' % (self.patient, self.num)
|
||||
return "%s: encounter num %d" % (self.patient, self.num)
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
|
@ -113,7 +112,7 @@ class Encounter(TestBase):
|
|||
class ExampleModel(TestBase):
|
||||
name = models.CharField(max_length=MAX_CHARFIELD_LEN)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
|
|
@ -123,7 +122,7 @@ class M2MModel(TestBase):
|
|||
name = models.CharField(max_length=MAX_CHARFIELD_LEN)
|
||||
models = models.ManyToManyField(ExampleModel)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).parent.parent
|
||||
|
|
@ -9,51 +10,51 @@ BASE_DIR = Path(__file__).parent.parent
|
|||
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'secret!' # noqa: S105
|
||||
SECRET_KEY = "secret!" # noqa: S105
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS: List[str] = []
|
||||
ALLOWED_HOSTS: list[str] = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
# Test Project:
|
||||
'test_project.apps.TestAppConfig',
|
||||
"test_project.apps.TestAppConfig",
|
||||
# Our app:
|
||||
'eav',
|
||||
"eav",
|
||||
]
|
||||
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -64,15 +65,15 @@ TEMPLATES = [
|
|||
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ':memory:',
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": ":memory:",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
EAV2_PRIMARY_KEY_FIELD = 'django.db.models.AutoField'
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||
EAV2_PRIMARY_KEY_FIELD = "django.db.models.AutoField"
|
||||
|
||||
|
||||
# Password validation
|
||||
|
|
@ -84,9 +85,9 @@ AUTH_PASSWORD_VALIDATORS = []
|
|||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.1/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
|
|
@ -98,4 +99,4 @@ USE_TZ = False
|
|||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.1/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_URL = "/static/"
|
||||
|
|
|
|||
|
|
@ -27,22 +27,22 @@ else:
|
|||
class Attributes(TestCase):
|
||||
def setUp(self):
|
||||
class EncounterEavConfig(EavConfig):
|
||||
manager_attr = 'eav_objects'
|
||||
eav_attr = 'eav_field'
|
||||
generic_relation_attr = 'encounter_eav_values'
|
||||
generic_relation_related_name = 'encounters'
|
||||
manager_attr = "eav_objects"
|
||||
eav_attr = "eav_field"
|
||||
generic_relation_attr = "encounter_eav_values"
|
||||
generic_relation_related_name = "encounters"
|
||||
|
||||
@classmethod
|
||||
def get_attributes(cls, instance=None):
|
||||
return Attribute.objects.filter(slug__contains='a')
|
||||
return Attribute.objects.filter(slug__contains="a")
|
||||
|
||||
eav.register(Encounter, EncounterEavConfig)
|
||||
eav.register(Patient)
|
||||
|
||||
Attribute.objects.create(name='age', datatype=Attribute.TYPE_INT)
|
||||
Attribute.objects.create(name='height', datatype=Attribute.TYPE_FLOAT)
|
||||
Attribute.objects.create(name='weight', datatype=Attribute.TYPE_FLOAT)
|
||||
Attribute.objects.create(name='color', datatype=Attribute.TYPE_TEXT)
|
||||
Attribute.objects.create(name="age", datatype=Attribute.TYPE_INT)
|
||||
Attribute.objects.create(name="height", datatype=Attribute.TYPE_FLOAT)
|
||||
Attribute.objects.create(name="weight", datatype=Attribute.TYPE_FLOAT)
|
||||
Attribute.objects.create(name="color", datatype=Attribute.TYPE_TEXT)
|
||||
|
||||
def tearDown(self):
|
||||
eav.unregister(Encounter)
|
||||
|
|
@ -53,14 +53,14 @@ class Attributes(TestCase):
|
|||
self.assertEqual(Encounter._eav_config_cls.get_attributes().count(), 1)
|
||||
|
||||
def test_duplicate_attributs(self):
|
||||
'''
|
||||
"""
|
||||
Ensure that no two Attributes with the same slug can exist.
|
||||
'''
|
||||
"""
|
||||
with self.assertRaises(ValidationError):
|
||||
Attribute.objects.create(name='height', datatype=Attribute.TYPE_FLOAT)
|
||||
Attribute.objects.create(name="height", datatype=Attribute.TYPE_FLOAT)
|
||||
|
||||
def test_setting_attributes(self):
|
||||
p = Patient.objects.create(name='Jon')
|
||||
p = Patient.objects.create(name="Jon")
|
||||
e = Encounter.objects.create(patient=p, num=1)
|
||||
|
||||
p.eav.age = 3
|
||||
|
|
@ -73,7 +73,7 @@ class Attributes(TestCase):
|
|||
t.eav.age = 6
|
||||
t.eav.height = 10
|
||||
t.save()
|
||||
p = Patient.objects.get(name='Jon')
|
||||
p = Patient.objects.get(name="Jon")
|
||||
self.assertEqual(p.eav.age, 3)
|
||||
self.assertEqual(p.eav.height, 2.3)
|
||||
e = Encounter.objects.get(num=1)
|
||||
|
|
@ -96,20 +96,21 @@ class Attributes(TestCase):
|
|||
eav.unregister(Encounter)
|
||||
eav.register(Encounter, EncounterEavConfig)
|
||||
|
||||
p = Patient.objects.create(name='Jon')
|
||||
p = Patient.objects.create(name="Jon")
|
||||
e = Encounter.objects.create(patient=p, num=1)
|
||||
|
||||
with self.assertRaises(IllegalAssignmentException):
|
||||
e.eav.color = 'red'
|
||||
e.eav.color = "red"
|
||||
e.save()
|
||||
|
||||
def test_uuid_pk(self):
|
||||
"""Tests for when model pk is UUID."""
|
||||
d1 = Doctor.objects.create(name='Lu')
|
||||
d1.eav.age = 10
|
||||
expected_age = 10
|
||||
d1 = Doctor.objects.create(name="Lu")
|
||||
d1.eav.age = expected_age
|
||||
d1.save()
|
||||
|
||||
assert d1.eav.age == 10
|
||||
assert d1.eav.age == expected_age
|
||||
|
||||
# Validate repr of Value for an entity with a UUID PK
|
||||
v1 = Value.objects.filter(entity_uuid=d1.pk).first()
|
||||
|
|
@ -119,7 +120,7 @@ class Attributes(TestCase):
|
|||
def test_big_integer(self):
|
||||
"""Tests an integer larger than 32-bit a value."""
|
||||
big_num = 3147483647
|
||||
patient = Patient.objects.create(name='Jon')
|
||||
patient = Patient.objects.create(name="Jon")
|
||||
patient.eav.age = big_num
|
||||
|
||||
patient.save()
|
||||
|
|
@ -169,5 +170,7 @@ class TestAttributeModel(django.TestCase):
|
|||
def test_attribute_create_with_invalid_slug():
|
||||
with pytest.raises(ValidationError):
|
||||
Attribute.objects.create(
|
||||
name="Test Attribute", slug="123-invalid", datatype=Attribute.TYPE_TEXT
|
||||
name="Test Attribute",
|
||||
slug="123-invalid",
|
||||
datatype=Attribute.TYPE_TEXT,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class DataValidation(TestCase):
|
|||
p.save()
|
||||
|
||||
Attribute.objects.create(
|
||||
name='Weight', datatype=Attribute.TYPE_INT, required=True
|
||||
name='Weight', datatype=Attribute.TYPE_INT, required=True,
|
||||
)
|
||||
p.eav.age = 6
|
||||
self.assertRaises(ValidationError, p.save)
|
||||
|
|
@ -43,10 +43,10 @@ class DataValidation(TestCase):
|
|||
|
||||
def test_create_required_field(self):
|
||||
Attribute.objects.create(
|
||||
name='Weight', datatype=Attribute.TYPE_INT, required=True
|
||||
name='Weight', datatype=Attribute.TYPE_INT, required=True,
|
||||
)
|
||||
self.assertRaises(
|
||||
ValidationError, Patient.objects.create, name='Joe', eav__age=5
|
||||
ValidationError, Patient.objects.create, name='Joe', eav__age=5,
|
||||
)
|
||||
self.assertEqual(Patient.objects.count(), 0)
|
||||
self.assertEqual(Value.objects.count(), 0)
|
||||
|
|
@ -57,7 +57,7 @@ class DataValidation(TestCase):
|
|||
|
||||
def test_validation_error_create(self):
|
||||
self.assertRaises(
|
||||
ValidationError, Patient.objects.create, name='Joe', eav__age='df'
|
||||
ValidationError, Patient.objects.create, name='Joe', eav__age='df',
|
||||
)
|
||||
self.assertEqual(Patient.objects.count(), 0)
|
||||
self.assertEqual(Value.objects.count(), 0)
|
||||
|
|
@ -143,7 +143,7 @@ class DataValidation(TestCase):
|
|||
ynu.values.add(no)
|
||||
ynu.values.add(unkown)
|
||||
Attribute.objects.create(
|
||||
name='Fever', datatype=Attribute.TYPE_ENUM, enum_group=ynu
|
||||
name='Fever', datatype=Attribute.TYPE_ENUM, enum_group=ynu,
|
||||
)
|
||||
|
||||
p = Patient.objects.create(name='Joe')
|
||||
|
|
@ -199,5 +199,5 @@ class DataValidation(TestCase):
|
|||
p.eav.multi = "one;two;three"
|
||||
p.save()
|
||||
self.assertEqual(
|
||||
Patient.objects.get(pk=p.pk).eav.multi, ["one", "two", "three"]
|
||||
Patient.objects.get(pk=p.pk).eav.multi, ["one", "two", "three"],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import sys
|
||||
|
||||
import pytest
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from django.core.handlers.base import BaseHandler
|
||||
|
|
@ -8,9 +6,9 @@ from django.test import TestCase
|
|||
from django.test.client import RequestFactory
|
||||
|
||||
import eav
|
||||
from eav.admin import *
|
||||
from eav.admin import BaseEntityAdmin
|
||||
from eav.forms import BaseDynamicEntityForm
|
||||
from eav.models import Attribute
|
||||
from eav.models import Attribute, EnumGroup, EnumValue
|
||||
from test_project.models import ExampleModel, M2MModel, Patient
|
||||
|
||||
|
||||
|
|
@ -20,15 +18,7 @@ class MockRequest(RequestFactory):
|
|||
request = RequestFactory.request(self, **request)
|
||||
handler = BaseHandler()
|
||||
handler.load_middleware()
|
||||
# BaseHandler_request_middleware is not set in Django2.0
|
||||
# and removed in Django2.1
|
||||
if sys.version_info[0] < 2:
|
||||
for middleware_method in handler._request_middleware:
|
||||
if middleware_method(request):
|
||||
raise Exception(
|
||||
"Couldn't create request mock object - "
|
||||
"request middleware returned a response"
|
||||
)
|
||||
|
||||
return request
|
||||
|
||||
|
||||
|
|
@ -48,49 +38,51 @@ request.user = MockSuperUser()
|
|||
class PatientForm(ModelForm):
|
||||
class Meta:
|
||||
model = Patient
|
||||
fields = '__all__'
|
||||
fields = ("name", "email", "example")
|
||||
|
||||
|
||||
class PatientDynamicForm(BaseDynamicEntityForm):
|
||||
class Meta:
|
||||
model = Patient
|
||||
fields = '__all__'
|
||||
fields = ("name", "email", "example")
|
||||
|
||||
|
||||
class M2MModelForm(ModelForm):
|
||||
class Meta:
|
||||
model = M2MModel
|
||||
fields = '__all__'
|
||||
fields = ("name", "models")
|
||||
|
||||
|
||||
class Forms(TestCase):
|
||||
def setUp(self):
|
||||
eav.register(Patient)
|
||||
Attribute.objects.create(name='weight', datatype=Attribute.TYPE_FLOAT)
|
||||
Attribute.objects.create(name='color', datatype=Attribute.TYPE_TEXT)
|
||||
Attribute.objects.create(name="weight", datatype=Attribute.TYPE_FLOAT)
|
||||
Attribute.objects.create(name="color", datatype=Attribute.TYPE_TEXT)
|
||||
|
||||
self.female = EnumValue.objects.create(value='Female')
|
||||
self.male = EnumValue.objects.create(value='Male')
|
||||
gender_group = EnumGroup.objects.create(name='Gender')
|
||||
self.female = EnumValue.objects.create(value="Female")
|
||||
self.male = EnumValue.objects.create(value="Male")
|
||||
gender_group = EnumGroup.objects.create(name="Gender")
|
||||
gender_group.values.add(self.female, self.male)
|
||||
|
||||
Attribute.objects.create(
|
||||
name='gender', datatype=Attribute.TYPE_ENUM, enum_group=gender_group
|
||||
name="gender",
|
||||
datatype=Attribute.TYPE_ENUM,
|
||||
enum_group=gender_group,
|
||||
)
|
||||
|
||||
self.instance = Patient.objects.create(name='Jim Morrison')
|
||||
self.instance = Patient.objects.create(name="Jim Morrison")
|
||||
|
||||
def test_valid_submit(self):
|
||||
self.instance.eav.color = 'Blue'
|
||||
self.instance.eav.color = "Blue"
|
||||
form = PatientForm(self.instance.__dict__, instance=self.instance)
|
||||
jim = form.save()
|
||||
|
||||
self.assertEqual(jim.eav.color, 'Blue')
|
||||
self.assertEqual(jim.eav.color, "Blue")
|
||||
|
||||
def test_invalid_submit(self):
|
||||
form = PatientForm(dict(color='Blue'), instance=self.instance)
|
||||
form = PatientForm({"color": "Blue"}, instance=self.instance)
|
||||
with self.assertRaises(ValueError):
|
||||
jim = form.save()
|
||||
form.save()
|
||||
|
||||
def test_valid_enums(self):
|
||||
self.instance.eav.gender = self.female
|
||||
|
|
@ -100,41 +92,41 @@ class Forms(TestCase):
|
|||
self.assertEqual(rose.eav.gender, self.female)
|
||||
|
||||
def test_m2m(self):
|
||||
m2mmodel = M2MModel.objects.create(name='name')
|
||||
model = ExampleModel.objects.create(name='name')
|
||||
form = M2MModelForm(dict(name='Lorem', models=[model.pk]), instance=m2mmodel)
|
||||
m2mmodel = M2MModel.objects.create(name="name")
|
||||
model = ExampleModel.objects.create(name="name")
|
||||
form = M2MModelForm({"name": "Lorem", "models": [model.pk]}, instance=m2mmodel)
|
||||
form.save()
|
||||
self.assertEqual(len(m2mmodel.models.all()), 1)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@pytest.fixture
|
||||
def patient() -> Patient:
|
||||
"""Return an eav enabled Patient instance."""
|
||||
eav.register(Patient)
|
||||
return Patient.objects.create(name='Jim Morrison')
|
||||
return Patient.objects.create(name="Jim Morrison")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@pytest.fixture
|
||||
def create_attributes() -> None:
|
||||
"""Create some Attributes to use for testing."""
|
||||
Attribute.objects.create(name='weight', datatype=Attribute.TYPE_FLOAT)
|
||||
Attribute.objects.create(name='color', datatype=Attribute.TYPE_TEXT)
|
||||
Attribute.objects.create(name="weight", datatype=Attribute.TYPE_FLOAT)
|
||||
Attribute.objects.create(name="color", datatype=Attribute.TYPE_TEXT)
|
||||
|
||||
|
||||
@pytest.mark.django_db()
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
'csv_data, separator',
|
||||
("csv_data", "separator"),
|
||||
[
|
||||
('', ';'),
|
||||
('justone', ','),
|
||||
('one;two;three', ';'),
|
||||
('alpha,beta,gamma', ','),
|
||||
(None, ','),
|
||||
("", ";"),
|
||||
("justone", ","),
|
||||
("one;two;three", ";"),
|
||||
("alpha,beta,gamma", ","),
|
||||
(None, ","),
|
||||
],
|
||||
)
|
||||
def test_csvdynamicform(patient, csv_data, separator) -> None:
|
||||
"""Ensure that a TYPE_CSV field works correctly with forms."""
|
||||
Attribute.objects.create(name='csv', datatype=Attribute.TYPE_CSV)
|
||||
Attribute.objects.create(name="csv", datatype=Attribute.TYPE_CSV)
|
||||
patient.eav.csv = csv_data
|
||||
patient.save()
|
||||
patient.refresh_from_db()
|
||||
|
|
@ -143,7 +135,7 @@ def test_csvdynamicform(patient, csv_data, separator) -> None:
|
|||
patient.__dict__,
|
||||
instance=patient,
|
||||
)
|
||||
form.fields['csv'].separator = separator
|
||||
form.fields["csv"].separator = separator
|
||||
assert form.is_valid()
|
||||
jim = form.save()
|
||||
|
||||
|
|
@ -151,7 +143,7 @@ def test_csvdynamicform(patient, csv_data, separator) -> None:
|
|||
assert jim.eav.csv == expected_result
|
||||
|
||||
|
||||
@pytest.mark.django_db()
|
||||
@pytest.mark.django_db
|
||||
def test_csvdynamicform_empty(patient) -> None:
|
||||
"""Test to ensure an instance with no eav values is correct."""
|
||||
form = PatientDynamicForm(
|
||||
|
|
@ -162,29 +154,31 @@ def test_csvdynamicform_empty(patient) -> None:
|
|||
assert form.save()
|
||||
|
||||
|
||||
@pytest.mark.django_db()
|
||||
@pytest.mark.usefixtures('create_attributes')
|
||||
@pytest.mark.parametrize('define_fieldsets', (True, False))
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.usefixtures("create_attributes")
|
||||
@pytest.mark.parametrize("define_fieldsets", [True, False])
|
||||
def test_entity_admin_form(patient, define_fieldsets):
|
||||
"""Test the BaseEntityAdmin form setup and dynamic fieldsets handling."""
|
||||
admin = BaseEntityAdmin(Patient, AdminSite())
|
||||
admin.readonly_fields = ('email',)
|
||||
admin.readonly_fields = ("email",)
|
||||
admin.form = BaseDynamicEntityForm
|
||||
expected_fieldsets = 2
|
||||
|
||||
if define_fieldsets:
|
||||
# Use all fields in Patient model
|
||||
admin.fieldsets = (
|
||||
(None, {'fields': ['name', 'example']}),
|
||||
('Contact Info', {'fields': ['email']}),
|
||||
(None, {"fields": ["name", "example"]}),
|
||||
("Contact Info", {"fields": ["email"]}),
|
||||
)
|
||||
expected_fieldsets = 3
|
||||
|
||||
view = admin.change_view(request, str(patient.pk))
|
||||
|
||||
adminform = view.context_data['adminform']
|
||||
adminform = view.context_data["adminform"]
|
||||
|
||||
# Count the total fields in fieldsets
|
||||
total_fields = sum(
|
||||
len(fields_info['fields']) for _, fields_info in adminform.fieldsets
|
||||
len(fields_info["fields"]) for _, fields_info in adminform.fieldsets
|
||||
)
|
||||
|
||||
# 3 for 'name', 'email', 'example'
|
||||
|
|
@ -193,27 +187,27 @@ def test_entity_admin_form(patient, define_fieldsets):
|
|||
assert total_fields == expected_fields_count
|
||||
|
||||
# Ensure our fieldset count is correct
|
||||
if define_fieldsets:
|
||||
assert len(adminform.fieldsets) == 3
|
||||
else:
|
||||
assert len(adminform.fieldsets) == 2
|
||||
assert len(adminform.fieldsets) == expected_fieldsets
|
||||
|
||||
|
||||
@pytest.mark.django_db()
|
||||
@pytest.mark.django_db
|
||||
def test_entity_admin_form_no_attributes(patient):
|
||||
"""Test the BaseEntityAdmin form with no Attributes created."""
|
||||
admin = BaseEntityAdmin(Patient, AdminSite())
|
||||
admin.readonly_fields = ('email',)
|
||||
admin.readonly_fields = ("email",)
|
||||
admin.form = BaseDynamicEntityForm
|
||||
|
||||
# Only fields defined in Patient model
|
||||
expected_fields = 3
|
||||
|
||||
view = admin.change_view(request, str(patient.pk))
|
||||
|
||||
adminform = view.context_data['adminform']
|
||||
adminform = view.context_data["adminform"]
|
||||
|
||||
# Count the total fields in fieldsets
|
||||
total_fields = sum(
|
||||
len(fields_info['fields']) for _, fields_info in adminform.fieldsets
|
||||
len(fields_info["fields"]) for _, fields_info in adminform.fieldsets
|
||||
)
|
||||
|
||||
# 3 for 'name', 'email', 'example'
|
||||
assert total_fields == 3
|
||||
assert total_fields == expected_fields
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def test_generate_slug_uniqueness() -> None:
|
|||
generated_slugs[input_str] = slug
|
||||
|
||||
assert len(generated_slugs) == len(
|
||||
inputs
|
||||
inputs,
|
||||
), "Number of unique slugs doesn't match number of inputs"
|
||||
|
||||
|
||||
|
|
@ -72,5 +72,5 @@ def test_generate_slug_valid_identifier(input_str: str) -> None:
|
|||
slug = generate_slug(input_str)
|
||||
assert slug.isidentifier(), (
|
||||
f"Generated slug '{slug}' for input '{input_str}' "
|
||||
"is not a valid Python identifier"
|
||||
+ "is not a valid Python identifier"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from eav.models import Attribute, EnumGroup, EnumValue, Value
|
|||
from test_project.models import Patient
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@pytest.fixture
|
||||
def enumgroup(db):
|
||||
"""Sample `EnumGroup` object for testing."""
|
||||
test_group = EnumGroup.objects.create(name='Yes / No')
|
||||
|
|
@ -19,14 +19,14 @@ def enumgroup(db):
|
|||
|
||||
def test_enumgroup_display(enumgroup):
|
||||
"""Test repr() and str() of EnumGroup."""
|
||||
assert '<EnumGroup {0}>'.format(enumgroup.name) == repr(enumgroup)
|
||||
assert f'<EnumGroup {enumgroup.name}>' == repr(enumgroup)
|
||||
assert str(enumgroup) == str(enumgroup.name)
|
||||
|
||||
|
||||
def test_enumvalue_display(enumgroup):
|
||||
"""Test repr() and str() of EnumValue."""
|
||||
test_value = enumgroup.values.first()
|
||||
assert '<EnumValue {0}>'.format(test_value.value) == repr(test_value)
|
||||
assert f'<EnumValue {test_value.value}>' == repr(test_value)
|
||||
assert str(test_value) == test_value.value
|
||||
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ class MiscModels(TestCase):
|
|||
def test_attribute_help_text(self):
|
||||
desc = 'Patient Age'
|
||||
a = Attribute.objects.create(
|
||||
name='age', description=desc, datatype=Attribute.TYPE_INT
|
||||
name='age', description=desc, datatype=Attribute.TYPE_INT,
|
||||
)
|
||||
self.assertEqual(a.help_text, desc)
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ class MiscModels(TestCase):
|
|||
ynu.values.add(yes)
|
||||
ynu.values.add(no)
|
||||
Attribute.objects.create(
|
||||
name='is_patient', datatype=Attribute.TYPE_ENUM, enum_group=ynu
|
||||
name='is_patient', datatype=Attribute.TYPE_ENUM, enum_group=ynu,
|
||||
)
|
||||
eav.register(Patient)
|
||||
p = Patient.objects.create(name='Joe')
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
from django.test import TestCase
|
||||
|
||||
import eav
|
||||
from eav.models import Attribute, EnumGroup, EnumValue, Value
|
||||
from test_project.models import Patient
|
||||
import eav
|
||||
|
||||
|
||||
class ModelTest(TestCase):
|
||||
|
|
@ -38,7 +39,7 @@ class ModelTest(TestCase):
|
|||
enum_group = EnumGroup.objects.first()
|
||||
enum_group_natural_key = enum_group.natural_key()
|
||||
enum_group_retrieved_model = EnumGroup.objects.get_by_natural_key(
|
||||
*enum_group_natural_key
|
||||
*enum_group_natural_key,
|
||||
)
|
||||
self.assertEqual(enum_group_retrieved_model, enum_group)
|
||||
|
||||
|
|
@ -46,6 +47,6 @@ class ModelTest(TestCase):
|
|||
enum_value = EnumValue.objects.first()
|
||||
enum_value_natural_key = enum_value.natural_key()
|
||||
enum_value_retrieved_model = EnumValue.objects.get_by_natural_key(
|
||||
*enum_value_natural_key
|
||||
*enum_value_natural_key,
|
||||
)
|
||||
self.assertEqual(enum_value_retrieved_model, enum_value)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import uuid
|
||||
|
||||
import pytest
|
||||
from django.db import models
|
||||
|
||||
from eav.logic.object_pk import get_pk_format
|
||||
from eav.logic.object_pk import _DEFAULT_CHARFIELD_LEN, get_pk_format
|
||||
|
||||
|
||||
def test_get_uuid_primary_key(settings) -> None:
|
||||
|
|
@ -21,7 +20,7 @@ def test_get_char_primary_key(settings) -> None:
|
|||
assert isinstance(primary_field, models.CharField)
|
||||
assert primary_field.primary_key
|
||||
assert not primary_field.editable
|
||||
assert primary_field.max_length == 40
|
||||
assert primary_field.max_length == _DEFAULT_CHARFIELD_LEN
|
||||
|
||||
|
||||
def test_get_default_primary_key(settings) -> None:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
||||
from django.db.models import Q
|
||||
from django.db.utils import NotSupportedError
|
||||
|
|
@ -6,7 +9,7 @@ from django.test import TestCase
|
|||
import eav
|
||||
from eav.models import Attribute, EnumGroup, EnumValue, Value
|
||||
from eav.registry import EavConfig
|
||||
from test_project.models import Encounter, Patient, ExampleModel
|
||||
from test_project.models import Encounter, ExampleModel, Patient
|
||||
|
||||
|
||||
class Queries(TestCase):
|
||||
|
|
@ -14,32 +17,34 @@ class Queries(TestCase):
|
|||
eav.register(Encounter)
|
||||
eav.register(Patient)
|
||||
|
||||
Attribute.objects.create(name='age', datatype=Attribute.TYPE_INT)
|
||||
Attribute.objects.create(name='height', datatype=Attribute.TYPE_FLOAT)
|
||||
Attribute.objects.create(name='weight', datatype=Attribute.TYPE_FLOAT)
|
||||
Attribute.objects.create(name='city', datatype=Attribute.TYPE_TEXT)
|
||||
Attribute.objects.create(name='country', datatype=Attribute.TYPE_TEXT)
|
||||
Attribute.objects.create(name='extras', datatype=Attribute.TYPE_JSON)
|
||||
Attribute.objects.create(name='illness', datatype=Attribute.TYPE_CSV)
|
||||
Attribute.objects.create(name="age", datatype=Attribute.TYPE_INT)
|
||||
Attribute.objects.create(name="height", datatype=Attribute.TYPE_FLOAT)
|
||||
Attribute.objects.create(name="weight", datatype=Attribute.TYPE_FLOAT)
|
||||
Attribute.objects.create(name="city", datatype=Attribute.TYPE_TEXT)
|
||||
Attribute.objects.create(name="country", datatype=Attribute.TYPE_TEXT)
|
||||
Attribute.objects.create(name="extras", datatype=Attribute.TYPE_JSON)
|
||||
Attribute.objects.create(name="illness", datatype=Attribute.TYPE_CSV)
|
||||
|
||||
self.yes = EnumValue.objects.create(value='yes')
|
||||
self.no = EnumValue.objects.create(value='no')
|
||||
self.unknown = EnumValue.objects.create(value='unknown')
|
||||
self.yes = EnumValue.objects.create(value="yes")
|
||||
self.no = EnumValue.objects.create(value="no")
|
||||
self.unknown = EnumValue.objects.create(value="unknown")
|
||||
|
||||
ynu = EnumGroup.objects.create(name='Yes / No / Unknown')
|
||||
ynu = EnumGroup.objects.create(name="Yes / No / Unknown")
|
||||
ynu.values.add(self.yes)
|
||||
ynu.values.add(self.no)
|
||||
ynu.values.add(self.unknown)
|
||||
|
||||
Attribute.objects.create(
|
||||
name='fever', datatype=Attribute.TYPE_ENUM, enum_group=ynu
|
||||
name="fever",
|
||||
datatype=Attribute.TYPE_ENUM,
|
||||
enum_group=ynu,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
eav.unregister(Encounter)
|
||||
eav.unregister(Patient)
|
||||
|
||||
def init_data(self):
|
||||
def init_data(self) -> None:
|
||||
yes = self.yes
|
||||
no = self.no
|
||||
|
||||
|
|
@ -47,24 +52,24 @@ class Queries(TestCase):
|
|||
# Name, age, fever,
|
||||
# city, country, extras
|
||||
# possible illness
|
||||
['Anne', 3, no, 'New York', 'USA', {"chills": "yes"}, "cold"],
|
||||
['Bob', 15, no, 'Bamako', 'Mali', {}, ""],
|
||||
["Anne", 3, no, "New York", "USA", {"chills": "yes"}, "cold"],
|
||||
["Bob", 15, no, "Bamako", "Mali", {}, ""],
|
||||
[
|
||||
'Cyrill',
|
||||
"Cyrill",
|
||||
15,
|
||||
yes,
|
||||
'Kisumu',
|
||||
'Kenya',
|
||||
"Kisumu",
|
||||
"Kenya",
|
||||
{"chills": "yes", "headache": "no"},
|
||||
"flu",
|
||||
],
|
||||
['Daniel', 3, no, 'Nice', 'France', {"headache": "yes"}, "cold"],
|
||||
["Daniel", 3, no, "Nice", "France", {"headache": "yes"}, "cold"],
|
||||
[
|
||||
'Eugene',
|
||||
"Eugene",
|
||||
2,
|
||||
yes,
|
||||
'France',
|
||||
'Nice',
|
||||
"France",
|
||||
"Nice",
|
||||
{"chills": "no", "headache": "yes"},
|
||||
"flu;cold",
|
||||
],
|
||||
|
|
@ -82,26 +87,26 @@ class Queries(TestCase):
|
|||
)
|
||||
|
||||
def test_get_or_create_with_eav(self):
|
||||
Patient.objects.get_or_create(name='Bob', eav__age=5)
|
||||
Patient.objects.get_or_create(name="Bob", eav__age=5)
|
||||
self.assertEqual(Patient.objects.count(), 1)
|
||||
self.assertEqual(Value.objects.count(), 1)
|
||||
Patient.objects.get_or_create(name='Bob', eav__age=5)
|
||||
Patient.objects.get_or_create(name="Bob", eav__age=5)
|
||||
self.assertEqual(Patient.objects.count(), 1)
|
||||
self.assertEqual(Value.objects.count(), 1)
|
||||
Patient.objects.get_or_create(name='Bob', eav__age=6)
|
||||
Patient.objects.get_or_create(name="Bob", eav__age=6)
|
||||
self.assertEqual(Patient.objects.count(), 2)
|
||||
self.assertEqual(Value.objects.count(), 2)
|
||||
|
||||
def test_get_or_create_with_defaults(self):
|
||||
"""Tests EntityManager.get_or_create() with defaults keyword."""
|
||||
city_name = 'Tokyo'
|
||||
email = 'mari@test.com'
|
||||
city_name = "Tokyo"
|
||||
email = "mari@test.com"
|
||||
p1, _ = Patient.objects.get_or_create(
|
||||
name='Mari',
|
||||
name="Mari",
|
||||
eav__age=27,
|
||||
defaults={
|
||||
'email': email,
|
||||
'eav__city': city_name,
|
||||
"email": email,
|
||||
"eav__city": city_name,
|
||||
},
|
||||
)
|
||||
assert Patient.objects.count() == 1
|
||||
|
|
@ -109,175 +114,258 @@ class Queries(TestCase):
|
|||
assert p1.eav.city == city_name
|
||||
|
||||
def test_get_with_eav(self):
|
||||
p1, _ = Patient.objects.get_or_create(name='Bob', eav__age=6)
|
||||
p1, _ = Patient.objects.get_or_create(name="Bob", eav__age=6)
|
||||
self.assertEqual(Patient.objects.get(eav__age=6), p1)
|
||||
|
||||
Patient.objects.create(name='Fred', eav__age=6)
|
||||
Patient.objects.create(name="Fred", eav__age=6)
|
||||
self.assertRaises(
|
||||
MultipleObjectsReturned, lambda: Patient.objects.get(eav__age=6)
|
||||
MultipleObjectsReturned,
|
||||
lambda: Patient.objects.get(eav__age=6),
|
||||
)
|
||||
|
||||
def test_filtering_on_normal_and_eav_fields(self):
|
||||
def test_no_results_for_contradictory_conditions(self) -> None:
|
||||
"""Test that contradictory conditions return no results."""
|
||||
self.init_data()
|
||||
|
||||
# Check number of objects in DB.
|
||||
self.assertEqual(Patient.objects.count(), 5)
|
||||
self.assertEqual(Value.objects.count(), 29)
|
||||
|
||||
# Nobody
|
||||
q1 = Q(eav__fever=self.yes) & Q(eav__fever=self.no)
|
||||
p = Patient.objects.filter(q1)
|
||||
self.assertEqual(p.count(), 0)
|
||||
|
||||
# Anne, Daniel
|
||||
# Should return no patients due to contradictory conditions
|
||||
assert p.count() == 0
|
||||
|
||||
def test_filtering_on_numeric_eav_fields(self) -> None:
|
||||
"""Test filtering on numeric EAV fields."""
|
||||
self.init_data()
|
||||
q1 = Q(eav__age__gte=3) # Everyone except Eugene
|
||||
q2 = Q(eav__age__lt=15) # Anne, Daniel, Eugene
|
||||
p = Patient.objects.filter(q2 & q1)
|
||||
self.assertEqual(p.count(), 2)
|
||||
|
||||
# Anne
|
||||
q1 = Q(eav__city__contains='Y') & Q(eav__fever='no')
|
||||
# Should return Anne and Daniel
|
||||
assert p.count() == 2
|
||||
|
||||
def test_filtering_on_text_and_boolean_eav_fields(self) -> None:
|
||||
"""Test filtering on text and boolean EAV fields."""
|
||||
self.init_data()
|
||||
q1 = Q(eav__city__contains="Y") & Q(eav__fever="no")
|
||||
q2 = Q(eav__age=3)
|
||||
p = Patient.objects.filter(q1 & q2)
|
||||
self.assertEqual(p.count(), 1)
|
||||
|
||||
# Anne
|
||||
q1 = Q(eav__city__contains='Y') & Q(eav__fever=self.no)
|
||||
# Should return only Anne
|
||||
assert p.count() == 1
|
||||
|
||||
def test_filtering_with_enum_eav_fields(self) -> None:
|
||||
"""Test filtering with enum EAV fields."""
|
||||
self.init_data()
|
||||
q1 = Q(eav__city__contains="Y") & Q(eav__fever=self.no)
|
||||
q2 = Q(eav__age=3)
|
||||
p = Patient.objects.filter(q1 & q2)
|
||||
self.assertEqual(p.count(), 1)
|
||||
|
||||
# Anne, Daniel
|
||||
q1 = Q(eav__city__contains='Y', eav__fever=self.no)
|
||||
q2 = Q(eav__city='Nice')
|
||||
# Should return only Anne
|
||||
assert p.count() == 1
|
||||
|
||||
def test_complex_query_with_or_conditions(self) -> None:
|
||||
"""Test complex query with OR conditions."""
|
||||
self.init_data()
|
||||
q1 = Q(eav__city__contains="Y", eav__fever=self.no)
|
||||
q2 = Q(eav__city="Nice")
|
||||
q3 = Q(eav__age=3)
|
||||
p = Patient.objects.filter((q1 | q2) & q3)
|
||||
self.assertEqual(p.count(), 2)
|
||||
|
||||
# Everyone
|
||||
# Should return Anne and Daniel
|
||||
assert p.count() == 2
|
||||
|
||||
def test_filtering_with_multiple_enum_values(self) -> None:
|
||||
"""Test filtering with multiple enum values."""
|
||||
self.init_data()
|
||||
q1 = Q(eav__fever=self.no) | Q(eav__fever=self.yes)
|
||||
p = Patient.objects.filter(q1)
|
||||
self.assertEqual(p.count(), 5)
|
||||
|
||||
# Anne, Bob, Daniel
|
||||
# Should return all patients
|
||||
assert p.count() == 5
|
||||
|
||||
def test_complex_query_with_multiple_conditions(self) -> None:
|
||||
"""Test complex query with multiple conditions."""
|
||||
self.init_data()
|
||||
q1 = Q(eav__fever=self.no) # Anne, Bob, Daniel
|
||||
q2 = Q(eav__fever=self.yes) # Cyrill, Eugene
|
||||
q3 = Q(eav__country__contains='e') # Cyrill, Daniel, Eugene
|
||||
q3 = Q(eav__country__contains="e") # Cyrill, Daniel, Eugene
|
||||
q4 = q2 & q3 # Cyrill, Daniel, Eugene
|
||||
q5 = (q1 | q4) & q1 # Anne, Bob, Daniel
|
||||
p = Patient.objects.filter(q5)
|
||||
self.assertEqual(p.count(), 3)
|
||||
|
||||
# Everyone except Anne
|
||||
q1 = Q(eav__city__contains='Y')
|
||||
# Should return Anne, Bob, and Daniel
|
||||
assert p.count() == 3
|
||||
|
||||
def test_excluding_with_eav_fields(self) -> None:
|
||||
"""Test excluding with EAV fields."""
|
||||
self.init_data()
|
||||
q1 = Q(eav__city__contains="Y")
|
||||
p = Patient.objects.exclude(q1)
|
||||
self.assertEqual(p.count(), 4)
|
||||
|
||||
# Anne, Bob, Daniel
|
||||
q1 = Q(eav__city__contains='Y')
|
||||
# Should return all patients except Anne
|
||||
assert p.count() == 4
|
||||
|
||||
def test_filtering_with_or_conditions(self) -> None:
|
||||
"""Test filtering with OR conditions."""
|
||||
self.init_data()
|
||||
q1 = Q(eav__city__contains="Y")
|
||||
q2 = Q(eav__fever=self.no)
|
||||
q3 = q1 | q2
|
||||
p = Patient.objects.filter(q3)
|
||||
self.assertEqual(p.count(), 3)
|
||||
|
||||
# Anne, Daniel
|
||||
# Should return Anne, Bob, and Daniel
|
||||
assert p.count() == 3
|
||||
|
||||
def test_filtering_on_single_eav_field(self) -> None:
|
||||
"""Test filtering on a single EAV field."""
|
||||
self.init_data()
|
||||
q1 = Q(eav__age=3)
|
||||
p = Patient.objects.filter(q1)
|
||||
self.assertEqual(p.count(), 2)
|
||||
|
||||
# Eugene
|
||||
q1 = Q(name__contains='E', eav__fever=self.yes)
|
||||
# Should return Anne and Daniel
|
||||
assert p.count() == 2
|
||||
|
||||
def test_combining_normal_and_eav_fields(self) -> None:
|
||||
"""Test combining normal and EAV fields in a query."""
|
||||
self.init_data()
|
||||
q1 = Q(name__contains="E", eav__fever=self.yes)
|
||||
p = Patient.objects.filter(q1)
|
||||
self.assertEqual(p.count(), 1)
|
||||
|
||||
# Extras: Chills
|
||||
# Without
|
||||
# Should return only Eugene
|
||||
assert p.count() == 1
|
||||
|
||||
def test_filtering_on_json_eav_field(self) -> None:
|
||||
"""Test filtering on JSON EAV field."""
|
||||
self.init_data()
|
||||
q1 = Q(eav__extras__has_key="chills")
|
||||
p = Patient.objects.exclude(q1)
|
||||
self.assertEqual(p.count(), 2)
|
||||
|
||||
# With
|
||||
# Should return patients without 'chills' in extras
|
||||
assert p.count() == 2
|
||||
|
||||
q1 = Q(eav__extras__has_key="chills")
|
||||
p = Patient.objects.filter(q1)
|
||||
self.assertEqual(p.count(), 3)
|
||||
|
||||
# No chills
|
||||
# Should return patients with 'chills' in extras
|
||||
assert p.count() == 3
|
||||
|
||||
q1 = Q(eav__extras__chills="no")
|
||||
p = Patient.objects.filter(q1)
|
||||
self.assertEqual(p.count(), 1)
|
||||
|
||||
# Has chills
|
||||
# Should return patients with 'chills' set to 'no'
|
||||
assert p.count() == 1
|
||||
|
||||
q1 = Q(eav__extras__chills="yes")
|
||||
p = Patient.objects.filter(q1)
|
||||
self.assertEqual(p.count(), 2)
|
||||
|
||||
# Extras: Empty
|
||||
# Yes
|
||||
# Should return patients with 'chills' set to 'yes'
|
||||
assert p.count() == 2
|
||||
|
||||
def test_filtering_on_empty_json_eav_field(self) -> None:
|
||||
"""Test filtering on empty JSON EAV field."""
|
||||
self.init_data()
|
||||
q1 = Q(eav__extras={})
|
||||
p = Patient.objects.filter(q1)
|
||||
self.assertEqual(p.count(), 1)
|
||||
|
||||
# No
|
||||
# Should return patients with empty extras
|
||||
assert p.count() == 1
|
||||
|
||||
q1 = Q(eav__extras={})
|
||||
p = Patient.objects.exclude(q1)
|
||||
self.assertEqual(p.count(), 4)
|
||||
|
||||
# Illness:
|
||||
# Cold
|
||||
# Should return patients with non-empty extras
|
||||
assert p.count() == 4
|
||||
|
||||
def test_filtering_on_text_eav_field_with_icontains(self) -> None:
|
||||
"""Test filtering on text EAV field with icontains."""
|
||||
self.init_data()
|
||||
q1 = Q(eav__illness__icontains="cold")
|
||||
p = Patient.objects.exclude(q1)
|
||||
self.assertEqual(p.count(), 2)
|
||||
|
||||
# Flu
|
||||
# Should return patients without 'cold' in illness
|
||||
assert p.count() == 2
|
||||
|
||||
q1 = Q(eav__illness__icontains="flu")
|
||||
p = Patient.objects.exclude(q1)
|
||||
self.assertEqual(p.count(), 3)
|
||||
|
||||
# Empty
|
||||
# Should return patients without 'flu' in illness
|
||||
assert p.count() == 3
|
||||
|
||||
def test_filtering_on_null_eav_field(self) -> None:
|
||||
"""Test filtering on null EAV field."""
|
||||
self.init_data()
|
||||
q1 = Q(eav__illness__isnull=False)
|
||||
p = Patient.objects.filter(~q1)
|
||||
self.assertEqual(p.count(), 1)
|
||||
|
||||
def _order(self, ordering):
|
||||
# Should return patients with null illness
|
||||
assert p.count() == 1
|
||||
|
||||
def _order(self, ordering) -> list[str]:
|
||||
query = Patient.objects.all().order_by(*ordering)
|
||||
return list(query.values_list('name', flat=True))
|
||||
return list(query.values_list("name", flat=True))
|
||||
|
||||
def assert_order_by_results(self, eav_attr='eav'):
|
||||
self.assertEqual(
|
||||
['Bob', 'Eugene', 'Cyrill', 'Anne', 'Daniel'],
|
||||
self._order(['%s__city' % eav_attr]),
|
||||
)
|
||||
def assert_order_by_results(self, eav_attr="eav") -> None:
|
||||
"""Test the ordering functionality of EAV attributes."""
|
||||
# Ordering by a single EAV attribute
|
||||
assert self._order([f"{eav_attr}__city"]) == [
|
||||
"Bob",
|
||||
"Eugene",
|
||||
"Cyrill",
|
||||
"Anne",
|
||||
"Daniel",
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
['Eugene', 'Anne', 'Daniel', 'Bob', 'Cyrill'],
|
||||
self._order(['%s__age' % eav_attr, '%s__city' % eav_attr]),
|
||||
)
|
||||
# Ordering by multiple EAV attributes
|
||||
assert self._order([f"{eav_attr}__age", f"{eav_attr}__city"]) == [
|
||||
"Eugene",
|
||||
"Anne",
|
||||
"Daniel",
|
||||
"Bob",
|
||||
"Cyrill",
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
['Eugene', 'Cyrill', 'Anne', 'Daniel', 'Bob'],
|
||||
self._order(['%s__fever' % eav_attr, '%s__age' % eav_attr]),
|
||||
)
|
||||
# Ordering by EAV attributes with different data types
|
||||
assert self._order([f"{eav_attr}__fever", f"{eav_attr}__age"]) == [
|
||||
"Eugene",
|
||||
"Cyrill",
|
||||
"Anne",
|
||||
"Daniel",
|
||||
"Bob",
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
['Eugene', 'Cyrill', 'Daniel', 'Bob', 'Anne'],
|
||||
self._order(['%s__fever' % eav_attr, '-name']),
|
||||
)
|
||||
# Combining EAV and regular model field ordering
|
||||
assert self._order([f"{eav_attr}__fever", "-name"]) == [
|
||||
"Eugene",
|
||||
"Cyrill",
|
||||
"Daniel",
|
||||
"Bob",
|
||||
"Anne",
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
['Eugene', 'Daniel', 'Cyrill', 'Bob', 'Anne'],
|
||||
self._order(['-name', '%s__age' % eav_attr]),
|
||||
)
|
||||
# Mixing regular and EAV field ordering
|
||||
assert self._order(["-name", f"{eav_attr}__age"]) == [
|
||||
"Eugene",
|
||||
"Daniel",
|
||||
"Cyrill",
|
||||
"Bob",
|
||||
"Anne",
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
['Anne', 'Bob', 'Cyrill', 'Daniel', 'Eugene'],
|
||||
self._order(['example__name']),
|
||||
)
|
||||
# Ordering by a related model field
|
||||
assert self._order(["example__name"]) == [
|
||||
"Anne",
|
||||
"Bob",
|
||||
"Cyrill",
|
||||
"Daniel",
|
||||
"Eugene",
|
||||
]
|
||||
|
||||
with self.assertRaises(NotSupportedError):
|
||||
Patient.objects.all().order_by('%s__first__second' % eav_attr)
|
||||
# Error handling for unsupported nested EAV attributes
|
||||
with pytest.raises(NotSupportedError):
|
||||
Patient.objects.all().order_by(f"{eav_attr}__first__second")
|
||||
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
Patient.objects.all().order_by('%s__nonsense' % eav_attr)
|
||||
# Error handling for non-existent EAV attributes
|
||||
with pytest.raises(ObjectDoesNotExist):
|
||||
Patient.objects.all().order_by(f"{eav_attr}__nonsense")
|
||||
|
||||
def test_order_by(self):
|
||||
self.init_data()
|
||||
|
|
@ -291,11 +379,11 @@ class Queries(TestCase):
|
|||
self.init_data()
|
||||
eav.unregister(Patient)
|
||||
eav.register(Patient, config_cls=CustomConfig)
|
||||
self.assert_order_by_results(eav_attr='data')
|
||||
self.assert_order_by_results(eav_attr="data")
|
||||
|
||||
def test_fk_filter(self):
|
||||
e = ExampleModel.objects.create(name='test1')
|
||||
p = Patient.objects.get_or_create(name='Beth', example=e)[0]
|
||||
e = ExampleModel.objects.create(name="test1")
|
||||
p = Patient.objects.get_or_create(name="Beth", example=e)[0]
|
||||
c = ExampleModel.objects.filter(patient=p)
|
||||
self.assertEqual(c.count(), 1)
|
||||
|
||||
|
|
@ -310,12 +398,12 @@ class Queries(TestCase):
|
|||
|
||||
# Use the filter method with 3 EAV attribute conditions
|
||||
patients = Patient.objects.filter(
|
||||
name='Anne',
|
||||
name="Anne",
|
||||
eav__age=3,
|
||||
eav__illness='cold',
|
||||
eav__fever='no',
|
||||
eav__illness="cold",
|
||||
eav__fever="no",
|
||||
)
|
||||
|
||||
# Assert that the expected patient is returned
|
||||
self.assertEqual(len(patients), 1)
|
||||
self.assertEqual(patients[0].name, 'Anne')
|
||||
self.assertEqual(patients[0].name, "Anne")
|
||||
|
|
|
|||
|
|
@ -22,72 +22,72 @@ class RegistryTests(TestCase):
|
|||
|
||||
def register_encounter(self):
|
||||
class EncounterEav(EavConfig):
|
||||
manager_attr = 'eav_objects'
|
||||
eav_attr = 'eav_field'
|
||||
generic_relation_attr = 'encounter_eav_values'
|
||||
generic_relation_related_name = 'encounters'
|
||||
manager_attr = "eav_objects"
|
||||
eav_attr = "eav_field"
|
||||
generic_relation_attr = "encounter_eav_values"
|
||||
generic_relation_related_name = "encounters"
|
||||
|
||||
@classmethod
|
||||
def get_attributes(cls):
|
||||
return 'testing'
|
||||
return "testing"
|
||||
|
||||
eav.register(Encounter, EncounterEav)
|
||||
|
||||
def test_registering_with_defaults(self):
|
||||
eav.register(Patient)
|
||||
self.assertTrue(hasattr(Patient, '_eav_config_cls'))
|
||||
self.assertEqual(Patient._eav_config_cls.manager_attr, 'objects')
|
||||
self.assertTrue(hasattr(Patient, "_eav_config_cls"))
|
||||
self.assertEqual(Patient._eav_config_cls.manager_attr, "objects")
|
||||
self.assertFalse(Patient._eav_config_cls.manager_only)
|
||||
self.assertEqual(Patient._eav_config_cls.eav_attr, 'eav')
|
||||
self.assertEqual(Patient._eav_config_cls.generic_relation_attr, 'eav_values')
|
||||
self.assertEqual(Patient._eav_config_cls.eav_attr, "eav")
|
||||
self.assertEqual(Patient._eav_config_cls.generic_relation_attr, "eav_values")
|
||||
self.assertEqual(Patient._eav_config_cls.generic_relation_related_name, None)
|
||||
eav.unregister(Patient)
|
||||
|
||||
def test_registering_overriding_defaults(self):
|
||||
eav.register(Patient)
|
||||
self.register_encounter()
|
||||
self.assertTrue(hasattr(Patient, '_eav_config_cls'))
|
||||
self.assertEqual(Patient._eav_config_cls.manager_attr, 'objects')
|
||||
self.assertEqual(Patient._eav_config_cls.eav_attr, 'eav')
|
||||
self.assertTrue(hasattr(Patient, "_eav_config_cls"))
|
||||
self.assertEqual(Patient._eav_config_cls.manager_attr, "objects")
|
||||
self.assertEqual(Patient._eav_config_cls.eav_attr, "eav")
|
||||
|
||||
self.assertTrue(hasattr(Encounter, '_eav_config_cls'))
|
||||
self.assertEqual(Encounter._eav_config_cls.get_attributes(), 'testing')
|
||||
self.assertEqual(Encounter._eav_config_cls.manager_attr, 'eav_objects')
|
||||
self.assertEqual(Encounter._eav_config_cls.eav_attr, 'eav_field')
|
||||
self.assertTrue(hasattr(Encounter, "_eav_config_cls"))
|
||||
self.assertEqual(Encounter._eav_config_cls.get_attributes(), "testing")
|
||||
self.assertEqual(Encounter._eav_config_cls.manager_attr, "eav_objects")
|
||||
self.assertEqual(Encounter._eav_config_cls.eav_attr, "eav_field")
|
||||
eav.unregister(Patient)
|
||||
eav.unregister(Encounter)
|
||||
|
||||
def test_registering_via_decorator_with_defaults(self):
|
||||
self.assertTrue(hasattr(ExampleModel, '_eav_config_cls'))
|
||||
self.assertEqual(ExampleModel._eav_config_cls.manager_attr, 'objects')
|
||||
self.assertEqual(ExampleModel._eav_config_cls.eav_attr, 'eav')
|
||||
self.assertTrue(hasattr(ExampleModel, "_eav_config_cls"))
|
||||
self.assertEqual(ExampleModel._eav_config_cls.manager_attr, "objects")
|
||||
self.assertEqual(ExampleModel._eav_config_cls.eav_attr, "eav")
|
||||
|
||||
def test_register_via_metaclass_with_defaults(self):
|
||||
self.assertTrue(hasattr(ExampleMetaclassModel, '_eav_config_cls'))
|
||||
self.assertEqual(ExampleMetaclassModel._eav_config_cls.manager_attr, 'objects')
|
||||
self.assertEqual(ExampleMetaclassModel._eav_config_cls.eav_attr, 'eav')
|
||||
self.assertTrue(hasattr(ExampleMetaclassModel, "_eav_config_cls"))
|
||||
self.assertEqual(ExampleMetaclassModel._eav_config_cls.manager_attr, "objects")
|
||||
self.assertEqual(ExampleMetaclassModel._eav_config_cls.eav_attr, "eav")
|
||||
|
||||
def test_unregistering(self):
|
||||
old_mgr = Patient.objects
|
||||
eav.register(Patient)
|
||||
self.assertTrue(Patient.objects.__class__.__name__ == 'EntityManager')
|
||||
self.assertTrue(Patient.objects.__class__.__name__ == "EntityManager")
|
||||
eav.unregister(Patient)
|
||||
self.assertFalse(Patient.objects.__class__.__name__ == 'EntityManager')
|
||||
self.assertFalse(Patient.objects.__class__.__name__ == "EntityManager")
|
||||
self.assertEqual(Patient.objects, old_mgr)
|
||||
self.assertFalse(hasattr(Patient, '_eav_config_cls'))
|
||||
self.assertFalse(hasattr(Patient, "_eav_config_cls"))
|
||||
|
||||
def test_unregistering_via_decorator(self):
|
||||
self.assertTrue(ExampleModel.objects.__class__.__name__ == 'EntityManager')
|
||||
self.assertTrue(ExampleModel.objects.__class__.__name__ == "EntityManager")
|
||||
eav.unregister(ExampleModel)
|
||||
self.assertFalse(ExampleModel.objects.__class__.__name__ == 'EntityManager')
|
||||
self.assertFalse(ExampleModel.objects.__class__.__name__ == "EntityManager")
|
||||
|
||||
def test_unregistering_via_metaclass(self):
|
||||
self.assertTrue(
|
||||
ExampleMetaclassModel.objects.__class__.__name__ == 'EntityManager'
|
||||
ExampleMetaclassModel.objects.__class__.__name__ == "EntityManager",
|
||||
)
|
||||
eav.unregister(ExampleMetaclassModel)
|
||||
self.assertFalse(
|
||||
ExampleMetaclassModel.objects.__class__.__name__ == 'EntityManager'
|
||||
ExampleMetaclassModel.objects.__class__.__name__ == "EntityManager",
|
||||
)
|
||||
|
||||
def test_unregistering_unregistered_model_proceeds_silently(self):
|
||||
|
|
@ -98,10 +98,10 @@ class RegistryTests(TestCase):
|
|||
eav.register(Patient)
|
||||
|
||||
def test_doesnt_register_nonmodel(self):
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(TypeError):
|
||||
|
||||
@eav.decorators.register_eav()
|
||||
class Foo(object):
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
def test_model_without_local_managers(self):
|
||||
|
|
@ -132,5 +132,5 @@ def test_default_manager_stays() -> None:
|
|||
|
||||
# Explicity test this as for our test setup, we want to have a state where
|
||||
# the default manager is 'objects'
|
||||
assert instance_meta.default_manager.name == 'objects'
|
||||
assert instance_meta.default_manager.name == "objects"
|
||||
assert len(instance_meta.managers) == 2
|
||||
|
|
|
|||
Loading…
Reference in a new issue