From 413cdf74734b25a257f236fa316427fe5a4b40ad Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Wed, 6 Sep 2023 14:24:49 +0100 Subject: [PATCH 01/34] add uuid configuration for id generation of eav models --- eav/logic/object_pk.py | 14 ++++++++++++++ eav/models.py | 11 +++++++++++ test_project/settings.py | 3 +++ 3 files changed, 28 insertions(+) create mode 100644 eav/logic/object_pk.py diff --git a/eav/logic/object_pk.py b/eav/logic/object_pk.py new file mode 100644 index 00000000..22411356 --- /dev/null +++ b/eav/logic/object_pk.py @@ -0,0 +1,14 @@ +import uuid + +from functools import partial +from django.db import models +from django.conf import settings + + +def get_pk_format(): + PrimaryField = partial(models.BigAutoField, primary_key=True, editable=False) + if settings.PRIMARY_KEY_TYPE == "UUID": + PrimaryField = partial( + models.UUIDField, primary_key=True, editable=False, default=uuid.uuid4 + ) + return PrimaryField() diff --git a/eav/models.py b/eav/models.py index 41c6b547..e122bf45 100644 --- a/eav/models.py +++ b/eav/models.py @@ -25,6 +25,7 @@ from eav.fields import CSVField, EavDatatypeField from eav.logic.entity_pk import get_entity_pk_type from eav.logic.slug import SLUGFIELD_MAX_LENGTH, generate_slug +from eav.logic.object_pk import get_pk_format from eav.validators import ( validate_bool, validate_csv, @@ -77,6 +78,9 @@ class Meta: verbose_name = _('EnumValue') verbose_name_plural = _('EnumValues') + # added + id = get_pk_format() + value = models.CharField( _('Value'), db_index=True, @@ -105,6 +109,8 @@ class EnumGroup(models.Model): class Meta: verbose_name = _('EnumGroup') verbose_name_plural = _('EnumGroups') + # added + id = get_pk_format() name = models.CharField( unique=True, @@ -205,6 +211,8 @@ class Meta: ) # Core attributes + # added + id = get_pk_format() datatype = EavDatatypeField( choices=DATATYPE_CHOICES, @@ -439,6 +447,9 @@ class Meta: verbose_name = _('Value') verbose_name_plural = _('Values') + # added + id = get_pk_format() + # Direct foreign keys attribute = models.ForeignKey( Attribute, diff --git a/test_project/settings.py b/test_project/settings.py index 5b66c076..1e4be098 100644 --- a/test_project/settings.py +++ b/test_project/settings.py @@ -32,6 +32,9 @@ 'eav', ] + +PRIMARY_KEY_TYPE = "UUID" + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', From 8977495d080c3603f07e9865198f7fbc97e16f94 Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Wed, 6 Sep 2023 15:40:13 +0100 Subject: [PATCH 02/34] add charfield possibility to eav id models field --- eav/logic/object_pk.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/eav/logic/object_pk.py b/eav/logic/object_pk.py index 22411356..e5c76036 100644 --- a/eav/logic/object_pk.py +++ b/eav/logic/object_pk.py @@ -6,9 +6,14 @@ def get_pk_format(): - PrimaryField = partial(models.BigAutoField, primary_key=True, editable=False) - if settings.PRIMARY_KEY_TYPE == "UUID": + if settings.PRIMARY_KEY_FIELD == "UUIDField": PrimaryField = partial( models.UUIDField, primary_key=True, editable=False, default=uuid.uuid4 ) + elif settings.PRIMARY_KEY_FIELD == "CharField": + PrimaryField = partial( + models.CharField, primary_key=True, editable=False, max_length=40 + ) + else: + PrimaryField = partial(models.BigAutoField, primary_key=True, editable=False) return PrimaryField() From de7fb7963c02070a7012bc4e21c1dab1ab9ea15b Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Wed, 6 Sep 2023 16:54:16 +0100 Subject: [PATCH 03/34] feat: natural key handler added to models except value model --- eav/logic/model_managers.py | 15 +++++++++++++++ eav/logic/object_pk.py | 4 ++-- eav/models.py | 28 +++++++++++++++++++++++++++- test_project/settings.py | 7 ++++--- 4 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 eav/logic/model_managers.py diff --git a/eav/logic/model_managers.py b/eav/logic/model_managers.py new file mode 100644 index 00000000..b7dcca79 --- /dev/null +++ b/eav/logic/model_managers.py @@ -0,0 +1,15 @@ +from django.db import models + +class EnumValueManager(models.Manager): + def get_by_natural_key(self, value): + return self.get(value=value) + + +class EnumGroupManager(models.Manager): + def get_by_natural_key(self, name): + return self.get(name=name) + + +class AttributeManager(models.Manager): + def get_by_natural_key(self, name, slug): + return self.get(name=name, slug=slug) diff --git a/eav/logic/object_pk.py b/eav/logic/object_pk.py index e5c76036..70647373 100644 --- a/eav/logic/object_pk.py +++ b/eav/logic/object_pk.py @@ -6,11 +6,11 @@ def get_pk_format(): - if settings.PRIMARY_KEY_FIELD == "UUIDField": + if settings.PRIMARY_KEY_FIELD == "django.db.models.UUIDField": PrimaryField = partial( models.UUIDField, primary_key=True, editable=False, default=uuid.uuid4 ) - elif settings.PRIMARY_KEY_FIELD == "CharField": + elif settings.PRIMARY_KEY_FIELD == "django.db.models.CharField": PrimaryField = partial( models.CharField, primary_key=True, editable=False, max_length=40 ) diff --git a/eav/models.py b/eav/models.py index e122bf45..a540e47f 100644 --- a/eav/models.py +++ b/eav/models.py @@ -24,6 +24,11 @@ from eav.exceptions import IllegalAssignmentException from eav.fields import CSVField, EavDatatypeField from eav.logic.entity_pk import get_entity_pk_type +from eav.logic.model_managers import ( + EnumValueManager, + EnumGroupManager, + AttributeManager, +) from eav.logic.slug import SLUGFIELD_MAX_LENGTH, generate_slug from eav.logic.object_pk import get_pk_format from eav.validators import ( @@ -74,6 +79,8 @@ class EnumValue(models.Model): the same *Yes* and *No* *EnumValues* for both *EnumGroups*. """ + objects = EnumValueManager() + class Meta: verbose_name = _('EnumValue') verbose_name_plural = _('EnumValues') @@ -88,9 +95,14 @@ class Meta: max_length=SLUGFIELD_MAX_LENGTH, ) + def natural_key(self): + return (self.value,) + def __str__(self): """String representation of `EnumValue` instance.""" - return str(self.value) + return str( + self.value, + ) def __repr__(self): """String representation of `EnumValue` object.""" @@ -106,9 +118,12 @@ class EnumGroup(models.Model): See :class:`EnumValue` for an example. """ + objects = EnumGroupManager() + class Meta: verbose_name = _('EnumGroup') verbose_name_plural = _('EnumGroups') + # added id = get_pk_format() @@ -122,6 +137,9 @@ class Meta: verbose_name=_('Enum group'), ) + def natural_key(self): + return (self.name,) + def __str__(self): """String representation of `EnumGroup` instance.""" return str(self.name) @@ -183,6 +201,8 @@ class Attribute(models.Model): change it's datatype. """ + objects = AttributeManager() + class Meta: ordering = ['name'] verbose_name = _('Attribute') @@ -296,6 +316,12 @@ class Meta: verbose_name=_('Created'), ) + def natural_key(self): + return ( + self.name, + self.slug, + ) + @property def help_text(self): return self.description diff --git a/test_project/settings.py b/test_project/settings.py index 1e4be098..a25758f3 100644 --- a/test_project/settings.py +++ b/test_project/settings.py @@ -33,7 +33,7 @@ ] -PRIMARY_KEY_TYPE = "UUID" +PRIMARY_KEY_FIELD = "django.db.models.BigAutoField" MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', @@ -68,10 +68,11 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:', - }, + 'NAME': BASE_DIR / 'db.sqlite3', + } } + DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' From a73e3272536b088c721a59843ae874df0948a4ae Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Wed, 6 Sep 2023 17:19:19 +0100 Subject: [PATCH 04/34] feat: natural key handler added to value model --- eav/logic/model_managers.py | 9 +++++++++ eav/models.py | 16 +++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/eav/logic/model_managers.py b/eav/logic/model_managers.py index b7dcca79..c17e3a20 100644 --- a/eav/logic/model_managers.py +++ b/eav/logic/model_managers.py @@ -1,5 +1,6 @@ from django.db import models + class EnumValueManager(models.Manager): def get_by_natural_key(self, value): return self.get(value=value) @@ -13,3 +14,11 @@ def get_by_natural_key(self, name): class AttributeManager(models.Manager): def get_by_natural_key(self, name, slug): return self.get(name=name, slug=slug) + + +class ValueManager(models.Manager): + def get_by_natural_key(self, id, value, attribute): + from eav.models import Attribute + + attribute = Attribute.objects.get(**attribute) + return self.get(id=id, value=value, attribute=attribute) diff --git a/eav/models.py b/eav/models.py index a540e47f..b57c998b 100644 --- a/eav/models.py +++ b/eav/models.py @@ -28,6 +28,7 @@ EnumValueManager, EnumGroupManager, AttributeManager, + ValueManager, ) from eav.logic.slug import SLUGFIELD_MAX_LENGTH, generate_slug from eav.logic.object_pk import get_pk_format @@ -469,6 +470,8 @@ class Value(models.Model): # noqa: WPS110 # = """ + objects = ValueManager() + class Meta: verbose_name = _('Value') verbose_name_plural = _('Values') @@ -597,11 +600,12 @@ class Meta: fk_field='generic_value_id', ) + def natural_key(self): + return (self.id, self.value, self.attribute.natural_key()) + def __str__(self): """String representation of a Value.""" - entity = self.entity_pk_int - if self.entity_uuid: - entity = self.entity_pk_uuid + entity = self.entity_pk_uuid if self.entity_uuid else self.entity_pk_int return '{0}: "{1}" ({2})'.format( self.attribute.name, self.value, @@ -610,13 +614,11 @@ def __str__(self): def __repr__(self): """Representation of Value object.""" - entity = self.entity_pk_int - if self.entity_uuid: - entity = self.entity_pk_uuid + entity = self.entity_pk_uuid if self.entity_uuid else self.entity_pk_int return '{0}: "{1}" ({2})'.format( self.attribute.name, self.value, - entity.pk, + entity, ) def save(self, *args, **kwargs): From 6a01f163b644b9bfb8c594738e2c0e8aa9381db5 Mon Sep 17 00:00:00 2001 From: mathiasag7 <50689712+mathiasag7@users.noreply.github.com> Date: Wed, 6 Sep 2023 17:47:14 +0100 Subject: [PATCH 05/34] Update CHANGELOG.md --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d22db50..061c231c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ We follow [Semantic Versions](https://semver.org/) starting at the `0.14.0` rele ### Bug Fixes +## 1.5.0 (2023-09-06) + +### Features + +- Support for many type of primary key (UUIDField, BigAutoField, CharField) +- Support for natural key use for some models (EnumValue, EnumGroup, Attribute, Value) + ## 1.4.0 (2023-07-07) ### Features From 4be9a2413a3025957b53a47eff0adaafcd9a9222 Mon Sep 17 00:00:00 2001 From: mathiasag7 <50689712+mathiasag7@users.noreply.github.com> Date: Wed, 6 Sep 2023 17:48:19 +0100 Subject: [PATCH 06/34] Update pyproject.toml --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bb336b24..a8dafa5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,8 +15,8 @@ include = '\.pyi?$' [tool.poetry] name = "django-eav2" -description = "Entity-Attribute-Value storage for Django" -version = "1.4.0" +description = "Entity-Attribute-Value storage for Django with the possibility of changing the pk field and using natural key for serialization" +version = "1.5.0" license = "GNU Lesser General Public License (LGPL), Version 3" packages = [ { include = "eav" } From 832d4f3332d72f366edbbc01ee4530713634be82 Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Thu, 7 Sep 2023 10:08:51 +0100 Subject: [PATCH 07/34] fix: get attribute for value natural key function --- eav/logic/model_managers.py | 6 +++--- eav/models.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eav/logic/model_managers.py b/eav/logic/model_managers.py index c17e3a20..1396d1fa 100644 --- a/eav/logic/model_managers.py +++ b/eav/logic/model_managers.py @@ -17,8 +17,8 @@ def get_by_natural_key(self, name, slug): class ValueManager(models.Manager): - def get_by_natural_key(self, id, value, attribute): + def get_by_natural_key(self, id, attribute): from eav.models import Attribute - attribute = Attribute.objects.get(**attribute) - return self.get(id=id, value=value, attribute=attribute) + attribute = Attribute.objects.get(name=attribute[0], slug=attribute[1]) + return self.get(id=id, attribute=attribute) diff --git a/eav/models.py b/eav/models.py index b57c998b..d07ece77 100644 --- a/eav/models.py +++ b/eav/models.py @@ -601,7 +601,7 @@ class Meta: ) def natural_key(self): - return (self.id, self.value, self.attribute.natural_key()) + return (self.id, self.attribute.natural_key()) def __str__(self): """String representation of a Value.""" From 7ebfb8562195e5331ab20ef456c1a763fef4e55a Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Thu, 7 Sep 2023 10:14:57 +0100 Subject: [PATCH 08/34] update package log --- CHANGELOG.md | 14 +++++++++++--- README.md | 10 +++++----- pyproject.toml | 22 +++++----------------- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 061c231c..865bf9c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ We follow [Semantic Versions](https://semver.org/) starting at the `0.14.0` rele ### Bug Fixes +## 1.5.1 (2023-09-07) + +### Bug Fixes + +- Fixing error on generate foreign key based on attribute natural key at deserialization of value object + ## 1.5.0 (2023-09-06) ### Features @@ -51,6 +57,7 @@ We follow [Semantic Versions](https://semver.org/) starting at the `0.14.0` rele - Make Read the Docs dependencies all optional ## 1.2.2 (2022-08-13) + ### Bug Fixes - Fixes AttributeError when using CSVFormField [#187](https://github.com/jazzband/django-eav2/issues/187) @@ -58,6 +65,7 @@ We follow [Semantic Versions](https://semver.org/) starting at the `0.14.0` rele - Migrates Attribute.slug to django.db.models.SlugField() [#223](https://github.com/jazzband/django-eav2/issues/223) ## 1.2.1 (2022-02-08) + ### Bug Fixes - Fixes FieldError when filtering on foreign keys [#163](https://github.com/jazzband/django-eav2/issues/163) @@ -88,7 +96,7 @@ We follow [Semantic Versions](https://semver.org/) starting at the `0.14.0` rele - Bumps min python version to `3.6.2` -**Full Changelog**: https://github.com/jazzband/django-eav2/compare/1.0.0...1.1.0 +**Full Changelog**: ## 1.0.0 (2021-10-21) @@ -109,7 +117,7 @@ We follow [Semantic Versions](https://semver.org/) starting at the `0.14.0` rele - Revamps all tooling, including moving to `poetry`, `pytest`, and `black` - Adds Github Actions and Dependabot -**Full Changelog**: https://github.com/jazzband/django-eav2/compare/0.14.0...1.0.0 +**Full Changelog**: ## 0.14.0 (2021-04-23) @@ -118,6 +126,6 @@ We follow [Semantic Versions](https://semver.org/) starting at the `0.14.0` rele - This release will be the last to support this range of Django versions: 1.11, 2.0, 2.1, 2.2, 3.0. SInce all of their extended support was ended by Django Project. - From the next release only will be supported 2.2 LTS, 3.1, and 3.2 LTS (eventually 4.x) -**Full Changelog**: https://github.com/jazzband/django-eav2/compare/0.13.0...0.14.0 +**Full Changelog**: (Anything before 0.14.0 was not recorded.) diff --git a/README.md b/README.md index 4866d677..22dba631 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Django Version](https://img.shields.io/pypi/djversions/django-eav2.svg?color=green)](https://pypi.org/project/django-eav2/) [![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/) -## Django EAV 2 - Entity-Attribute-Value storage for Django +## Django EAV 2 - Entity-Attribute-Value storage for Django with the possibility of changing the pk field and using natural key for serialization Django EAV 2 is a fork of django-eav (which itself was derived from eav-django). You can find documentation here. @@ -157,7 +157,7 @@ Supplier.objects.filter(eav__city='London') ### References -[1] Exploring Performance Issues for a Clinical Database Organized Using an Entity-Attribute-Value Representation, https://doi.org/10.1136/jamia.2000.0070475
-[2] What is so bad about EAV, anyway?, https://sqlblog.org/2009/11/19/what-is-so-bad-about-eav-anyway
-[3] Magento for Developers: Part 7—Advanced ORM: Entity Attribute Value, https://devdocs.magento.com/guides/m1x/magefordev/mage-for-dev-7.html
-[4] Data Extraction and Ad Hoc Query of an Entity— Attribute— Value Database, https://www.ncbi.nlm.nih.gov/pmc/articles/PMC61332/ +[1] Exploring Performance Issues for a Clinical Database Organized Using an Entity-Attribute-Value Representation,
+[2] What is so bad about EAV, anyway?,
+[3] Magento for Developers: Part 7—Advanced ORM: Entity Attribute Value,
+[4] Data Extraction and Ad Hoc Query of an Entity— Attribute— Value Database, diff --git a/pyproject.toml b/pyproject.toml index a8dafa5e..3afe24a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,28 +16,18 @@ include = '\.pyi?$' [tool.poetry] name = "django-eav2" description = "Entity-Attribute-Value storage for Django with the possibility of changing the pk field and using natural key for serialization" -version = "1.5.0" +version = "1.5.1" license = "GNU Lesser General Public License (LGPL), Version 3" -packages = [ - { include = "eav" } -] +packages = [{ include = "eav" }] -authors = [ - "Mauro Lizaur ", -] +authors = ["Mauro Lizaur "] readme = "README.md" repository = "https://github.com/jazzband/django-eav2" -keywords = [ - "django", - "django-eav2", - "database", - "eav", - "sql", -] +keywords = ["django", "django-eav2", "database", "eav", "sql"] classifiers = [ "Development Status :: 3 - Alpha", @@ -56,9 +46,7 @@ classifiers = [ ] [tool.semantic_release] -version_variable = [ - "pyproject.toml:version" -] +version_variable = ["pyproject.toml:version"] branch = "master" upload_to_pypi = false upload_to_release = false From ce2b606dc47d6f998e8db0e4a8db5cba1093ab10 Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Thu, 7 Sep 2023 16:49:22 +0100 Subject: [PATCH 09/34] fix: error on attribute creation --- CHANGELOG.md | 6 ++++++ eav/fields.py | 4 ++++ eav/logic/model_managers.py | 5 +++-- eav/models.py | 20 ++++++++++---------- pyproject.toml | 2 +- 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 865bf9c8..1bdcc0fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ We follow [Semantic Versions](https://semver.org/) starting at the `0.14.0` rele ### Bug Fixes +## 1.5.2 (2023-09-07) + +### Bug Fixes + +- Fixing error at new attribute creation at datatype field validation + ## 1.5.1 (2023-09-07) ### Bug Fixes diff --git a/eav/fields.py b/eav/fields.py index bd728749..003ff7ab 100644 --- a/eav/fields.py +++ b/eav/fields.py @@ -20,6 +20,10 @@ def validate(self, value, instance): if not instance.pk: return + + # added + if not type(instance).objects.filter(pk=instance.pk).exists(): + return if type(instance).objects.get(pk=instance.pk).datatype == instance.datatype: return diff --git a/eav/logic/model_managers.py b/eav/logic/model_managers.py index 1396d1fa..c9d2b9a2 100644 --- a/eav/logic/model_managers.py +++ b/eav/logic/model_managers.py @@ -17,8 +17,9 @@ def get_by_natural_key(self, name, slug): class ValueManager(models.Manager): - def get_by_natural_key(self, id, attribute): + def get_by_natural_key(self, attribute, entity_id, entity_uuid): from eav.models import Attribute attribute = Attribute.objects.get(name=attribute[0], slug=attribute[1]) - return self.get(id=id, attribute=attribute) + + return self.get(attribute=attribute, entity_id=entity_id, entity_uuid=entity_uuid) diff --git a/eav/models.py b/eav/models.py index d07ece77..9017b81a 100644 --- a/eav/models.py +++ b/eav/models.py @@ -220,15 +220,15 @@ class Meta: 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, _("Texte")), + (TYPE_DATE, _("Date")), + (TYPE_FLOAT, _("Nombre décimal")), + (TYPE_INT, _("Entier")), + (TYPE_BOOLEAN, _("Vrai / Faux")), + (TYPE_OBJECT, _("Django Object")), + (TYPE_ENUM, _("Multiple Choice")), + (TYPE_JSON, _("JSON Object")), + (TYPE_CSV, _("Comma-Separated-Value")), ) # Core attributes @@ -601,7 +601,7 @@ class Meta: ) def natural_key(self): - return (self.id, self.attribute.natural_key()) + return (self.attribute.natural_key(), self.entity_id, self.entity_uuid) def __str__(self): """String representation of a Value.""" diff --git a/pyproject.toml b/pyproject.toml index 3afe24a7..eeb945ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ include = '\.pyi?$' [tool.poetry] name = "django-eav2" description = "Entity-Attribute-Value storage for Django with the possibility of changing the pk field and using natural key for serialization" -version = "1.5.1" +version = "1.5.2" license = "GNU Lesser General Public License (LGPL), Version 3" packages = [{ include = "eav" }] From dffbe601704e6d7ba8fec1bec358c8be65f4bca8 Mon Sep 17 00:00:00 2001 From: mathiasag7 <50689712+mathiasag7@users.noreply.github.com> Date: Thu, 5 Oct 2023 14:59:03 +0100 Subject: [PATCH 10/34] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 22dba631..0f4bd390 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,14 @@ ## Django EAV 2 - Entity-Attribute-Value storage for Django with the possibility of changing the pk field and using natural key for serialization +## What is new here ? + +With this version of a href="https://django-eav2.rtfd.io">django eav, you can use an IntegerField or a UUIDField as the primary key for your eav models. + Django EAV 2 is a fork of django-eav (which itself was derived from eav-django). You can find documentation here. + ## What is EAV anyway? > Entity–attribute–value model (EAV) is a data model to encode, in a space-efficient manner, entities where the number of attributes (properties, parameters) that can be used to describe them is potentially vast, but the number that will actually apply to a given entity is relatively modest. Such entities correspond to the mathematical notion of a sparse matrix. (Wikipedia) From 2c69da6b8695583a7f7d06f6ca685df6fc5da373 Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Thu, 5 Oct 2023 15:31:16 +0100 Subject: [PATCH 11/34] readme updated --- README.md | 125 +++++++++++++++++++----------------------------------- 1 file changed, 43 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 22dba631..34db5244 100644 --- a/README.md +++ b/README.md @@ -4,84 +4,15 @@ [![Django Version](https://img.shields.io/pypi/djversions/django-eav2.svg?color=green)](https://pypi.org/project/django-eav2/) [![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/) -## Django EAV 2 - Entity-Attribute-Value storage for Django with the possibility of changing the pk field and using natural key for serialization +## Django EAV 2 - Entity-Attribute-Value storage for Django -Django EAV 2 is a fork of django-eav (which itself was derived from eav-django). -You can find documentation here. +Django EAV 2 is a fork of django-eav. You can find documentation here for more information. -## What is EAV anyway? +## What is new here ? -> Entity–attribute–value model (EAV) is a data model to encode, in a space-efficient manner, entities where the number of attributes (properties, parameters) that can be used to describe them is potentially vast, but the number that will actually apply to a given entity is relatively modest. Such entities correspond to the mathematical notion of a sparse matrix. (Wikipedia) +With this version of django eav, you can use an IntegerField or a UUIDField as the primary key for your eav models. +You can also use the natural key for serialization instead of the primary key. -Data in EAV is stored as a 3-tuple (typically corresponding to three distinct tables): - -- The entity: the item being described, e.g. `Person(name='Mike')`. -- The attribute: often a foreign key into a table of attributes, e.g. `Attribute(slug='height', datatype=FLOAT)`. -- The value of the attribute, with links both an attribute and an entity, e.g. `Value(value_float=15.5, person=mike, attr=height)`. - -Entities in **django-eav2** are your typical Django model instances. Attributes (name and type) are stored in their own table, which makes it easy to manipulate the list of available attributes in the system. Values are an intermediate table between attributes and entities, each instance holding a single value. -This implementation also makes it easy to edit attributes in Django Admin and form instances. - -You will find detailed description of the EAV here: - -- [Wikipedia - Entity–attribute–value model](https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model) - -## EAV - The Good, the Bad or the Ugly? - -EAV is a trade-off between flexibility and complexity. As such, it should not be thought of as either an amelioration pattern, nor an anti-pattern. It is more of a [gray pattern](https://wiki.c2.com/?GreyPattern) - it exists in some context, to solve certain set of problems. When used appropriately, it can introduce great flexibility, cut prototyping time or deacrease complexity. When used carelessly, however, it can complicate database schema, degrade the performance and make maintainance hard. **As with every tool, it should not be overused.** In the following paragraphs we briefly discuss the pros, the cons and pointers to keep in mind when using EAV. - -### When to use EAV? - -Originally, EAV was introduced to workaround a problem which cannot be easily solved within relational model. In order to achieve this, EAV bypasses normal schema restrictions. Some refer to this as an example of the [inner-platform effect](https://en.wikipedia.org/wiki/Inner-platform_effect#Examples). Naturally, in such scenarios RDMS resources cannot be used efficiently. - -Typical application of the EAV model sets to solve the problem of sparse data with a large number of applicable attributes, but only a small fraction that applies to a given entity that may not be known beforehand. Consider the classic example: - -> A problem that data modelers commonly encounter in the biomedical domain is organizing and storing highly diverse and heterogeneous data. For example, a single patient may have thousands of applicable descriptive parameters, all of which need to be easily accessible in an electronic patient record system. These requirements pose significant modeling and implementation challenges. [1] - -And: - -> [...] what do you do when you have customers that demand real-time, on-demand addition of attributes that they want to store? In one of the systems I manage, our customers wanted to do exactly this. Since we run a SaaS (software as a service) application, we have many customers across several different industries, who in turn want to use our system to store different types of information about _their_ customers. A salon chain might want to record facts such as 'hair color,' 'hair type,' and 'haircut frequency'; while an investment company might want to record facts such as 'portfolio name,' 'last portfolio adjustment date,' and 'current portfolio balance.' [2] - -In both of these problems we have to deal with sparse and heterogeneous properties that apply only to potentially different subsets of particular entities. Applying EAV to a sub-schema of the database allows to model the desired behaviour. Traditional solution would involves wide tables with many columns storing NULL values for attributes that don't apply to an entity. - -Very common use case for EAV are custom product attributes in E-commerce implementations, such as Magento. [3] - -As a rule of thumb, EAV can be used when: - -- Model attributes are to be added and removed by end users (or are unknowable in some different way). EAV supports these without ALTER TABLE statements and allows the attributes to be strongly typed and easily searchable. -- There will be many attributes and values are sparse, in contrast to having tables with mostly-null columns. -- The data is highly dynamic/volatile/vulnerable to change. This problem is present in the second example given above. Other example would be rapidly evolving system, such as a prototype with constantly changing requirements. -- We want to store meta-data or supporting information, e.g. to customize system's behavior. -- Numerous classes of data need to be represented, each class has a limited number of attributes, but the number of instances of each class is very small. -- We want to minimise programmer's input when changing the data model. - -For more throughout discussion on the appriopriate use-cases see: - -1. [Wikipedia - Scenarios that are appropriate for EAV modeling](https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model#Scenarios_that_are_appropriate_for_EAV_modeling) -2. [StackOverflow - Entity Attribute Value Database vs. strict Relational Model E-commerce](https://stackoverflow.com/questions/870808/entity-attribute-value-database-vs-strict-relational-model-ecommerce) -3. [WikiWikiWeb - Generic Data Model](https://wiki.c2.com/?GenericDataModel) - -## When to avoid it? - -As we outlined in the opening section, EAV is a trade-off. It should not be used when: - -##### 1. System is performance critical - -> Attribute-centric query is inherently more difficult when data are stored in EAV form than when they are stored conventionally. [4] - -In general, the more structured your data model, the more efficiently you can deal with it. Therefore, loose data storage such as EAV has obvious trade-off in performance. Specifically, application of the EAV model makes performing JOINs on tables more complicated. - -##### 2. Low complexity/low maintenance cost is of priority - -EAV complicates data model by splitting information across tables. This increases conceptual complexity as well as SQL statements required to query the data. In consequence, optimization in one area that also makes the system harder to understand and maintain. - -However, it is important to note that: - -> An EAV design should be employed only for that sub-schema of a database where sparse attributes need to be modeled: even here, they need to be supported by third normal form metadata tables. There are relatively few database-design problems where sparse attributes are encountered: this is why the circumstances where EAV design is applicable are relatively rare. [1] - -## Alternatives - -In some use-cases, JSONB (binary JSON data) datatype (Postgres 9.4+ and analogous in other RDMSs) can be used as an alternative to EAV. JSONB supports indexing, which amortizes performance trade-off. It's important to keep in mind that JSONB is not RDMS-agnostic solution and has it's own problems, such as typing. ## Installation @@ -102,6 +33,43 @@ INSTALLED_APPS = [ ] ``` +Add `django.db.models.UUIDField` or `django.db.models.BigAutoField` as value of `PRIMARY_KEY_FIELD` in your settings + +``` python +PRIMARY_KEY_FIELD = "django.db.models.UUIDField" # as exemple +``` +### Note: Primary key mandatory modification field + +If the primary key of eav models are to be modified (UUIDField -> BigAutoField, BigAutoField -> UUIDField) in the middle of the project when the migrations are already done, you have to change the value of `PRIMARY_KEY_FIELD` in your settings. + +##### Step 1 + Change the value of `PRIMARY_KEY_FIELD` into `django.db.models.CharField` in your settings. + + ```python + PRIMARY_KEY_FIELD = "django.db.models.CharField" + ``` + + Run the migrations + + ```bash + python manage.py makemigrations + python manage.py migrate + ``` + +##### Step 2 + Change the value of `PRIMARY_KEY_FIELD` into the desired value (`django.db.models.BigAutoField` or `django.db.models.UUIDField`) in your settings. + + ```python + PRIMARY_KEY_FIELD = "django.db.models.BigAutoField" # as exemple + ``` + + Run again the migrations. + +```bash + python manage.py makemigrations + python manage.py migrate + ``` + ### Note: Django 2.2 Users Since `models.JSONField()` isn't supported in Django 2.2, we use [django-jsonfield-backport](https://github.com/laymonage/django-jsonfield-backport) to provide [JSONField](https://docs.djangoproject.com/en/dev/releases/3.1/#jsonfield-for-all-supported-database-backends) functionality. @@ -151,13 +119,6 @@ Supplier.objects.filter(eav__city='London') # = ]> ``` -**What next? Check out the documentation.** +**For futher information? Check out the django eav2 documentation.** --- - -### References - -[1] Exploring Performance Issues for a Clinical Database Organized Using an Entity-Attribute-Value Representation,
-[2] What is so bad about EAV, anyway?,
-[3] Magento for Developers: Part 7—Advanced ORM: Entity Attribute Value,
-[4] Data Extraction and Ad Hoc Query of an Entity— Attribute— Value Database, From 0aa172c030b1048e405e1b95d314a7c85f0ccfa0 Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Fri, 6 Oct 2023 11:38:02 +0100 Subject: [PATCH 12/34] changelog updated --- CHANGELOG.md | 19 +++--------- README.md | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 90 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bdcc0fb..5e8f940e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ # Version History +# Version History We follow [Semantic Versions](https://semver.org/) starting at the `0.14.0` release. @@ -8,24 +9,14 @@ We follow [Semantic Versions](https://semver.org/) starting at the `0.14.0` rele ### Bug Fixes -## 1.5.2 (2023-09-07) - -### Bug Fixes - -- Fixing error at new attribute creation at datatype field validation - -## 1.5.1 (2023-09-07) - -### Bug Fixes - -- Fixing error on generate foreign key based on attribute natural key at deserialization of value object +- Fixes querying with multiple eav kwargs [#395](https://github.com/jazzband/django-eav2/issues/395) -## 1.5.0 (2023-09-06) +## 1.5.0 (2023-09-07) ### Features -- Support for many type of primary key (UUIDField, BigAutoField, CharField) -- Support for natural key use for some models (EnumValue, EnumGroup, Attribute, Value) +- Support for many type of primary key (UUIDField, BigAutoField) +- Support for natural key use for some models for serialization (EnumValue, EnumGroup, Attribute, Value) ## 1.4.0 (2023-07-07) diff --git a/README.md b/README.md index 34db5244..37286add 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,82 @@ ## Django EAV 2 - Entity-Attribute-Value storage for Django -Django EAV 2 is a fork of django-eav. You can find documentation here for more information. +Django EAV 2 is a fork of django-eav (which itself was derived from eav-django). +You can find documentation here. + +## What is EAV anyway? + +> Entity–attribute–value model (EAV) is a data model to encode, in a space-efficient manner, entities where the number of attributes (properties, parameters) that can be used to describe them is potentially vast, but the number that will actually apply to a given entity is relatively modest. Such entities correspond to the mathematical notion of a sparse matrix. (Wikipedia) + +Data in EAV is stored as a 3-tuple (typically corresponding to three distinct tables): + +- The entity: the item being described, e.g. `Person(name='Mike')`. +- The attribute: often a foreign key into a table of attributes, e.g. `Attribute(slug='height', datatype=FLOAT)`. +- The value of the attribute, with links both an attribute and an entity, e.g. `Value(value_float=15.5, person=mike, attr=height)`. + +Entities in **django-eav2** are your typical Django model instances. Attributes (name and type) are stored in their own table, which makes it easy to manipulate the list of available attributes in the system. Values are an intermediate table between attributes and entities, each instance holding a single value. +This implementation also makes it easy to edit attributes in Django Admin and form instances. + +You will find detailed description of the EAV here: + +- [Wikipedia - Entity–attribute–value model](https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model) + +## EAV - The Good, the Bad or the Ugly? + +EAV is a trade-off between flexibility and complexity. As such, it should not be thought of as either an amelioration pattern, nor an anti-pattern. It is more of a [gray pattern](https://wiki.c2.com/?GreyPattern) - it exists in some context, to solve certain set of problems. When used appropriately, it can introduce great flexibility, cut prototyping time or deacrease complexity. When used carelessly, however, it can complicate database schema, degrade the performance and make maintainance hard. **As with every tool, it should not be overused.** In the following paragraphs we briefly discuss the pros, the cons and pointers to keep in mind when using EAV. + +### When to use EAV? + +Originally, EAV was introduced to workaround a problem which cannot be easily solved within relational model. In order to achieve this, EAV bypasses normal schema restrictions. Some refer to this as an example of the [inner-platform effect](https://en.wikipedia.org/wiki/Inner-platform_effect#Examples). Naturally, in such scenarios RDMS resources cannot be used efficiently. + +Typical application of the EAV model sets to solve the problem of sparse data with a large number of applicable attributes, but only a small fraction that applies to a given entity that may not be known beforehand. Consider the classic example: + +> A problem that data modelers commonly encounter in the biomedical domain is organizing and storing highly diverse and heterogeneous data. For example, a single patient may have thousands of applicable descriptive parameters, all of which need to be easily accessible in an electronic patient record system. These requirements pose significant modeling and implementation challenges. [1] + +And: + +> [...] what do you do when you have customers that demand real-time, on-demand addition of attributes that they want to store? In one of the systems I manage, our customers wanted to do exactly this. Since we run a SaaS (software as a service) application, we have many customers across several different industries, who in turn want to use our system to store different types of information about _their_ customers. A salon chain might want to record facts such as 'hair color,' 'hair type,' and 'haircut frequency'; while an investment company might want to record facts such as 'portfolio name,' 'last portfolio adjustment date,' and 'current portfolio balance.' [2] + +In both of these problems we have to deal with sparse and heterogeneous properties that apply only to potentially different subsets of particular entities. Applying EAV to a sub-schema of the database allows to model the desired behaviour. Traditional solution would involves wide tables with many columns storing NULL values for attributes that don't apply to an entity. + +Very common use case for EAV are custom product attributes in E-commerce implementations, such as Magento. [3] + +As a rule of thumb, EAV can be used when: + +- Model attributes are to be added and removed by end users (or are unknowable in some different way). EAV supports these without ALTER TABLE statements and allows the attributes to be strongly typed and easily searchable. +- There will be many attributes and values are sparse, in contrast to having tables with mostly-null columns. +- The data is highly dynamic/volatile/vulnerable to change. This problem is present in the second example given above. Other example would be rapidly evolving system, such as a prototype with constantly changing requirements. +- We want to store meta-data or supporting information, e.g. to customize system's behavior. +- Numerous classes of data need to be represented, each class has a limited number of attributes, but the number of instances of each class is very small. +- We want to minimise programmer's input when changing the data model. + +For more throughout discussion on the appriopriate use-cases see: + +1. [Wikipedia - Scenarios that are appropriate for EAV modeling](https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model#Scenarios_that_are_appropriate_for_EAV_modeling) +2. [StackOverflow - Entity Attribute Value Database vs. strict Relational Model E-commerce](https://stackoverflow.com/questions/870808/entity-attribute-value-database-vs-strict-relational-model-ecommerce) +3. [WikiWikiWeb - Generic Data Model](https://wiki.c2.com/?GenericDataModel) + +## When to avoid it? + +As we outlined in the opening section, EAV is a trade-off. It should not be used when: + +##### 1. System is performance critical + +> Attribute-centric query is inherently more difficult when data are stored in EAV form than when they are stored conventionally. [4] + +In general, the more structured your data model, the more efficiently you can deal with it. Therefore, loose data storage such as EAV has obvious trade-off in performance. Specifically, application of the EAV model makes performing JOINs on tables more complicated. + +##### 2. Low complexity/low maintenance cost is of priority + +EAV complicates data model by splitting information across tables. This increases conceptual complexity as well as SQL statements required to query the data. In consequence, optimization in one area that also makes the system harder to understand and maintain. + +However, it is important to note that: + +> An EAV design should be employed only for that sub-schema of a database where sparse attributes need to be modeled: even here, they need to be supported by third normal form metadata tables. There are relatively few database-design problems where sparse attributes are encountered: this is why the circumstances where EAV design is applicable are relatively rare. [1] + +## Alternatives + +In some use-cases, JSONB (binary JSON data) datatype (Postgres 9.4+ and analogous in other RDMSs) can be used as an alternative to EAV. JSONB supports indexing, which amortizes performance trade-off. It's important to keep in mind that JSONB is not RDMS-agnostic solution and has it's own problems, such as typing. ## What is new here ? @@ -38,6 +113,7 @@ Add `django.db.models.UUIDField` or `django.db.models.BigAutoField` as value of ``` python PRIMARY_KEY_FIELD = "django.db.models.UUIDField" # as exemple ``` + ### Note: Primary key mandatory modification field If the primary key of eav models are to be modified (UUIDField -> BigAutoField, BigAutoField -> UUIDField) in the middle of the project when the migrations are already done, you have to change the value of `PRIMARY_KEY_FIELD` in your settings. @@ -119,6 +195,13 @@ Supplier.objects.filter(eav__city='London') # = ]> ``` -**For futher information? Check out the django eav2 documentation.** +**What next? Check out the documentation.** --- + +### References + +[1] Exploring Performance Issues for a Clinical Database Organized Using an Entity-Attribute-Value Representation, https://doi.org/10.1136/jamia.2000.0070475
+[2] What is so bad about EAV, anyway?, https://sqlblog.org/2009/11/19/what-is-so-bad-about-eav-anyway
+[3] Magento for Developers: Part 7—Advanced ORM: Entity Attribute Value, https://devdocs.magento.com/guides/m1x/magefordev/mage-for-dev-7.html
+[4] Data Extraction and Ad Hoc Query of an Entity— Attribute— Value Database, https://www.ncbi.nlm.nih.gov/pmc/articles/PMC61332/ From f342f3cab64a0a8361b90d1f5965481031c46a53 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 10:46:13 +0000 Subject: [PATCH 13/34] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- eav/fields.py | 2 +- eav/logic/model_managers.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/eav/fields.py b/eav/fields.py index 003ff7ab..c9f29103 100644 --- a/eav/fields.py +++ b/eav/fields.py @@ -20,7 +20,7 @@ def validate(self, value, instance): if not instance.pk: return - + # added if not type(instance).objects.filter(pk=instance.pk).exists(): return diff --git a/eav/logic/model_managers.py b/eav/logic/model_managers.py index c9d2b9a2..0ea69548 100644 --- a/eav/logic/model_managers.py +++ b/eav/logic/model_managers.py @@ -22,4 +22,6 @@ def get_by_natural_key(self, attribute, entity_id, entity_uuid): attribute = Attribute.objects.get(name=attribute[0], slug=attribute[1]) - return self.get(attribute=attribute, entity_id=entity_id, entity_uuid=entity_uuid) + return self.get( + attribute=attribute, entity_id=entity_id, entity_uuid=entity_uuid + ) From 6e90e9c6b172b2596166a21847be0a31c030893e Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Fri, 13 Oct 2023 10:24:35 +0100 Subject: [PATCH 14/34] test: test_attribute errors corrected on AutoField --- eav/logic/{model_managers.py => managers.py} | 0 eav/models.py | 2 +- test_project/settings.py | 1 + tests/test_attributes.py | 10 ++++++++++ 4 files changed, 12 insertions(+), 1 deletion(-) rename eav/logic/{model_managers.py => managers.py} (100%) diff --git a/eav/logic/model_managers.py b/eav/logic/managers.py similarity index 100% rename from eav/logic/model_managers.py rename to eav/logic/managers.py diff --git a/eav/models.py b/eav/models.py index 9017b81a..2f352762 100644 --- a/eav/models.py +++ b/eav/models.py @@ -24,7 +24,7 @@ from eav.exceptions import IllegalAssignmentException from eav.fields import CSVField, EavDatatypeField from eav.logic.entity_pk import get_entity_pk_type -from eav.logic.model_managers import ( +from eav.logic.managers import ( EnumValueManager, EnumGroupManager, AttributeManager, diff --git a/test_project/settings.py b/test_project/settings.py index a25758f3..4c8355ce 100644 --- a/test_project/settings.py +++ b/test_project/settings.py @@ -74,6 +74,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +PRIMARY_KEY_FIELD = 'django.db.models.AutoField' # Password validation diff --git a/tests/test_attributes.py b/tests/test_attributes.py index e6b8e74c..d8735383 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -1,8 +1,10 @@ +import uuid import string from django.core.exceptions import ValidationError from django.test import TestCase from hypothesis import given, settings +from django.conf import settings as django_settings from hypothesis import strategies as st from hypothesis.extra import django from hypothesis.strategies import just @@ -13,6 +15,13 @@ from eav.registry import EavConfig from test_project.models import Doctor, Encounter, Patient, RegisterTestModel +if django_settings.PRIMARY_KEY_FIELD == "django.db.models.UUIDField": + auto_field_strategy = st.builds(uuid.uuid4, version=4, max_length=32) +elif django_settings.PRIMARY_KEY_FIELD == "django.db.models.CharField": + auto_field_strategy = st.text(min_size=1, max_size=255) +else: + auto_field_strategy = st.integers(min_value=1, max_value=32) + class Attributes(TestCase): def setUp(self): @@ -123,6 +132,7 @@ class TestAttributeModel(django.TestCase): @given( django.from_model( Attribute, + id=auto_field_strategy, datatype=just(Attribute.TYPE_TEXT), enum_group=just(None), ), From 668dcab50caa0f00db62e4e93313ca0fbcdb1091 Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Fri, 13 Oct 2023 10:30:55 +0100 Subject: [PATCH 15/34] test: pytest.ini file added --- pytest.ini | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..c4c5624a --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[tool:pytest] +DJANGO_SETTINGS_MODULE = test_project.settings +PRIMARY_KEY_FIELD = "django.db.models.BigAutoField" +python_files = tests/test_*.py \ No newline at end of file From e64425dfd62189155b3daf79f9b183bbb1499053 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 13 Oct 2023 09:32:02 +0000 Subject: [PATCH 16/34] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index c4c5624a..7a0f314e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,4 @@ [tool:pytest] DJANGO_SETTINGS_MODULE = test_project.settings PRIMARY_KEY_FIELD = "django.db.models.BigAutoField" -python_files = tests/test_*.py \ No newline at end of file +python_files = tests/test_*.py From 0d705d97cee6113ab97ad00588138bad4b29631a Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Fri, 13 Oct 2023 11:02:04 +0100 Subject: [PATCH 17/34] test: pyproject.toml updated" --- pyproject.toml | 4 ++-- pytest.ini | 4 ---- tests/test_attributes.py | 1 + 3 files changed, 3 insertions(+), 6 deletions(-) delete mode 100644 pytest.ini diff --git a/pyproject.toml b/pyproject.toml index e6c44f3c..1c0879ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,8 +15,8 @@ include = '\.pyi?$' [tool.poetry] name = "django-eav2" -description = "Entity-Attribute-Value storage for Django with the possibility of changing the pk field and using natural key for serialization" -version = "1.5.2" +description = "Entity-Attribute-Value storage for Django" +version = "1.5.0" license = "GNU Lesser General Public License (LGPL), Version 3" packages = [{ include = "eav" }] diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 7a0f314e..00000000 --- a/pytest.ini +++ /dev/null @@ -1,4 +0,0 @@ -[tool:pytest] -DJANGO_SETTINGS_MODULE = test_project.settings -PRIMARY_KEY_FIELD = "django.db.models.BigAutoField" -python_files = tests/test_*.py diff --git a/tests/test_attributes.py b/tests/test_attributes.py index d8735383..a2675493 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -15,6 +15,7 @@ from eav.registry import EavConfig from test_project.models import Doctor, Encounter, Patient, RegisterTestModel + if django_settings.PRIMARY_KEY_FIELD == "django.db.models.UUIDField": auto_field_strategy = st.builds(uuid.uuid4, version=4, max_length=32) elif django_settings.PRIMARY_KEY_FIELD == "django.db.models.CharField": From 0000e2620587fc7d54fbf10c60e5a1b89f883049 Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Fri, 13 Oct 2023 11:28:38 +0100 Subject: [PATCH 18/34] migration file created --- .gitignore | 1 - ...te_datatype_alter_attribute_id_and_more.py | 61 +++++++++++++++++++ tests/test_attributes.py | 2 +- 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 eav/migrations/0010_alter_attribute_datatype_alter_attribute_id_and_more.py diff --git a/.gitignore b/.gitignore index 60b68e62..d52beced 100644 --- a/.gitignore +++ b/.gitignore @@ -93,7 +93,6 @@ venv/ ENV/ env.bak/ venv.bak/ - # Spyder project settings .spyderproject .spyproject diff --git a/eav/migrations/0010_alter_attribute_datatype_alter_attribute_id_and_more.py b/eav/migrations/0010_alter_attribute_datatype_alter_attribute_id_and_more.py new file mode 100644 index 00000000..2437611d --- /dev/null +++ b/eav/migrations/0010_alter_attribute_datatype_alter_attribute_id_and_more.py @@ -0,0 +1,61 @@ +# Generated by Django 4.2.6 on 2023-10-13 10:19 + +from django.db import migrations, models +import eav.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('eav', '0009_enchance_naming'), + ] + + operations = [ + migrations.AlterField( + model_name='attribute', + name='datatype', + field=eav.fields.EavDatatypeField( + choices=[ + ('text', 'Texte'), + ('date', 'Date'), + ('float', 'Nombre décimal'), + ('int', 'Entier'), + ('bool', 'Vrai / Faux'), + ('object', 'Django Object'), + ('enum', 'Multiple Choice'), + ('json', 'JSON Object'), + ('csv', 'Comma-Separated-Value'), + ], + max_length=6, + verbose_name='Data Type', + ), + ), + migrations.AlterField( + model_name='attribute', + name='id', + field=models.BigAutoField( + editable=False, primary_key=True, serialize=False + ), + ), + migrations.AlterField( + model_name='enumgroup', + name='id', + field=models.BigAutoField( + editable=False, primary_key=True, serialize=False + ), + ), + migrations.AlterField( + model_name='enumvalue', + name='id', + field=models.BigAutoField( + editable=False, primary_key=True, serialize=False + ), + ), + migrations.AlterField( + model_name='value', + name='id', + field=models.BigAutoField( + editable=False, primary_key=True, serialize=False + ), + ), + ] diff --git a/tests/test_attributes.py b/tests/test_attributes.py index a2675493..984c34e5 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -4,9 +4,9 @@ from django.core.exceptions import ValidationError from django.test import TestCase from hypothesis import given, settings +from hypothesis.extra import django from django.conf import settings as django_settings from hypothesis import strategies as st -from hypothesis.extra import django from hypothesis.strategies import just import eav From 7a00e04afc6853f92d30cdc2af079f3c6cf8d935 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 13 Oct 2023 10:29:16 +0000 Subject: [PATCH 19/34] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../0010_alter_attribute_datatype_alter_attribute_id_and_more.py | 1 - 1 file changed, 1 deletion(-) diff --git a/eav/migrations/0010_alter_attribute_datatype_alter_attribute_id_and_more.py b/eav/migrations/0010_alter_attribute_datatype_alter_attribute_id_and_more.py index 2437611d..acb12fef 100644 --- a/eav/migrations/0010_alter_attribute_datatype_alter_attribute_id_and_more.py +++ b/eav/migrations/0010_alter_attribute_datatype_alter_attribute_id_and_more.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [ ('eav', '0009_enchance_naming'), ] From d26f61ac94ddc03e042cfa10b29ecb29bcc5f892 Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Fri, 13 Oct 2023 11:42:59 +0100 Subject: [PATCH 20/34] test updated --- tests/test_primary_key_format.py | 43 ++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/test_primary_key_format.py diff --git a/tests/test_primary_key_format.py b/tests/test_primary_key_format.py new file mode 100644 index 00000000..92c6c352 --- /dev/null +++ b/tests/test_primary_key_format.py @@ -0,0 +1,43 @@ +# test_primary_key_format.py + +from django.test import TestCase +from django.conf import settings +from django.db import models +from functools import partial +import uuid +from eav.logic.object_pk import get_pk_format + +class GetPrimaryKeyFormatTestCase(TestCase): + def test_get_uuid_primary_key(self): + settings.PRIMARY_KEY_FIELD = "django.db.models.UUIDField" + primary_field = get_pk_format() + self.assertTrue(isinstance(primary_field, models.UUIDField)) + self.assertTrue(primary_field.primary_key) + self.assertFalse(primary_field.editable) + self.assertEqual(primary_field.default, uuid.uuid4) + + def test_get_char_primary_key(self): + settings.PRIMARY_KEY_FIELD = "django.db.models.CharField" + primary_field = get_pk_format() + self.assertTrue(isinstance(primary_field, models.CharField)) + self.assertTrue(primary_field.primary_key) + self.assertFalse(primary_field.editable) + self.assertEqual(primary_field.max_length, 40) + + def test_get_default_primary_key(self): + # This test covers the default case for "BigAutoField" + settings.PRIMARY_KEY_FIELD = "AnyOtherField" + primary_field = get_pk_format() + self.assertTrue(isinstance(primary_field, models.BigAutoField)) + self.assertTrue(primary_field.primary_key) + self.assertFalse(primary_field.editable) + + def test_unrecognized_primary_key_field(self): + # Test when an unrecognized primary key field is specified in settings + settings.PRIMARY_KEY_FIELD = "UnrecognizedField" + with self.assertRaises(ValueError): + get_pk_format() + + def tearDown(self): + # Reset the PRIMARY_KEY_FIELD setting after each test + settings.PRIMARY_KEY_FIELD = "django.db.models.BigAutoField" From 4ef7f7a271fae1fc295cbff6045efa00a6a5a199 Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Fri, 13 Oct 2023 11:44:20 +0100 Subject: [PATCH 21/34] test updated --- tests/test_primary_key_format.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_primary_key_format.py b/tests/test_primary_key_format.py index 92c6c352..c7758f13 100644 --- a/tests/test_primary_key_format.py +++ b/tests/test_primary_key_format.py @@ -7,6 +7,7 @@ import uuid from eav.logic.object_pk import get_pk_format + class GetPrimaryKeyFormatTestCase(TestCase): def test_get_uuid_primary_key(self): settings.PRIMARY_KEY_FIELD = "django.db.models.UUIDField" From 9407982d2908ab74ce1709c00ce4c47da421d701 Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Mon, 16 Oct 2023 10:20:02 +0100 Subject: [PATCH 22/34] revert changes --- README.md | 14 ++++----- eav/logic/object_pk.py | 4 +-- pyproject.toml | 50 ++++++++++++++++++-------------- test_project/settings.py | 8 ++--- tests/test_attributes.py | 4 +-- tests/test_primary_key_format.py | 13 ++++----- 6 files changed, 48 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index ae9c77db..8778e174 100644 --- a/README.md +++ b/README.md @@ -103,21 +103,21 @@ INSTALLED_APPS = [ ] ``` -Add `django.db.models.UUIDField` or `django.db.models.BigAutoField` as value of `PRIMARY_KEY_FIELD` in your settings +Add `django.db.models.UUIDField` or `django.db.models.BigAutoField` as value of `EAV2_PRIMARY_KEY_FIELD` in your settings ``` python -PRIMARY_KEY_FIELD = "django.db.models.UUIDField" # as exemple +EAV2_PRIMARY_KEY_FIELD = "django.db.models.UUIDField" # as exemple ``` ### Note: Primary key mandatory modification field -If the primary key of eav models are to be modified (UUIDField -> BigAutoField, BigAutoField -> UUIDField) in the middle of the project when the migrations are already done, you have to change the value of `PRIMARY_KEY_FIELD` in your settings. +If the primary key of eav models are to be modified (UUIDField -> BigAutoField, BigAutoField -> UUIDField) in the middle of the project when the migrations are already done, you have to change the value of `EAV2_PRIMARY_KEY_FIELD` in your settings. ##### Step 1 - Change the value of `PRIMARY_KEY_FIELD` into `django.db.models.CharField` in your settings. + Change the value of `EAV2_PRIMARY_KEY_FIELD` into `django.db.models.CharField` in your settings. ```python - PRIMARY_KEY_FIELD = "django.db.models.CharField" + EAV2_PRIMARY_KEY_FIELD = "django.db.models.CharField" ``` Run the migrations @@ -128,10 +128,10 @@ If the primary key of eav models are to be modified (UUIDField -> BigAutoField, ``` ##### Step 2 - Change the value of `PRIMARY_KEY_FIELD` into the desired value (`django.db.models.BigAutoField` or `django.db.models.UUIDField`) in your settings. + Change the value of `EAV2_PRIMARY_KEY_FIELD` into the desired value (`django.db.models.BigAutoField` or `django.db.models.UUIDField`) in your settings. ```python - PRIMARY_KEY_FIELD = "django.db.models.BigAutoField" # as exemple + EAV2_PRIMARY_KEY_FIELD = "django.db.models.BigAutoField" # as exemple ``` Run again the migrations. diff --git a/eav/logic/object_pk.py b/eav/logic/object_pk.py index 70647373..62d36f89 100644 --- a/eav/logic/object_pk.py +++ b/eav/logic/object_pk.py @@ -6,11 +6,11 @@ def get_pk_format(): - if settings.PRIMARY_KEY_FIELD == "django.db.models.UUIDField": + if settings.EAV2_PRIMARY_KEY_FIELD == "django.db.models.UUIDField": PrimaryField = partial( models.UUIDField, primary_key=True, editable=False, default=uuid.uuid4 ) - elif settings.PRIMARY_KEY_FIELD == "django.db.models.CharField": + elif settings.EAV2_PRIMARY_KEY_FIELD == "django.db.models.CharField": PrimaryField = partial( models.CharField, primary_key=True, editable=False, max_length=40 ) diff --git a/pyproject.toml b/pyproject.toml index 1c0879ce..86fbf71c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["poetry-core>=1.0.0"] +requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" @@ -16,18 +16,28 @@ include = '\.pyi?$' [tool.poetry] name = "django-eav2" description = "Entity-Attribute-Value storage for Django" -version = "1.5.0" +version = "1.4.0" license = "GNU Lesser General Public License (LGPL), Version 3" -packages = [{ include = "eav" }] +packages = [ + { include = "eav" } +] -authors = ["Mauro Lizaur "] +authors = [ + "Mauro Lizaur ", +] readme = "README.md" repository = "https://github.com/jazzband/django-eav2" -keywords = ["django", "django-eav2", "database", "eav", "sql"] +keywords = [ + "django", + "django-eav2", + "database", + "eav", + "sql", +] classifiers = [ "Development Status :: 3 - Alpha", @@ -46,7 +56,9 @@ classifiers = [ ] [tool.semantic_release] -version_variable = ["pyproject.toml:version"] +version_variable = [ + "pyproject.toml:version" +] branch = "master" upload_to_pypi = false upload_to_release = false @@ -56,14 +68,7 @@ build_command = "pip install poetry && poetry build" python = "^3.8" django = ">=3.2,<4.3" -# Docs extra: -sphinx = { version = ">=5,<8", optional = true } -sphinx-autodoc-typehints = { version = "^1.12", optional = true } -m2r2 = { version = "^0.3", optional = true } -tomlkit = { version = ">=0.11,<0.13", optional = true } -sphinx-rtd-theme = { version = "^1.0.0", optional = true } - -[tool.poetry.dev-dependencies] +[tool.poetry.group.test.dependencies] mypy = "^1.6" wemake-python-styleguide = "^0.17" @@ -82,11 +87,12 @@ hypothesis = "^6.87.1" doc8 = "^0.11.2" -[tool.poetry.extras] -docs = [ - "sphinx", - "sphinx-autodoc-typehints", - "sphinx_rtd_theme", - "m2r2", - "tomlkit", -] +[tool.poetry.group.docs] +optional = true + +[tool.poetry.group.docs.dependencies] +sphinx = ">=5.0,<8.0" +sphinx-rtd-theme = "^1.3.0" +sphinx-autodoc-typehints = "^1.19.5" +m2r2 = "^0.3" +tomlkit = ">=0.11,<0.13" diff --git a/test_project/settings.py b/test_project/settings.py index 4c8355ce..07f7f724 100644 --- a/test_project/settings.py +++ b/test_project/settings.py @@ -33,8 +33,6 @@ ] -PRIMARY_KEY_FIELD = "django.db.models.BigAutoField" - MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -68,13 +66,13 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } + 'NAME': ':memory:', + }, } DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' -PRIMARY_KEY_FIELD = 'django.db.models.AutoField' +EAV2_PRIMARY_KEY_FIELD = 'django.db.models.AutoField' # Password validation diff --git a/tests/test_attributes.py b/tests/test_attributes.py index 984c34e5..1347eb2f 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -16,9 +16,9 @@ from test_project.models import Doctor, Encounter, Patient, RegisterTestModel -if django_settings.PRIMARY_KEY_FIELD == "django.db.models.UUIDField": +if django_settings.EAV2_PRIMARY_KEY_FIELD == "django.db.models.UUIDField": auto_field_strategy = st.builds(uuid.uuid4, version=4, max_length=32) -elif django_settings.PRIMARY_KEY_FIELD == "django.db.models.CharField": +elif django_settings.EAV2_PRIMARY_KEY_FIELD == "django.db.models.CharField": auto_field_strategy = st.text(min_size=1, max_size=255) else: auto_field_strategy = st.integers(min_value=1, max_value=32) diff --git a/tests/test_primary_key_format.py b/tests/test_primary_key_format.py index c7758f13..365eb5ad 100644 --- a/tests/test_primary_key_format.py +++ b/tests/test_primary_key_format.py @@ -3,14 +3,13 @@ from django.test import TestCase from django.conf import settings from django.db import models -from functools import partial import uuid from eav.logic.object_pk import get_pk_format class GetPrimaryKeyFormatTestCase(TestCase): def test_get_uuid_primary_key(self): - settings.PRIMARY_KEY_FIELD = "django.db.models.UUIDField" + settings.EAV2_PRIMARY_KEY_FIELD = "django.db.models.UUIDField" primary_field = get_pk_format() self.assertTrue(isinstance(primary_field, models.UUIDField)) self.assertTrue(primary_field.primary_key) @@ -18,7 +17,7 @@ def test_get_uuid_primary_key(self): self.assertEqual(primary_field.default, uuid.uuid4) def test_get_char_primary_key(self): - settings.PRIMARY_KEY_FIELD = "django.db.models.CharField" + settings.EAV2_PRIMARY_KEY_FIELD = "django.db.models.CharField" primary_field = get_pk_format() self.assertTrue(isinstance(primary_field, models.CharField)) self.assertTrue(primary_field.primary_key) @@ -27,7 +26,7 @@ def test_get_char_primary_key(self): def test_get_default_primary_key(self): # This test covers the default case for "BigAutoField" - settings.PRIMARY_KEY_FIELD = "AnyOtherField" + settings.EAV2_PRIMARY_KEY_FIELD = "AnyOtherField" primary_field = get_pk_format() self.assertTrue(isinstance(primary_field, models.BigAutoField)) self.assertTrue(primary_field.primary_key) @@ -35,10 +34,10 @@ def test_get_default_primary_key(self): def test_unrecognized_primary_key_field(self): # Test when an unrecognized primary key field is specified in settings - settings.PRIMARY_KEY_FIELD = "UnrecognizedField" + settings.EAV2_PRIMARY_KEY_FIELD = "UnrecognizedField" with self.assertRaises(ValueError): get_pk_format() def tearDown(self): - # Reset the PRIMARY_KEY_FIELD setting after each test - settings.PRIMARY_KEY_FIELD = "django.db.models.BigAutoField" + # Reset the EAV2_PRIMARY_KEY_FIELD setting after each test + settings.EAV2_PRIMARY_KEY_FIELD = "django.db.models.BigAutoField" From 1d70d73a2c5c77e080b317666ac4c858a99abd72 Mon Sep 17 00:00:00 2001 From: Mike <22396211+Dresdn@users.noreply.github.com> Date: Tue, 24 Oct 2023 08:10:01 -0700 Subject: [PATCH 23/34] convert tests to pytest --- tests/test_primary_key_format.py | 73 +++++++++++++++----------------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/tests/test_primary_key_format.py b/tests/test_primary_key_format.py index 365eb5ad..c359a26c 100644 --- a/tests/test_primary_key_format.py +++ b/tests/test_primary_key_format.py @@ -1,43 +1,40 @@ -# test_primary_key_format.py +import uuid -from django.test import TestCase -from django.conf import settings +import pytest from django.db import models -import uuid + from eav.logic.object_pk import get_pk_format -class GetPrimaryKeyFormatTestCase(TestCase): - def test_get_uuid_primary_key(self): - settings.EAV2_PRIMARY_KEY_FIELD = "django.db.models.UUIDField" - primary_field = get_pk_format() - self.assertTrue(isinstance(primary_field, models.UUIDField)) - self.assertTrue(primary_field.primary_key) - self.assertFalse(primary_field.editable) - self.assertEqual(primary_field.default, uuid.uuid4) - - def test_get_char_primary_key(self): - settings.EAV2_PRIMARY_KEY_FIELD = "django.db.models.CharField" - primary_field = get_pk_format() - self.assertTrue(isinstance(primary_field, models.CharField)) - self.assertTrue(primary_field.primary_key) - self.assertFalse(primary_field.editable) - self.assertEqual(primary_field.max_length, 40) - - def test_get_default_primary_key(self): - # This test covers the default case for "BigAutoField" - settings.EAV2_PRIMARY_KEY_FIELD = "AnyOtherField" - primary_field = get_pk_format() - self.assertTrue(isinstance(primary_field, models.BigAutoField)) - self.assertTrue(primary_field.primary_key) - self.assertFalse(primary_field.editable) - - def test_unrecognized_primary_key_field(self): - # Test when an unrecognized primary key field is specified in settings - settings.EAV2_PRIMARY_KEY_FIELD = "UnrecognizedField" - with self.assertRaises(ValueError): - get_pk_format() - - def tearDown(self): - # Reset the EAV2_PRIMARY_KEY_FIELD setting after each test - settings.EAV2_PRIMARY_KEY_FIELD = "django.db.models.BigAutoField" +def test_get_uuid_primary_key(settings) -> None: + settings.EAV2_PRIMARY_KEY_FIELD = "django.db.models.UUIDField" + primary_field = get_pk_format() + assert isinstance(primary_field, models.UUIDField) + assert primary_field.primary_key + assert not primary_field.editable + assert primary_field.default == uuid.uuid4 + + +def test_get_char_primary_key(settings) -> None: + settings.EAV2_PRIMARY_KEY_FIELD = "django.db.models.CharField" + primary_field = get_pk_format() + assert isinstance(primary_field, models.CharField) + assert primary_field.primary_key + assert not primary_field.editable + assert primary_field.max_length == 40 + + +def test_get_default_primary_key(settings) -> None: + # This test covers the default case for "BigAutoField" + settings.EAV2_PRIMARY_KEY_FIELD = "AnyOtherField" + primary_field = get_pk_format() + assert isinstance(primary_field, models.BigAutoField) + assert primary_field.primary_key + assert not primary_field.editable + + +def test_unrecognized_primary_key_field(settings): + # Test when an unrecognized primary key field is specified in settings + settings.EAV2_PRIMARY_KEY_FIELD = "UnrecognizedField" + with pytest.raises(ValueError): + get_pk_format() From f7cd292b9117b00755324732d308d22f0327e701 Mon Sep 17 00:00:00 2001 From: Mike <22396211+Dresdn@users.noreply.github.com> Date: Tue, 24 Oct 2023 08:11:14 -0700 Subject: [PATCH 24/34] remove test cehcking for ValueError get_pk_format() always returns a valid value --- tests/test_primary_key_format.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/test_primary_key_format.py b/tests/test_primary_key_format.py index c359a26c..4b9410c8 100644 --- a/tests/test_primary_key_format.py +++ b/tests/test_primary_key_format.py @@ -31,10 +31,3 @@ def test_get_default_primary_key(settings) -> None: assert isinstance(primary_field, models.BigAutoField) assert primary_field.primary_key assert not primary_field.editable - - -def test_unrecognized_primary_key_field(settings): - # Test when an unrecognized primary key field is specified in settings - settings.EAV2_PRIMARY_KEY_FIELD = "UnrecognizedField" - with pytest.raises(ValueError): - get_pk_format() From db893d3aabe444136a7379c5ee171710a9c12ebf Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Wed, 25 Oct 2023 09:34:23 +0100 Subject: [PATCH 25/34] feat: test_natural_keys implemented --- poetry.lock | 148 ++++++++++++++++++------------------- tests/test_natural_keys.py | 48 ++++++++++++ 2 files changed, 121 insertions(+), 75 deletions(-) create mode 100644 tests/test_natural_keys.py diff --git a/poetry.lock b/poetry.lock index a0e9a84d..4b506f75 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4,7 +4,7 @@ name = "alabaster" version = "0.7.13" description = "A configurable sidebar-enabled Sphinx theme" -optional = true +optional = false python-versions = ">=3.6" files = [ {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, @@ -80,7 +80,7 @@ files = [ name = "babel" version = "2.11.0" description = "Internationalization utilities" -optional = true +optional = false python-versions = ">=3.6" files = [ {file = "Babel-2.11.0-py3-none-any.whl", hash = "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe"}, @@ -339,62 +339,63 @@ testing = ["flake8", "pytest", "pytest-cov", "pytest-virtualenv", "pytest-xdist" [[package]] name = "coverage" -version = "7.1.0" +version = "7.3.2" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "coverage-7.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b946bbcd5a8231383450b195cfb58cb01cbe7f8949f5758566b881df4b33baf"}, - {file = "coverage-7.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec8e767f13be637d056f7e07e61d089e555f719b387a7070154ad80a0ff31801"}, - {file = "coverage-7.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a5a5879a939cb84959d86869132b00176197ca561c664fc21478c1eee60d75"}, - {file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b643cb30821e7570c0aaf54feaf0bfb630b79059f85741843e9dc23f33aaca2c"}, - {file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32df215215f3af2c1617a55dbdfb403b772d463d54d219985ac7cd3bf124cada"}, - {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:33d1ae9d4079e05ac4cc1ef9e20c648f5afabf1a92adfaf2ccf509c50b85717f"}, - {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:29571503c37f2ef2138a306d23e7270687c0efb9cab4bd8038d609b5c2393a3a"}, - {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:63ffd21aa133ff48c4dff7adcc46b7ec8b565491bfc371212122dd999812ea1c"}, - {file = "coverage-7.1.0-cp310-cp310-win32.whl", hash = "sha256:4b14d5e09c656de5038a3f9bfe5228f53439282abcab87317c9f7f1acb280352"}, - {file = "coverage-7.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:8361be1c2c073919500b6601220a6f2f98ea0b6d2fec5014c1d9cfa23dd07038"}, - {file = "coverage-7.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da9b41d4539eefd408c46725fb76ecba3a50a3367cafb7dea5f250d0653c1040"}, - {file = "coverage-7.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5b15ed7644ae4bee0ecf74fee95808dcc34ba6ace87e8dfbf5cb0dc20eab45a"}, - {file = "coverage-7.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d12d076582507ea460ea2a89a8c85cb558f83406c8a41dd641d7be9a32e1274f"}, - {file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2617759031dae1bf183c16cef8fcfb3de7617f394c813fa5e8e46e9b82d4222"}, - {file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4e4881fa9e9667afcc742f0c244d9364d197490fbc91d12ac3b5de0bf2df146"}, - {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9d58885215094ab4a86a6aef044e42994a2bd76a446dc59b352622655ba6621b"}, - {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ffeeb38ee4a80a30a6877c5c4c359e5498eec095878f1581453202bfacc8fbc2"}, - {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3baf5f126f30781b5e93dbefcc8271cb2491647f8283f20ac54d12161dff080e"}, - {file = "coverage-7.1.0-cp311-cp311-win32.whl", hash = "sha256:ded59300d6330be27bc6cf0b74b89ada58069ced87c48eaf9344e5e84b0072f7"}, - {file = "coverage-7.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:6a43c7823cd7427b4ed763aa7fb63901ca8288591323b58c9cd6ec31ad910f3c"}, - {file = "coverage-7.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a726d742816cb3a8973c8c9a97539c734b3a309345236cd533c4883dda05b8d"}, - {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc7c85a150501286f8b56bd8ed3aa4093f4b88fb68c0843d21ff9656f0009d6a"}, - {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b4198d85a3755d27e64c52f8c95d6333119e49fd001ae5798dac872c95e0f8"}, - {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb726cb861c3117a553f940372a495fe1078249ff5f8a5478c0576c7be12050"}, - {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:51b236e764840a6df0661b67e50697aaa0e7d4124ca95e5058fa3d7cbc240b7c"}, - {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7ee5c9bb51695f80878faaa5598040dd6c9e172ddcf490382e8aedb8ec3fec8d"}, - {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c31b75ae466c053a98bf26843563b3b3517b8f37da4d47b1c582fdc703112bc3"}, - {file = "coverage-7.1.0-cp37-cp37m-win32.whl", hash = "sha256:3b155caf3760408d1cb903b21e6a97ad4e2bdad43cbc265e3ce0afb8e0057e73"}, - {file = "coverage-7.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2a60d6513781e87047c3e630b33b4d1e89f39836dac6e069ffee28c4786715f5"}, - {file = "coverage-7.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2cba5c6db29ce991029b5e4ac51eb36774458f0a3b8d3137241b32d1bb91f06"}, - {file = "coverage-7.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beeb129cacea34490ffd4d6153af70509aa3cda20fdda2ea1a2be870dfec8d52"}, - {file = "coverage-7.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c45948f613d5d18c9ec5eaa203ce06a653334cf1bd47c783a12d0dd4fd9c851"}, - {file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef382417db92ba23dfb5864a3fc9be27ea4894e86620d342a116b243ade5d35d"}, - {file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c7c0d0827e853315c9bbd43c1162c006dd808dbbe297db7ae66cd17b07830f0"}, - {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e5cdbb5cafcedea04924568d990e20ce7f1945a1dd54b560f879ee2d57226912"}, - {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9817733f0d3ea91bea80de0f79ef971ae94f81ca52f9b66500c6a2fea8e4b4f8"}, - {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:218fe982371ac7387304153ecd51205f14e9d731b34fb0568181abaf7b443ba0"}, - {file = "coverage-7.1.0-cp38-cp38-win32.whl", hash = "sha256:04481245ef966fbd24ae9b9e537ce899ae584d521dfbe78f89cad003c38ca2ab"}, - {file = "coverage-7.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8ae125d1134bf236acba8b83e74c603d1b30e207266121e76484562bc816344c"}, - {file = "coverage-7.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2bf1d5f2084c3932b56b962a683074a3692bce7cabd3aa023c987a2a8e7612f6"}, - {file = "coverage-7.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:98b85dd86514d889a2e3dd22ab3c18c9d0019e696478391d86708b805f4ea0fa"}, - {file = "coverage-7.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38da2db80cc505a611938d8624801158e409928b136c8916cd2e203970dde4dc"}, - {file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3164d31078fa9efe406e198aecd2a02d32a62fecbdef74f76dad6a46c7e48311"}, - {file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db61a79c07331e88b9a9974815c075fbd812bc9dbc4dc44b366b5368a2936063"}, - {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ccb092c9ede70b2517a57382a601619d20981f56f440eae7e4d7eaafd1d1d09"}, - {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:33ff26d0f6cc3ca8de13d14fde1ff8efe1456b53e3f0273e63cc8b3c84a063d8"}, - {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d47dd659a4ee952e90dc56c97d78132573dc5c7b09d61b416a9deef4ebe01a0c"}, - {file = "coverage-7.1.0-cp39-cp39-win32.whl", hash = "sha256:d248cd4a92065a4d4543b8331660121b31c4148dd00a691bfb7a5cdc7483cfa4"}, - {file = "coverage-7.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7ed681b0f8e8bcbbffa58ba26fcf5dbc8f79e7997595bf071ed5430d8c08d6f3"}, - {file = "coverage-7.1.0-pp37.pp38.pp39-none-any.whl", hash = "sha256:755e89e32376c850f826c425ece2c35a4fc266c081490eb0a841e7c1cb0d3bda"}, - {file = "coverage-7.1.0.tar.gz", hash = "sha256:10188fe543560ec4874f974b5305cd1a8bdcfa885ee00ea3a03733464c4ca265"}, + {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, + {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, + {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, + {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, + {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, + {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, + {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, + {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, + {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, + {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, + {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, + {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, + {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, + {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, ] [package.dependencies] @@ -895,7 +896,7 @@ files = [ name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" -optional = true +optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, @@ -971,7 +972,7 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, @@ -1017,7 +1018,7 @@ dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils name = "m2r2" version = "0.3.2" description = "Markdown and reStructuredText in a single file." -optional = true +optional = false python-versions = "*" files = [ {file = "m2r2-0.3.2-py3-none-any.whl", hash = "sha256:d3684086b61b4bebe2307f15189495360f05a123c9bda2a66462649b7ca236aa"}, @@ -1032,7 +1033,7 @@ mistune = "0.8.4" name = "markupsafe" version = "2.1.2" description = "Safely add untrusted strings to HTML/XML markup." -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, @@ -1135,7 +1136,7 @@ files = [ name = "mistune" version = "0.8.4" description = "The fastest markdown parser in pure Python" -optional = true +optional = false python-versions = "*" files = [ {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, @@ -1535,7 +1536,7 @@ unidecode = ["Unidecode (>=1.1.1)"] name = "pytz" version = "2022.7.1" description = "World timezone definitions, modern and historical" -optional = true +optional = false python-versions = "*" files = [ {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, @@ -1806,7 +1807,7 @@ files = [ name = "sphinx" version = "7.1.2" description = "Python documentation generator" -optional = true +optional = false python-versions = ">=3.8" files = [ {file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"}, @@ -1841,7 +1842,7 @@ test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"] name = "sphinx-autodoc-typehints" version = "1.24.0" description = "Type hints (PEP 484) support for the Sphinx autodoc extension" -optional = true +optional = false python-versions = ">=3.8" files = [ {file = "sphinx_autodoc_typehints-1.24.0-py3-none-any.whl", hash = "sha256:6a73c0c61a9144ce2ed5ef2bed99d615254e5005c1cc32002017d72d69fb70e6"}, @@ -1860,7 +1861,7 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "p name = "sphinx-rtd-theme" version = "1.3.0" description = "Read the Docs theme for Sphinx" -optional = true +optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ {file = "sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0"}, @@ -1879,7 +1880,7 @@ dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] name = "sphinxcontrib-applehelp" version = "1.0.4" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -optional = true +optional = false python-versions = ">=3.8" files = [ {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, @@ -1894,7 +1895,7 @@ test = ["pytest"] name = "sphinxcontrib-devhelp" version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -optional = true +optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, @@ -1909,7 +1910,7 @@ test = ["pytest"] name = "sphinxcontrib-htmlhelp" version = "2.0.1" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -optional = true +optional = false python-versions = ">=3.8" files = [ {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, @@ -1924,7 +1925,7 @@ test = ["html5lib", "pytest"] name = "sphinxcontrib-jquery" version = "4.1" description = "Extension to include jQuery on newer Sphinx releases" -optional = true +optional = false python-versions = ">=2.7" files = [ {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, @@ -1938,7 +1939,7 @@ Sphinx = ">=1.8" name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" -optional = true +optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, @@ -1952,7 +1953,7 @@ test = ["flake8", "mypy", "pytest"] name = "sphinxcontrib-qthelp" version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -optional = true +optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, @@ -1967,7 +1968,7 @@ test = ["pytest"] name = "sphinxcontrib-serializinghtml" version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -optional = true +optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, @@ -2181,10 +2182,7 @@ files = [ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] -[extras] -docs = ["m2r2", "sphinx", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "tomlkit"] - [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "e6d5921cae74ffd69eaed42ee87f86ac22483eebd40c4a1f291f943e5ca5c2d0" +content-hash = "82367cec9cc9e59ca1f3604bc50e88ba6e2181ecb88ce9dcdc9cd925fd165887" diff --git a/tests/test_natural_keys.py b/tests/test_natural_keys.py new file mode 100644 index 00000000..8149b702 --- /dev/null +++ b/tests/test_natural_keys.py @@ -0,0 +1,48 @@ +from django.test import TestCase +from eav.models import Attribute, EnumGroup, EnumValue, Value +from test_project.models import Patient +import eav + + +class ModelTest(TestCase): + def setUp(self): + 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) + + EnumGroup.objects.create(name='Yes / No') + EnumValue.objects.create(value='yes') + EnumValue.objects.create(value='no') + EnumValue.objects.create(value='unknown') + + + def test_attr_natural_keys(self): + attr = Attribute.objects.get(name='age') + attr_natural_key = attr.natural_key() + attr_retrieved_model = Attribute.objects.get_by_natural_key(*attr_natural_key) + self.assertEqual(attr_retrieved_model, attr) + + def test_value_natural_keys(self): + p = Patient.objects.create(name='Jon') + p.eav.age = 5 + p.save() + + val = p.eav_values.first() + + value_natural_key = val.natural_key() + value_retrieved_model = Value.objects.get_by_natural_key(*value_natural_key) + self.assertEqual(value_retrieved_model, val) + + def test_enum_group_natural_keys(self): + 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) + self.assertEqual(enum_group_retrieved_model, enum_group) + + def test_enum_value_natural_keys(self): + 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) + self.assertEqual(enum_value_retrieved_model, enum_value) From 5c0b0d6ebfdcce15fa23833fa2b9f6fe3e30506d Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Mon, 4 Dec 2023 09:52:06 +0100 Subject: [PATCH 26/34] changelog updated --- CHANGELOG.md | 7 +++++++ eav/queryset.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 820f7a08..8dd64cc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ We follow [Semantic Versions](https://semver.org/) starting at the `0.14.0` rele ### Bug Fixes ### Features +## 1.5.1 (2023-12-04) + +### Bug Fixes + +- Fixes errors in migrations [#406](https://github.com/jazzband/django-eav2/issues/406) + + ## 1.5.0 (2023-11-08) ### Bug Fixes diff --git a/eav/queryset.py b/eav/queryset.py index b7797078..c3c08019 100644 --- a/eav/queryset.py +++ b/eav/queryset.py @@ -22,7 +22,7 @@ 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 2ff929281a7dd26e704e466dab95ca887031a1c3 Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Fri, 22 Mar 2024 15:49:47 +0100 Subject: [PATCH 27/34] tests + migration file updated --- .../0010_dynamic_pk_type_for_models.py | 30 +++++---- ...ttribute_id_alter_enumgroup_id_and_more.py | 12 ++-- test_project/migrations/0001_initial.py | 61 +++++++++---------- test_project/settings.py | 3 +- tests/test_models.py | 51 ++++++++++++++++ tests/test_registry.py | 4 +- tests/test_widgets.py | 39 ++++++++++++ 7 files changed, 148 insertions(+), 52 deletions(-) create mode 100644 tests/test_models.py create mode 100644 tests/test_widgets.py diff --git a/eav/migrations/0010_dynamic_pk_type_for_models.py b/eav/migrations/0010_dynamic_pk_type_for_models.py index 8e9c17b4..1d276ef3 100644 --- a/eav/migrations/0010_dynamic_pk_type_for_models.py +++ b/eav/migrations/0010_dynamic_pk_type_for_models.py @@ -1,10 +1,8 @@ -# Generated by Django 4.2.6 on 2023-12-04 08:31 - from django.db import migrations, models class Migration(migrations.Migration): - """Migration to set CharField as default primary key for all models.""" + """Migration to use BigAutoField as default for all models.""" dependencies = [ ('eav', '0009_enchance_naming'), @@ -14,29 +12,37 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='attribute', name='id', - field=models.CharField( - editable=False, max_length=255, primary_key=True, serialize=False + field=models.BigAutoField( + editable=False, + primary_key=True, + serialize=False, ), ), migrations.AlterField( model_name='enumgroup', name='id', - field=models.CharField( - editable=False, max_length=255, primary_key=True, serialize=False + field=models.BigAutoField( + editable=False, + primary_key=True, + serialize=False, ), ), migrations.AlterField( model_name='enumvalue', name='id', - field=models.CharField( - editable=False, max_length=255, primary_key=True, serialize=False + field=models.BigAutoField( + editable=False, + primary_key=True, + serialize=False, ), ), migrations.AlterField( model_name='value', name='id', - field=models.CharField( - editable=False, max_length=255, primary_key=True, serialize=False + field=models.BigAutoField( + editable=False, + primary_key=True, + serialize=False, ), ), - ] + ] \ No newline at end of file diff --git a/eav/migrations/0011_alter_attribute_id_alter_enumgroup_id_and_more.py b/eav/migrations/0011_alter_attribute_id_alter_enumgroup_id_and_more.py index 3b068290..9380a1bf 100644 --- a/eav/migrations/0011_alter_attribute_id_alter_enumgroup_id_and_more.py +++ b/eav/migrations/0011_alter_attribute_id_alter_enumgroup_id_and_more.py @@ -1,9 +1,9 @@ -# Generated by Django 4.2.6 on 2023-12-20 15:33 - from django.db import migrations, models class Migration(migrations.Migration): + """Migration to set CharField as default primary key for all models.""" + dependencies = [ ('eav', '0010_dynamic_pk_type_for_models'), ] @@ -13,28 +13,28 @@ class Migration(migrations.Migration): model_name='attribute', name='id', field=models.CharField( - editable=False, max_length=40, primary_key=True, serialize=False + editable=False, max_length=255, primary_key=True, serialize=False ), ), migrations.AlterField( model_name='enumgroup', name='id', field=models.CharField( - editable=False, max_length=40, primary_key=True, serialize=False + editable=False, max_length=255, primary_key=True, serialize=False ), ), migrations.AlterField( model_name='enumvalue', name='id', field=models.CharField( - editable=False, max_length=40, primary_key=True, serialize=False + editable=False, max_length=255, primary_key=True, serialize=False ), ), migrations.AlterField( model_name='value', name='id', field=models.CharField( - editable=False, max_length=40, primary_key=True, serialize=False + editable=False, max_length=255, primary_key=True, serialize=False ), ), ] diff --git a/test_project/migrations/0001_initial.py b/test_project/migrations/0001_initial.py index ca50039f..b843ad94 100644 --- a/test_project/migrations/0001_initial.py +++ b/test_project/migrations/0001_initial.py @@ -1,18 +1,35 @@ -import uuid +# Generated by Django 4.2.11 on 2024-03-22 12:04 from django.db import migrations, models - -from test_project.models import MAX_CHARFIELD_LEN +import django.db.models.deletion +import uuid class Migration(migrations.Migration): - """Initial migration for test_project.""" initial = True dependencies = [] operations = [ + migrations.CreateModel( + name='Doctor', + fields=[ + ( + 'id', + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ('name', models.CharField(max_length=254)), + ], + options={ + 'abstract': False, + }, + ), migrations.CreateModel( name='ExampleMetaclassModel', fields=[ @@ -25,7 +42,7 @@ class Migration(migrations.Migration): verbose_name='ID', ), ), - ('name', models.CharField(max_length=MAX_CHARFIELD_LEN)), + ('name', models.CharField(max_length=254)), ], options={ 'abstract': False, @@ -43,7 +60,7 @@ class Migration(migrations.Migration): verbose_name='ID', ), ), - ('name', models.CharField(max_length=MAX_CHARFIELD_LEN)), + ('name', models.CharField(max_length=254)), ], options={ 'abstract': False, @@ -61,7 +78,7 @@ class Migration(migrations.Migration): verbose_name='ID', ), ), - ('name', models.CharField(max_length=MAX_CHARFIELD_LEN)), + ('name', models.CharField(max_length=254)), ], options={ 'abstract': False, @@ -79,14 +96,14 @@ class Migration(migrations.Migration): verbose_name='ID', ), ), - ('name', models.CharField(max_length=MAX_CHARFIELD_LEN)), - ('email', models.EmailField(blank=True, max_length=MAX_CHARFIELD_LEN)), + ('name', models.CharField(max_length=254)), + ('email', models.EmailField(blank=True, max_length=254)), ( 'example', models.ForeignKey( blank=True, null=True, - on_delete=models.deletion.PROTECT, + on_delete=django.db.models.deletion.PROTECT, to='test_project.examplemodel', ), ), @@ -107,8 +124,8 @@ class Migration(migrations.Migration): verbose_name='ID', ), ), - ('name', models.CharField(max_length=MAX_CHARFIELD_LEN)), - ('models', models.ManyToManyField(to='test_project.ExampleModel')), + ('name', models.CharField(max_length=254)), + ('models', models.ManyToManyField(to='test_project.examplemodel')), ], options={ 'abstract': False, @@ -130,7 +147,7 @@ class Migration(migrations.Migration): ( 'patient', models.ForeignKey( - on_delete=models.deletion.PROTECT, + on_delete=django.db.models.deletion.PROTECT, to='test_project.patient', ), ), @@ -139,22 +156,4 @@ class Migration(migrations.Migration): 'abstract': False, }, ), - migrations.CreateModel( - name='Doctor', - fields=[ - ( - 'id', - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - ), - ), - ('name', models.CharField(max_length=MAX_CHARFIELD_LEN)), - ], - options={ - 'abstract': False, - }, - ), ] diff --git a/test_project/settings.py b/test_project/settings.py index 3a332ca7..b893d3df 100644 --- a/test_project/settings.py +++ b/test_project/settings.py @@ -67,12 +67,13 @@ 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:', + # 'NAME': BASE_DIR / 'db.sqlite3' }, } DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' -EAV2_PRIMARY_KEY_FIELD = 'django.db.models.CharField' +EAV2_PRIMARY_KEY_FIELD = 'django.db.models.BigAutoField' # Password validation diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 00000000..17a307a2 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,51 @@ +import eav +import datetime +from django.test import TestCase +from eav.models import EnumValue, EnumGroup, Attribute, Value +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType + + +class EnumValueTestCase(TestCase): + def setUp(self): + self.enum_value = EnumValue.objects.create(value='Test Value') + + def test_enum_value_str(self): + self.assertEqual(str(self.enum_value), 'Test Value') + + +class EnumGroupTestCase(TestCase): + def setUp(self): + self.enum_group = EnumGroup.objects.create(name='Test Group') + + def test_enum_group_str(self): + self.assertEqual(str(self.enum_group), 'Test Group') + + +class AttributeTestCase(TestCase): + def setUp(self): + self.attribute = Attribute.objects.create( + name='Test Attribute', datatype='text' + ) + + def test_attribute_str(self): + self.assertEqual(str(self.attribute), 'Test Attribute (Text)') + + +class ValueModelTestCase(TestCase): + def setUp(self): + eav.register(User) + self.attribute = Attribute.objects.create(name='Test Attribute', datatype=Attribute.TYPE_TEXT, slug="test_attribute") + self.user = User.objects.create(username='crazy_dev_user') + user_content_type = ContentType.objects.get_for_model(User) + self.value = Value.objects.create( + entity_id=self.user.id, + entity_ct=user_content_type, + value_text='Test Value', + attribute=self.attribute + ) + + def test_value_str(self): + expected_str = f'{self.attribute.name}: "{self.value.value_text}" ({self.value.entity_pk_int})' + self.assertEqual(str(self.value), expected_str) + diff --git a/tests/test_registry.py b/tests/test_registry.py index 877e5683..9854a16e 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -105,10 +105,10 @@ class Foo(object): def test_model_without_local_managers(self): """Test when a model doesn't have local_managers.""" # Check just in case test model changes in the future - assert bool(User._meta.local_managers) is False + assert bool(User._meta.local_managers) eav.register(User) assert isinstance(User.objects, eav.managers.EntityManager) # Reverse check: managers should be empty again eav.unregister(User) - assert bool(User._meta.local_managers) is False + assert not bool(User._meta.local_managers) diff --git a/tests/test_widgets.py b/tests/test_widgets.py new file mode 100644 index 00000000..21ba51dc --- /dev/null +++ b/tests/test_widgets.py @@ -0,0 +1,39 @@ +from django.core.exceptions import ValidationError +from django.forms import Textarea +from django.test import TestCase +from eav.widgets import CSVWidget + +class TestCSVWidget(TestCase): + def test_prep_value_string(self): + self._extracted_from_test_prep_value_empty_2("Test Value") + + def test_prep_value_list(self): + widget = CSVWidget() + value = ["Value 1", "Value 2", "Value 3"] + self.assertEqual(widget.prep_value(value), "Value 1;Value 2;Value 3") + + def test_prep_value_empty(self): + self._extracted_from_test_prep_value_empty_2("") + + # TODO Rename this here and in `test_prep_value_string` and `test_prep_value_empty` + def _extracted_from_test_prep_value_empty_2(self, arg0): + widget = CSVWidget() + value = arg0 + self.assertEqual(widget.prep_value(value), arg0) + + def test_prep_value_invalid(self): + widget = CSVWidget() + value = 123 # An invalid value + with self.assertRaises(ValidationError): + widget.prep_value(value) + + def test_render(self): + widget = CSVWidget() + name = "test_field" + value = ["Value 1", "Value 2", "Value 3"] + rendered_widget = widget.render(name, value) + # You can add more specific assertions based on the expected output + self.assertIsInstance(rendered_widget, str) + self.assertIn("Value 1", rendered_widget) + self.assertIn("Value 2", rendered_widget) + self.assertIn("Value 3", rendered_widget) From 3b8d28c49e1d32cd6b0e09608e7a014fd038d637 Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Fri, 22 Mar 2024 15:55:01 +0100 Subject: [PATCH 28/34] tests + migration file updated --- eav/migrations/0010_dynamic_pk_type_for_models.py | 2 +- tests/test_models.py | 7 ++++--- tests/test_widgets.py | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/eav/migrations/0010_dynamic_pk_type_for_models.py b/eav/migrations/0010_dynamic_pk_type_for_models.py index 1d276ef3..28028ac8 100644 --- a/eav/migrations/0010_dynamic_pk_type_for_models.py +++ b/eav/migrations/0010_dynamic_pk_type_for_models.py @@ -45,4 +45,4 @@ class Migration(migrations.Migration): serialize=False, ), ), - ] \ No newline at end of file + ] diff --git a/tests/test_models.py b/tests/test_models.py index 17a307a2..7ec046cc 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -35,17 +35,18 @@ def test_attribute_str(self): class ValueModelTestCase(TestCase): def setUp(self): eav.register(User) - self.attribute = Attribute.objects.create(name='Test Attribute', datatype=Attribute.TYPE_TEXT, slug="test_attribute") + self.attribute = Attribute.objects.create( + name='Test Attribute', datatype=Attribute.TYPE_TEXT, slug="test_attribute" + ) self.user = User.objects.create(username='crazy_dev_user') user_content_type = ContentType.objects.get_for_model(User) self.value = Value.objects.create( entity_id=self.user.id, entity_ct=user_content_type, value_text='Test Value', - attribute=self.attribute + attribute=self.attribute, ) def test_value_str(self): expected_str = f'{self.attribute.name}: "{self.value.value_text}" ({self.value.entity_pk_int})' self.assertEqual(str(self.value), expected_str) - diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 21ba51dc..01719f3b 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -3,6 +3,7 @@ from django.test import TestCase from eav.widgets import CSVWidget + class TestCSVWidget(TestCase): def test_prep_value_string(self): self._extracted_from_test_prep_value_empty_2("Test Value") From 4c6ceedf0401f002b91770f70322570ccb204a28 Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Fri, 22 Mar 2024 16:04:42 +0100 Subject: [PATCH 29/34] tests + migration file updated --- ...ttribute_id_alter_enumgroup_id_and_more.py | 41 +++++++++++++++++++ tests/__init__.py | 0 2 files changed, 41 insertions(+) create mode 100644 eav/migrations/0012_alter_attribute_id_alter_enumgroup_id_and_more.py create mode 100644 tests/__init__.py diff --git a/eav/migrations/0012_alter_attribute_id_alter_enumgroup_id_and_more.py b/eav/migrations/0012_alter_attribute_id_alter_enumgroup_id_and_more.py new file mode 100644 index 00000000..3ae7b20b --- /dev/null +++ b/eav/migrations/0012_alter_attribute_id_alter_enumgroup_id_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.11 on 2024-03-22 14:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('eav', '0011_alter_attribute_id_alter_enumgroup_id_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='attribute', + name='id', + field=models.BigAutoField( + editable=False, primary_key=True, serialize=False + ), + ), + migrations.AlterField( + model_name='enumgroup', + name='id', + field=models.BigAutoField( + editable=False, primary_key=True, serialize=False + ), + ), + migrations.AlterField( + model_name='enumvalue', + name='id', + field=models.BigAutoField( + editable=False, primary_key=True, serialize=False + ), + ), + migrations.AlterField( + model_name='value', + name='id', + field=models.BigAutoField( + editable=False, primary_key=True, serialize=False + ), + ), + ] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b From 08a26993c1317951c69557e4f7cfa91f6b2ad487 Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Fri, 22 Mar 2024 16:20:56 +0100 Subject: [PATCH 30/34] tests + migration file updated --- ...ttribute_id_alter_enumgroup_id_and_more.py | 41 ------------------- test_project/settings.py | 3 +- tests/test_registry.py | 4 +- 3 files changed, 3 insertions(+), 45 deletions(-) delete mode 100644 eav/migrations/0012_alter_attribute_id_alter_enumgroup_id_and_more.py diff --git a/eav/migrations/0012_alter_attribute_id_alter_enumgroup_id_and_more.py b/eav/migrations/0012_alter_attribute_id_alter_enumgroup_id_and_more.py deleted file mode 100644 index 3ae7b20b..00000000 --- a/eav/migrations/0012_alter_attribute_id_alter_enumgroup_id_and_more.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 4.2.11 on 2024-03-22 14:59 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('eav', '0011_alter_attribute_id_alter_enumgroup_id_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='attribute', - name='id', - field=models.BigAutoField( - editable=False, primary_key=True, serialize=False - ), - ), - migrations.AlterField( - model_name='enumgroup', - name='id', - field=models.BigAutoField( - editable=False, primary_key=True, serialize=False - ), - ), - migrations.AlterField( - model_name='enumvalue', - name='id', - field=models.BigAutoField( - editable=False, primary_key=True, serialize=False - ), - ), - migrations.AlterField( - model_name='value', - name='id', - field=models.BigAutoField( - editable=False, primary_key=True, serialize=False - ), - ), - ] diff --git a/test_project/settings.py b/test_project/settings.py index b893d3df..1d9163c1 100644 --- a/test_project/settings.py +++ b/test_project/settings.py @@ -66,8 +66,7 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:', - # 'NAME': BASE_DIR / 'db.sqlite3' + 'NAME': ':memory:' }, } diff --git a/tests/test_registry.py b/tests/test_registry.py index 9854a16e..877e5683 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -105,10 +105,10 @@ class Foo(object): def test_model_without_local_managers(self): """Test when a model doesn't have local_managers.""" # Check just in case test model changes in the future - assert bool(User._meta.local_managers) + assert bool(User._meta.local_managers) is False eav.register(User) assert isinstance(User.objects, eav.managers.EntityManager) # Reverse check: managers should be empty again eav.unregister(User) - assert not bool(User._meta.local_managers) + assert bool(User._meta.local_managers) is False From d35289f8b81f877faee72243cab9ab20b6b39a01 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:21:10 +0000 Subject: [PATCH 31/34] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test_project/settings.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test_project/settings.py b/test_project/settings.py index 1d9163c1..d92e1c86 100644 --- a/test_project/settings.py +++ b/test_project/settings.py @@ -64,10 +64,7 @@ # 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:'}, } From b2f81240c1eadc13e3f9480cf6c0f882fce70c97 Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Mon, 25 Mar 2024 09:47:25 +0100 Subject: [PATCH 32/34] tests + migration file updated --- eav/models.py | 872 ----------------------------------------- tests/test_models.py | 52 --- tests/test_registry.py | 2 +- 3 files changed, 1 insertion(+), 925 deletions(-) delete mode 100644 eav/models.py delete mode 100644 tests/test_models.py diff --git a/eav/models.py b/eav/models.py deleted file mode 100644 index a318545b..00000000 --- a/eav/models.py +++ /dev/null @@ -1,872 +0,0 @@ -""" -This module defines the four concrete, non-abstract models: - * :class:`Value` - * :class:`Attribute` - * :class:`EnumValue` - * :class:`EnumGroup` - -Along with the :class:`Entity` helper class and :class:`EAVModelMeta` -optional metaclass for each eav model class. -""" - -from copy import copy -from typing import Tuple - -from django.contrib.contenttypes import fields as generic -from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ValidationError -from django.core.serializers.json import DjangoJSONEncoder -from django.db import models -from django.db.models.base import ModelBase -from django.utils import timezone -from django.utils.translation import gettext_lazy as _ - -from eav import register -from eav.exceptions import IllegalAssignmentException -from eav.fields import CSVField, EavDatatypeField -from eav.logic.entity_pk import get_entity_pk_type -from eav.logic.managers import ( - AttributeManager, - EnumGroupManager, - EnumValueManager, - ValueManager, -) -from eav.logic.object_pk import get_pk_format -from eav.logic.slug import SLUGFIELD_MAX_LENGTH, generate_slug -from eav.logic.object_pk import get_pk_format -from eav.validators import ( - validate_bool, - validate_csv, - validate_date, - validate_enum, - validate_float, - validate_int, - validate_json, - validate_object, - validate_text, -) - -try: - from typing import Final -except ImportError: - from typing_extensions import Final - - -CHARFIELD_LENGTH: Final = 100 - - -class EnumValue(models.Model): - """ - *EnumValue* objects are the value 'choices' to multiple choice *TYPE_ENUM* - :class:`Attribute` objects. They have only one field, *value*, a - ``CharField`` that must be unique. - - For example:: - - yes = EnumValue.objects.create(value='Yes') # doctest: SKIP - no = EnumValue.objects.create(value='No') - unknown = EnumValue.objects.create(value='Unknown') - - ynu = EnumGroup.objects.create(name='Yes / No / Unknown') - ynu.values.add(yes, no, unknown) - - Attribute.objects.create(name='has fever?', - datatype=Attribute.TYPE_ENUM, enum_group=ynu) - # = - - .. note:: - The same *EnumValue* objects should be reused within multiple - *EnumGroups*. For example, if you have one *EnumGroup* called: *Yes / - No / Unknown* and another called *Yes / No / Not applicable*, you should - only have a total of four *EnumValues* objects, as you should have used - the same *Yes* and *No* *EnumValues* for both *EnumGroups*. - """ - - objects = EnumValueManager() - - class Meta: - verbose_name = _('EnumValue') - verbose_name_plural = _('EnumValues') - - id = get_pk_format() - - value = models.CharField( - _('Value'), - db_index=True, - unique=True, - max_length=SLUGFIELD_MAX_LENGTH, - ) - - def natural_key(self) -> Tuple[str]: - """ - Retrieve the natural key for the EnumValue instance. - - The natural key for an EnumValue is defined by its `value`. This method returns - the value of the instance as a single-element tuple. - - Returns: - tuple: A tuple containing the value of the EnumValue instance. - """ - return (self.value,) - - def __str__(self): - """String representation of `EnumValue` instance.""" - return str( - self.value, - ) - - def __repr__(self): - """String representation of `EnumValue` object.""" - return ''.format(self.value) - - -class EnumGroup(models.Model): - """ - *EnumGroup* objects have two fields - a *name* ``CharField`` and *values*, - a ``ManyToManyField`` to :class:`EnumValue`. :class:`Attribute` classes - with datatype *TYPE_ENUM* have a ``ForeignKey`` field to *EnumGroup*. - - See :class:`EnumValue` for an example. - """ - - objects = EnumGroupManager() - - class Meta: - verbose_name = _('EnumGroup') - verbose_name_plural = _('EnumGroups') - - id = get_pk_format() - - name = models.CharField( - unique=True, - max_length=CHARFIELD_LENGTH, - verbose_name=_('Name'), - ) - values = models.ManyToManyField( - EnumValue, - verbose_name=_('Enum group'), - ) - - def natural_key(self) -> Tuple[str]: - """ - Retrieve the natural key for the EnumGroup instance. - - The natural key for an EnumGroup is defined by its `name`. This method - returns the name of the instance as a single-element tuple. - - Returns: - tuple: A tuple containing the name of the EnumGroup instance. - """ - return (self.name,) - - def __str__(self): - """String representation of `EnumGroup` instance.""" - return str(self.name) - - def __repr__(self): - """String representation of `EnumGroup` object.""" - return ''.format(self.name) - - -class Attribute(models.Model): - """ - Putting the **A** in *EAV*. This holds the attributes, or concepts. - Examples of possible *Attributes*: color, height, weight, number of - children, number of patients, has fever?, etc... - - Each attribute has a name, and a description, along with a slug that must - be unique. If you don't provide a slug, a default slug (derived from - name), will be created. - - The *required* field is a boolean that indicates whether this EAV attribute - is required for entities to which it applies. It defaults to *False*. - - .. warning:: - Just like a normal model field that is required, you will not be able - to save or create any entity object for which this attribute applies, - without first setting this EAV attribute. - - There are 7 possible values for datatype: - - * int (TYPE_INT) - * float (TYPE_FLOAT) - * text (TYPE_TEXT) - * date (TYPE_DATE) - * bool (TYPE_BOOLEAN) - * object (TYPE_OBJECT) - * enum (TYPE_ENUM) - * json (TYPE_JSON) - * csv (TYPE_CSV) - - - Examples:: - - Attribute.objects.create(name='Height', datatype=Attribute.TYPE_INT) - # = - - Attribute.objects.create(name='Color', datatype=Attribute.TYPE_TEXT) - # = - - yes = EnumValue.objects.create(value='yes') - no = EnumValue.objects.create(value='no') - unknown = EnumValue.objects.create(value='unknown') - ynu = EnumGroup.objects.create(name='Yes / No / Unknown') - ynu.values.add(yes, no, unknown) - - Attribute.objects.create(name='has fever?', datatype=Attribute.TYPE_ENUM, enum_group=ynu) - # = - - .. warning:: Once an Attribute has been used by an entity, you can not - change it's datatype. - """ - - objects = AttributeManager() - - class Meta: - ordering = ['name'] - verbose_name = _('Attribute') - verbose_name_plural = _('Attributes') - - TYPE_TEXT = 'text' - TYPE_FLOAT = 'float' - TYPE_INT = 'int' - TYPE_DATE = 'date' - TYPE_BOOLEAN = 'bool' - TYPE_OBJECT = 'object' - TYPE_ENUM = 'enum' - TYPE_JSON = 'json' - TYPE_CSV = 'csv' - - DATATYPE_CHOICES = ( - (TYPE_TEXT, _("Texte")), - (TYPE_DATE, _("Date")), - (TYPE_FLOAT, _("Nombre décimal")), - (TYPE_INT, _("Entier")), - (TYPE_BOOLEAN, _("Vrai / Faux")), - (TYPE_OBJECT, _("Django Object")), - (TYPE_ENUM, _("Multiple Choice")), - (TYPE_JSON, _("JSON Object")), - (TYPE_CSV, _("Comma-Separated-Value")), - ) - - # Core attributes - id = get_pk_format() - - datatype = EavDatatypeField( - choices=DATATYPE_CHOICES, - max_length=6, - verbose_name=_('Data Type'), - ) - - name = models.CharField( - max_length=CHARFIELD_LENGTH, - help_text=_('User-friendly attribute name'), - verbose_name=_('Name'), - ) - - """ - Main identifer for the attribute. - Upon creation, slug is autogenerated from the name. - (see :meth:`~eav.fields.EavSlugField.create_slug_from_name`). - """ - slug = models.SlugField( - max_length=SLUGFIELD_MAX_LENGTH, - db_index=True, - unique=True, - help_text=_('Short unique attribute label'), - verbose_name=_('Slug'), - ) - - """ - .. warning:: - This attribute should be used with caution. Setting this to *True* - means that *all* entities that *can* have this attribute will - be required to have a value for it. - """ - required = models.BooleanField( - default=False, - verbose_name=_('Required'), - ) - - entity_ct = models.ManyToManyField( - ContentType, - blank=True, - verbose_name=_('Entity content type'), - ) - """ - This field allows you to specify a relationship with any number of content types. - This would be useful, for example, if you wanted an attribute to apply only to - a subset of entities. In that case, you could filter by content type in the - :meth:`~eav.registry.EavConfig.get_attributes` method of that entity's config. - """ - - enum_group = models.ForeignKey( - EnumGroup, - on_delete=models.PROTECT, - blank=True, - null=True, - verbose_name=_('Choice Group'), - ) - - description = models.CharField( - max_length=256, - blank=True, - null=True, - help_text=_('Short description'), - verbose_name=_('Description'), - ) - - # Useful meta-information - - display_order = models.PositiveIntegerField( - default=1, - verbose_name=_('Display order'), - ) - - modified = models.DateTimeField( - auto_now=True, - verbose_name=_('Modified'), - ) - - created = models.DateTimeField( - default=timezone.now, - editable=False, - verbose_name=_('Created'), - ) - - def 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. - - Returns: - tuple: A tuple containing the name and slug of the Attribute instance. - """ - return ( - self.name, - self.slug, - ) - - @property - def help_text(self): - return self.description - - def get_validators(self): - """ - Returns the appropriate validator function from :mod:`~eav.validators` - as a list (of length one) for the datatype. - - .. note:: - The reason it returns it as a list, is eventually we may want this - method to look elsewhere for additional attribute specific - validators to return as well as the default, built-in one. - """ - DATATYPE_VALIDATORS = { - 'text': validate_text, - 'float': validate_float, - 'int': validate_int, - 'date': validate_date, - 'bool': validate_bool, - 'object': validate_object, - 'enum': validate_enum, - 'json': validate_json, - 'csv': validate_csv, - } - - return [DATATYPE_VALIDATORS[self.datatype]] - - def validate_value(self, value): - """ - Check *value* against the validators returned by - :meth:`get_validators` for this attribute. - """ - for validator in self.get_validators(): - validator(value) - - if self.datatype == self.TYPE_ENUM: - if isinstance(value, EnumValue): - value = value.value - if not self.enum_group.values.filter(value=value).exists(): - raise ValidationError( - _('%(val)s is not a valid choice for %(attr)s') - % dict(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(Attribute, self).save(*args, **kwargs) - - def clean(self): - """ - Validates the attribute. Will raise ``ValidationError`` if the - attribute's datatype is *TYPE_ENUM* and enum_group is not set, or if - the attribute is not *TYPE_ENUM* and the enum group is set. - """ - if self.datatype == self.TYPE_ENUM and not self.enum_group: - raise ValidationError( - _('You must set the choice group for multiple choice attributes') - ) - - if self.datatype != self.TYPE_ENUM and self.enum_group: - raise ValidationError( - _('You can only assign a choice group to multiple choice attributes') - ) - - def get_choices(self): - """ - Returns a query set of :class:`EnumValue` objects for this attribute. - Returns None if the datatype of this attribute is not *TYPE_ENUM*. - """ - return ( - self.enum_group.values.all() - if self.datatype == Attribute.TYPE_ENUM - else None - ) - - def save_value(self, entity, value): - """ - Called with *entity*, any Django object registered with eav, and - *value*, the :class:`Value` this attribute for *entity* should - be set to. - - If a :class:`Value` object for this *entity* and attribute doesn't - exist, one will be created. - - .. note:: - If *value* is None and a :class:`Value` object exists for this - Attribute and *entity*, it will delete that :class:`Value` object. - """ - ct = ContentType.objects.get_for_model(entity) - - entity_filter = { - 'entity_ct': ct, - 'attribute': self, - '{0}'.format(get_entity_pk_type(entity)): entity.pk, - } - - try: - value_obj = self.value_set.get(**entity_filter) - except Value.DoesNotExist: - if value == None or value == '': - return - - value_obj = Value.objects.create(**entity_filter) - - if value == None or value == '': - value_obj.delete() - return - - if value != value_obj.value: - value_obj.value = value - value_obj.save() - - def __str__(self): - return '{} ({})'.format(self.name, self.get_datatype_display()) - - -class Value(models.Model): # noqa: WPS110 - """Putting the **V** in *EAV*. - - This model stores the value for one particular :class:`Attribute` for - some entity. - - As with most EAV implementations, most of the columns of this model will - be blank, as onle one *value_* field will be used. - - Example:: - - import eav - from django.contrib.auth.models import User - - eav.register(User) - - u = User.objects.create(username='crazy_dev_user') - a = Attribute.objects.create(name='Fav Drink', datatype='text') - - Value.objects.create(entity = u, attribute = a, value_text = 'red bull') - # = - """ - - objects = ValueManager() - - class Meta: - verbose_name = _('Value') - verbose_name_plural = _('Values') - - id = get_pk_format() - - # Direct foreign keys - attribute = models.ForeignKey( - Attribute, - db_index=True, - on_delete=models.PROTECT, - verbose_name=_('Attribute'), - ) - - # Entity generic relationships. Rather than rely on database casting, - # this will instead use a separate ForeignKey field attribute that matches - # the FK type of the entity. - entity_id = models.IntegerField( - blank=True, - null=True, - verbose_name=_('Entity id'), - ) - - entity_uuid = models.UUIDField( - blank=True, - null=True, - verbose_name=_('Entity uuid'), - ) - - entity_ct = models.ForeignKey( - ContentType, - on_delete=models.PROTECT, - related_name='value_entities', - verbose_name=_('Entity ct'), - ) - - entity_pk_int = generic.GenericForeignKey( - ct_field='entity_ct', - fk_field='entity_id', - ) - - entity_pk_uuid = generic.GenericForeignKey( - ct_field='entity_ct', - fk_field='entity_uuid', - ) - - # Model attributes - created = models.DateTimeField( - default=timezone.now, - verbose_name=_('Created'), - ) - - modified = models.DateTimeField( - auto_now=True, - verbose_name=_('Modified'), - ) - - # Value attributes - value_bool = models.BooleanField( - blank=True, - null=True, - verbose_name=_('Value bool'), - ) - value_csv = CSVField( - blank=True, - null=True, - verbose_name=_('Value CSV'), - ) - value_date = models.DateTimeField( - blank=True, - null=True, - verbose_name=_('Value date'), - ) - value_float = models.FloatField( - blank=True, - null=True, - verbose_name=_('Value float'), - ) - value_int = models.BigIntegerField( - blank=True, - null=True, - verbose_name=_('Value int'), - ) - value_text = models.TextField( - blank=True, - null=True, - verbose_name=_('Value text'), - ) - - value_json = models.JSONField( - default=dict, - encoder=DjangoJSONEncoder, - blank=True, - null=True, - verbose_name=_('Value JSON'), - ) - - value_enum = models.ForeignKey( - EnumValue, - blank=True, - null=True, - on_delete=models.PROTECT, - related_name='eav_values', - verbose_name=_('Value enum'), - ) - - # Value object relationship - generic_value_id = models.IntegerField( - blank=True, - null=True, - verbose_name=_('Generic value id'), - ) - - generic_value_ct = models.ForeignKey( - ContentType, - blank=True, - null=True, - on_delete=models.PROTECT, - related_name='value_values', - verbose_name=_('Generic value content type'), - ) - - value_object = generic.GenericForeignKey( - ct_field='generic_value_ct', - fk_field='generic_value_id', - ) - - def natural_key(self) -> Tuple[Tuple[str, str], int, str]: - """ - Retrieve the natural key for the Value instance. - - The natural key for a Value is a combination of its `attribute` natural key, - `entity_id`, and `entity_uuid`. This method returns a tuple containing these - three elements. - - Returns: - tuple: A tuple containing the natural key of the attribute, entity ID, - and entity UUID of the Value instance. - """ - return (self.attribute.natural_key(), self.entity_id, self.entity_uuid) - - def __str__(self): - """String representation of a Value.""" - entity = self.entity_pk_uuid if self.entity_uuid else self.entity_pk_int - return '{0}: "{1}" ({2})'.format( - self.attribute.name, - self.value, - entity, - ) - - def __repr__(self): - """Representation of Value object.""" - entity = self.entity_pk_uuid if self.entity_uuid else self.entity_pk_int - return '{0}: "{1}" ({2})'.format( - 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, 'value_{0}'.format(self.attribute.datatype)) - - def _set_value(self, new_value): - """Set the object this value is holding.""" - setattr(self, 'value_{0}'.format(self.attribute.datatype), new_value) - - value = property(_get_value, _set_value) # noqa: WPS110 - - -class Entity(object): - """ - The helper class that will be attached to any entity - registered with eav. - """ - - @staticmethod - def pre_save_handler(sender, *args, **kwargs): - """ - Pre save handler attached to self.instance. Called before the - model instance we are attached to is saved. This allows us to call - :meth:`validate_attributes` before the entity is saved. - """ - instance = kwargs['instance'] - entity = getattr(kwargs['instance'], instance._eav_config_cls.eav_attr) - entity.validate_attributes() - - @staticmethod - def post_save_handler(sender, *args, **kwargs): - """ - Post save handler attached to self.instance. Calls :meth:`save` when - the model instance we are attached to is saved. - """ - instance = kwargs['instance'] - entity = getattr(instance, instance._eav_config_cls.eav_attr) - entity.save() - - def __init__(self, instance): - """ - Set self.instance equal to the instance of the model that we're attached - to. Also, store the content type of that instance. - """ - self.instance = instance - self.ct = ContentType.objects.get_for_model(instance) - - def __getattr__(self, name): - """ - Tha magic getattr helper. This is called whenever user invokes:: - - instance. - - Checks if *name* is a valid slug for attributes available to this - instances. If it is, tries to lookup the :class:`Value` with that - attribute slug. If there is one, it returns the value of the - class:`Value` object, otherwise it hasn't been set, so it returns - None. - """ - if not name.startswith('_'): - try: - attribute = self.get_attribute_by_slug(name) - except Attribute.DoesNotExist: - raise AttributeError( - _('%(obj)s has no EAV attribute named %(attr)s') - % dict(obj=self.instance, attr=name) - ) - - try: - return self.get_value_by_attribute(attribute).value - except Value.DoesNotExist: - return None - - return getattr(super(Entity, self), name) - - def get_all_attributes(self): - """ - Return a query set of all :class:`Attribute` objects that can be set - for this entity. - """ - return self.instance._eav_config_cls.get_attributes( - instance=self.instance - ).order_by('display_order') - - def _hasattr(self, attribute_slug): - """ - Since we override __getattr__ with a backdown to the database, this - exists as a way of checking whether a user has set a real attribute on - ourselves, without going to the db if not. - """ - return attribute_slug in self.__dict__ - - def _getattr(self, attribute_slug): - """ - Since we override __getattr__ with a backdown to the database, this - exists as a way of getting the value a user set for one of our - attributes, without going to the db to check. - """ - return self.__dict__[attribute_slug] - - def save(self): - """ - Saves all the EAV values that have been set on this entity. - """ - for attribute in self.get_all_attributes(): - if self._hasattr(attribute.slug): - attribute_value = self._getattr(attribute.slug) - if attribute.datatype == Attribute.TYPE_ENUM and not isinstance( - attribute_value, EnumValue - ): - if attribute_value is not None: - attribute_value = EnumValue.objects.get(value=attribute_value) - attribute.save_value(self.instance, attribute_value) - - def validate_attributes(self): - """ - Called before :meth:`save`, first validate all the entity values to - make sure they can be created / saved cleanly. - Raises ``ValidationError`` if they can't be. - """ - values_dict = self.get_values_dict() - - for attribute in self.get_all_attributes(): - value = None - - # Value was assigned to this instance. - if self._hasattr(attribute.slug): - value = self._getattr(attribute.slug) - values_dict.pop(attribute.slug, None) - # Otherwise try pre-loaded from DB. - else: - value = values_dict.pop(attribute.slug, None) - - if value is None: - if attribute.required: - raise ValidationError( - _('{} EAV field cannot be blank'.format(attribute.slug)) - ) - else: - try: - attribute.validate_value(value) - except ValidationError as e: - raise ValidationError( - _('%(attr)s EAV field %(err)s') - % dict(attr=attribute.slug, err=e) - ) - - illegal = values_dict or ( - self.get_object_attributes() - self.get_all_attribute_slugs() - ) - - if illegal: - raise IllegalAssignmentException( - 'Instance of the class {} cannot have values for attributes: {}.'.format( - self.instance.__class__, ', '.join(illegal) - ) - ) - - def get_values_dict(self): - return {v.attribute.slug: v.value for v in self.get_values()} - - def get_values(self): - """Get all set :class:`Value` objects for self.instance.""" - entity_filter = { - 'entity_ct': self.ct, - '{0}'.format(get_entity_pk_type(self.instance)): self.instance.pk, - } - - return Value.objects.filter(**entity_filter).select_related() - - def get_all_attribute_slugs(self): - """ - Returns a list of slugs for all attributes available to this entity. - """ - return set(self.get_all_attributes().values_list('slug', flat=True)) - - def get_attribute_by_slug(self, slug): - """ - Returns a single :class:`Attribute` with *slug*. - """ - return self.get_all_attributes().get(slug=slug) - - def get_value_by_attribute(self, attribute): - """ - Returns a single :class:`Value` for *attribute*. - """ - return self.get_values().get(attribute=attribute) - - def get_object_attributes(self): - """ - Returns entity instance attributes, except for - ``instance`` and ``ct`` which are used internally. - """ - return set(copy(self.__dict__).keys()) - set(['instance', 'ct']) - - def __iter__(self): - """ - Iterate over set eav values. This would allow you to do:: - - for i in m.eav: print(i) - """ - return iter(self.get_values()) - - -class EAVModelMeta(ModelBase): - def __new__(cls, name, bases, namespace, **kwds): - result = super(EAVModelMeta, cls).__new__(cls, name, bases, dict(namespace)) - register(result) - return result diff --git a/tests/test_models.py b/tests/test_models.py deleted file mode 100644 index 7ec046cc..00000000 --- a/tests/test_models.py +++ /dev/null @@ -1,52 +0,0 @@ -import eav -import datetime -from django.test import TestCase -from eav.models import EnumValue, EnumGroup, Attribute, Value -from django.contrib.auth.models import User -from django.contrib.contenttypes.models import ContentType - - -class EnumValueTestCase(TestCase): - def setUp(self): - self.enum_value = EnumValue.objects.create(value='Test Value') - - def test_enum_value_str(self): - self.assertEqual(str(self.enum_value), 'Test Value') - - -class EnumGroupTestCase(TestCase): - def setUp(self): - self.enum_group = EnumGroup.objects.create(name='Test Group') - - def test_enum_group_str(self): - self.assertEqual(str(self.enum_group), 'Test Group') - - -class AttributeTestCase(TestCase): - def setUp(self): - self.attribute = Attribute.objects.create( - name='Test Attribute', datatype='text' - ) - - def test_attribute_str(self): - self.assertEqual(str(self.attribute), 'Test Attribute (Text)') - - -class ValueModelTestCase(TestCase): - def setUp(self): - eav.register(User) - self.attribute = Attribute.objects.create( - name='Test Attribute', datatype=Attribute.TYPE_TEXT, slug="test_attribute" - ) - self.user = User.objects.create(username='crazy_dev_user') - user_content_type = ContentType.objects.get_for_model(User) - self.value = Value.objects.create( - entity_id=self.user.id, - entity_ct=user_content_type, - value_text='Test Value', - attribute=self.attribute, - ) - - def test_value_str(self): - expected_str = f'{self.attribute.name}: "{self.value.value_text}" ({self.value.entity_pk_int})' - self.assertEqual(str(self.value), expected_str) diff --git a/tests/test_registry.py b/tests/test_registry.py index 877e5683..4cde36e4 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -111,4 +111,4 @@ def test_model_without_local_managers(self): # Reverse check: managers should be empty again eav.unregister(User) - assert bool(User._meta.local_managers) is False + assert bool(User._meta.local_managers) is False \ No newline at end of file From b011b1beeea3036513d1be175ed558e4ab4943af Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 08:47:38 +0000 Subject: [PATCH 33/34] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_registry.py b/tests/test_registry.py index 4cde36e4..877e5683 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -111,4 +111,4 @@ def test_model_without_local_managers(self): # Reverse check: managers should be empty again eav.unregister(User) - assert bool(User._meta.local_managers) is False \ No newline at end of file + assert bool(User._meta.local_managers) is False From 7fb9e7cc297648cebd0987b3ebf91df42a10fc90 Mon Sep 17 00:00:00 2001 From: mathiasag7 Date: Fri, 29 Mar 2024 10:59:36 +0100 Subject: [PATCH 34/34] tests + migration file updated --- CONTRIBUTING.md | 27 ++++++++++++- .../0010_dynamic_pk_type_for_models.py | 27 +++++-------- ...ttribute_id_alter_enumgroup_id_and_more.py | 40 ------------------- test_project/settings.py | 6 +-- 4 files changed, 38 insertions(+), 62 deletions(-) delete mode 100644 eav/migrations/0011_alter_attribute_id_alter_enumgroup_id_and_more.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba24fa05..7094e135 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,6 +3,7 @@ This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines). # Contributing + We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: - Reporting a bug @@ -23,6 +24,19 @@ poetry install To activate your `virtualenv` run `poetry shell`. +## Configuration + +Set EAV2_PRIMARY_KEY_FIELD value to `django.db.models.UUIDField` or `django.db.models.BigAutoField` in your settings. + +```python +EAV2_PRIMARY_KEY_FIELD = "django.db.models.UUIDField" # as example +``` + +and run +```bash +python manage.py makemigrations +python manage.py migrate +``` ## Tests @@ -34,10 +48,20 @@ To run all tests: pytest ``` +## Cleanup + +At the end of the test, ensure that you delete the migration file created by the EAV2_PRIMARY_KEY_FIELD change. Additionally, verify that the migration files are clean and reset the value to django.db.models.CharField in your settings. + +```python +EAV2_PRIMARY_KEY_FIELD = "django.db.models.CharField" +``` + ## We develop with Github + We use github to host code, to track issues and feature requests, as well as accept pull requests. ### We use [Github Flow](https://guides.github.com/introduction/flow/index.html), so all code changes from community happen through pull requests + Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests: 1. Fork the repo and create your branch from `master`. @@ -48,11 +72,12 @@ Pull requests are the best way to propose changes to the codebase (we use [Githu 6. Describe the pull request using [this](https://github.com/jazzband/django-eav2/blob/master/PULL_REQUEST_TEMPLATE.md) template. ### Any contributions you make will be under the GNU Lesser General Public License v3.0 + In short, when you submit code changes, your submissions are understood to be under the same [LGPLv3](https://choosealicense.com/licenses/lgpl-3.0/) that covers the project. Feel free to contact the maintainers if that's a concern. ### Report bugs using Github's [issues](https://github.com/jazzband/django-eav2/issues) -We use GitHub issues to track public bugs. Report a bug by opening a new issue. Use [this](https://github.com/jazzband/django-eav2/blob/master/.github/ISSUE_TEMPLATE/bug_report.md) template to describe your reports. +We use GitHub issues to track public bugs. Report a bug by opening a new issue. Use [this](https://github.com/jazzband/django-eav2/blob/master/.github/ISSUE_TEMPLATE/bug_report.md) template to describe your reports. ### Use a consistent coding style diff --git a/eav/migrations/0010_dynamic_pk_type_for_models.py b/eav/migrations/0010_dynamic_pk_type_for_models.py index 28028ac8..c5054b6f 100644 --- a/eav/migrations/0010_dynamic_pk_type_for_models.py +++ b/eav/migrations/0010_dynamic_pk_type_for_models.py @@ -1,8 +1,9 @@ +# Generated by Django 4.2.11 on 2024-03-26 09:01 + from django.db import migrations, models class Migration(migrations.Migration): - """Migration to use BigAutoField as default for all models.""" dependencies = [ ('eav', '0009_enchance_naming'), @@ -12,37 +13,29 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='attribute', name='id', - field=models.BigAutoField( - editable=False, - primary_key=True, - serialize=False, + field=models.CharField( + editable=False, max_length=40, primary_key=True, serialize=False ), ), migrations.AlterField( model_name='enumgroup', name='id', - field=models.BigAutoField( - editable=False, - primary_key=True, - serialize=False, + field=models.CharField( + editable=False, max_length=40, primary_key=True, serialize=False ), ), migrations.AlterField( model_name='enumvalue', name='id', - field=models.BigAutoField( - editable=False, - primary_key=True, - serialize=False, + field=models.CharField( + editable=False, max_length=40, primary_key=True, serialize=False ), ), migrations.AlterField( model_name='value', name='id', - field=models.BigAutoField( - editable=False, - primary_key=True, - serialize=False, + field=models.CharField( + editable=False, max_length=40, primary_key=True, serialize=False ), ), ] diff --git a/eav/migrations/0011_alter_attribute_id_alter_enumgroup_id_and_more.py b/eav/migrations/0011_alter_attribute_id_alter_enumgroup_id_and_more.py deleted file mode 100644 index 9380a1bf..00000000 --- a/eav/migrations/0011_alter_attribute_id_alter_enumgroup_id_and_more.py +++ /dev/null @@ -1,40 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - """Migration to set CharField as default primary key for all models.""" - - dependencies = [ - ('eav', '0010_dynamic_pk_type_for_models'), - ] - - operations = [ - migrations.AlterField( - model_name='attribute', - name='id', - field=models.CharField( - editable=False, max_length=255, primary_key=True, serialize=False - ), - ), - migrations.AlterField( - model_name='enumgroup', - name='id', - field=models.CharField( - editable=False, max_length=255, primary_key=True, serialize=False - ), - ), - migrations.AlterField( - model_name='enumvalue', - name='id', - field=models.CharField( - editable=False, max_length=255, primary_key=True, serialize=False - ), - ), - migrations.AlterField( - model_name='value', - name='id', - field=models.CharField( - editable=False, max_length=255, primary_key=True, serialize=False - ), - ), - ] diff --git a/test_project/settings.py b/test_project/settings.py index d92e1c86..7cfab18b 100644 --- a/test_project/settings.py +++ b/test_project/settings.py @@ -63,13 +63,11 @@ # Database # https://docs.djangoproject.com/en/3.1/ref/settings/#databases -DATABASES = { - 'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:'}, -} +DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:'}} DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' -EAV2_PRIMARY_KEY_FIELD = 'django.db.models.BigAutoField' +EAV2_PRIMARY_KEY_FIELD = 'django.db.models.CharField' # Password validation