From 3b1be10e9b9d9368c1d65be703f0d5662128129e Mon Sep 17 00:00:00 2001 From: mathiasag7 <50689712+mathiasag7@users.noreply.github.com> Date: Mon, 30 Oct 2023 16:57:29 +0100 Subject: [PATCH] feat: Provide UUIDField option for pk field and a natural key feature for serialization (#428) --- CHANGELOG.md | 13 ++- README.md | 39 +++++++ eav/fields.py | 4 + eav/logic/managers.py | 97 ++++++++++++++++ eav/logic/object_pk.py | 45 ++++++++ .../0010_dynamic_pk_type_for_models.py | 48 ++++++++ eav/models.py | 90 +++++++++++++-- poetry.lock | 107 +++++++++--------- test_project/settings.py | 3 + tests/test_attributes.py | 13 ++- tests/test_natural_keys.py | 51 +++++++++ tests/test_primary_key_format.py | 33 ++++++ 12 files changed, 478 insertions(+), 65 deletions(-) create mode 100644 eav/logic/managers.py create mode 100644 eav/logic/object_pk.py create mode 100644 eav/migrations/0010_dynamic_pk_type_for_models.py create mode 100644 tests/test_natural_keys.py create mode 100644 tests/test_primary_key_format.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f640ae3..ed517b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ We follow [Semantic Versions](https://semver.org/) starting at the `0.14.0` rele - Fixes querying with multiple eav kwargs [#395](https://github.com/jazzband/django-eav2/issues/395) +### Features + +- 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) ### Features @@ -46,6 +51,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) @@ -53,6 +59,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) @@ -83,7 +90,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) @@ -104,7 +111,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) @@ -113,6 +120,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 4866d67..8778e17 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ However, it is important to note that: 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 Install with pip @@ -102,6 +103,44 @@ INSTALLED_APPS = [ ] ``` +Add `django.db.models.UUIDField` or `django.db.models.BigAutoField` as value of `EAV2_PRIMARY_KEY_FIELD` in your settings + +``` python +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 `EAV2_PRIMARY_KEY_FIELD` in your settings. + +##### Step 1 + Change the value of `EAV2_PRIMARY_KEY_FIELD` into `django.db.models.CharField` in your settings. + + ```python + EAV2_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 `EAV2_PRIMARY_KEY_FIELD` into the desired value (`django.db.models.BigAutoField` or `django.db.models.UUIDField`) in your settings. + + ```python + EAV2_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. diff --git a/eav/fields.py b/eav/fields.py index bd72874..c9f2910 100644 --- a/eav/fields.py +++ b/eav/fields.py @@ -21,6 +21,10 @@ class EavDatatypeField(models.CharField): 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/managers.py b/eav/logic/managers.py new file mode 100644 index 0000000..f80a26a --- /dev/null +++ b/eav/logic/managers.py @@ -0,0 +1,97 @@ +from django.db import models + + +class EnumValueManager(models.Manager): + """ + Custom manager for `EnumValue` model. + + This manager adds utility methods specific to the `EnumValue` model. + """ + + def get_by_natural_key(self, value): + """ + Retrieves an EnumValue instance using its `value` as a natural key. + + Args: + value (str): The value of the EnumValue instance. + + Returns: + EnumValue: The instance matching the provided value. + """ + return self.get(value=value) + + +class EnumGroupManager(models.Manager): + """ + Custom manager for `EnumGroup` model. + + This manager adds utility methods specific to the `EnumGroup` model. + """ + + def get_by_natural_key(self, name): + """ + Retrieves an EnumGroup instance using its `name` as a natural key. + + Args: + name (str): The name of the EnumGroup instance. + + Returns: + EnumGroup: The instance matching the provided name. + """ + return self.get(name=name) + + +class AttributeManager(models.Manager): + """ + Custom manager for `Attribute` model. + + This manager adds utility methods specific to the `Attribute` model. + """ + + def get_by_natural_key(self, name, slug): + """ + Retrieves an Attribute instance using its `name` and `slug` as natural keys. + + Args: + name (str): The name of the Attribute instance. + slug (str): The slug of the Attribute instance. + + Returns: + Attribute: The instance matching the provided name and slug. + """ + return self.get(name=name, slug=slug) + + +class ValueManager(models.Manager): + """ + Custom manager for `Value` model. + + This manager adds utility methods specific to the `Value` model. + """ + + def get_by_natural_key(self, attribute, entity_id, entity_uuid): + """ + Retrieve a Value instance using multiple natural keys. + + This method utilizes a combination of an `attribute` (defined by its + name and slug), `entity_id`, and `entity_uuid` to retrieve a unique + Value instance. + + Args: + attribute (tuple): A tuple containing the name and slug of the + Attribute instance. + entity_id (int): The ID of the associated entity. + entity_uuid (str): The UUID of the associated entity. + + Returns: + Value: The instance matching the provided keys. + """ + from eav.models import Attribute + + attribute = Attribute.objects.get(name=attribute[0], slug=attribute[1]) + + return self.get( + attribute=attribute, + entity_id=entity_id, + entity_uuid=entity_uuid, + ) diff --git a/eav/logic/object_pk.py b/eav/logic/object_pk.py new file mode 100644 index 0000000..3deee32 --- /dev/null +++ b/eav/logic/object_pk.py @@ -0,0 +1,45 @@ +import uuid +from functools import partial +from typing import Type + +from django.conf import settings +from django.db import models + +#: Constants +_DEFAULT_CHARFIELD_LEN: int = 40 + +_FIELD_MAPPING = { + "django.db.models.UUIDField": partial( + models.UUIDField, + primary_key=True, + editable=False, + default=uuid.uuid4, + ), + "django.db.models.CharField": partial( + models.CharField, + primary_key=True, + editable=False, + max_length=_DEFAULT_CHARFIELD_LEN, + ), +} + + +def get_pk_format() -> Type[models.Field]: + """ + Get the primary key field format based on the Django settings. + + This function returns a field factory function that corresponds to the + primary key format specified in Django settings. If the primary key + format is not recognized, it defaults to using BigAutoField. + + Returns: + Type[models.Field]: A field factory function that can be used to + create the primary key field instance. + """ + field_factory = _FIELD_MAPPING.get( + settings.EAV2_PRIMARY_KEY_FIELD, + partial(models.BigAutoField, primary_key=True, editable=False), + ) + + # Create and return the field instance + return field_factory() diff --git a/eav/migrations/0010_dynamic_pk_type_for_models.py b/eav/migrations/0010_dynamic_pk_type_for_models.py new file mode 100644 index 0000000..28028ac --- /dev/null +++ b/eav/migrations/0010_dynamic_pk_type_for_models.py @@ -0,0 +1,48 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + """Migration to use BigAutoField as default for all models.""" + + dependencies = [ + ('eav', '0009_enchance_naming'), + ] + + 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/eav/models.py b/eav/models.py index 41c6b54..865fc5b 100644 --- a/eav/models.py +++ b/eav/models.py @@ -10,6 +10,7 @@ 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 @@ -24,6 +25,13 @@ 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.validators import ( validate_bool, @@ -73,10 +81,14 @@ class EnumValue(models.Model): the same *Yes* and *No* *EnumValues* for both *EnumGroups*. """ + objects = EnumValueManager() + class Meta: verbose_name = _('EnumValue') verbose_name_plural = _('EnumValues') + id = get_pk_format() + value = models.CharField( _('Value'), db_index=True, @@ -84,9 +96,23 @@ class EnumValue(models.Model): 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) + return str( + self.value, + ) def __repr__(self): """String representation of `EnumValue` object.""" @@ -102,10 +128,14 @@ class EnumGroup(models.Model): See :class:`EnumValue` for an example. """ + objects = EnumGroupManager() + class Meta: verbose_name = _('EnumGroup') verbose_name_plural = _('EnumGroups') + id = get_pk_format() + name = models.CharField( unique=True, max_length=CHARFIELD_LENGTH, @@ -116,6 +146,18 @@ class EnumGroup(models.Model): 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) @@ -177,6 +219,8 @@ class Attribute(models.Model): change it's datatype. """ + objects = AttributeManager() + class Meta: ordering = ['name'] verbose_name = _('Attribute') @@ -205,6 +249,7 @@ class Attribute(models.Model): ) # Core attributes + id = get_pk_format() datatype = EavDatatypeField( choices=DATATYPE_CHOICES, @@ -288,6 +333,21 @@ class Attribute(models.Model): 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 @@ -435,10 +495,14 @@ class Value(models.Model): # noqa: WPS110 # = """ + objects = ValueManager() + class Meta: verbose_name = _('Value') verbose_name_plural = _('Values') + id = get_pk_format() + # Direct foreign keys attribute = models.ForeignKey( Attribute, @@ -560,11 +624,23 @@ class Value(models.Model): # noqa: WPS110 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_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, @@ -573,13 +649,11 @@ class Value(models.Model): # noqa: WPS110 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): diff --git a/poetry.lock b/poetry.lock index 1a2bd1c..4b506f7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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] diff --git a/test_project/settings.py b/test_project/settings.py index 5b66c07..07f7f72 100644 --- a/test_project/settings.py +++ b/test_project/settings.py @@ -32,6 +32,7 @@ INSTALLED_APPS = [ 'eav', ] + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -69,7 +70,9 @@ DATABASES = { }, } + DEFAULT_AUTO_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 e6b8e74..1347eb2 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -1,10 +1,12 @@ +import uuid import string from django.core.exceptions import ValidationError from django.test import TestCase from hypothesis import given, settings -from hypothesis import strategies as st from hypothesis.extra import django +from django.conf import settings as django_settings +from hypothesis import strategies as st from hypothesis.strategies import just import eav @@ -14,6 +16,14 @@ from eav.registry import EavConfig from test_project.models import Doctor, Encounter, Patient, RegisterTestModel +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.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) + + class Attributes(TestCase): def setUp(self): class EncounterEavConfig(EavConfig): @@ -123,6 +133,7 @@ class TestAttributeModel(django.TestCase): @given( django.from_model( Attribute, + id=auto_field_strategy, datatype=just(Attribute.TYPE_TEXT), enum_group=just(None), ), diff --git a/tests/test_natural_keys.py b/tests/test_natural_keys.py new file mode 100644 index 0000000..5e3b97a --- /dev/null +++ b/tests/test_natural_keys.py @@ -0,0 +1,51 @@ +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) diff --git a/tests/test_primary_key_format.py b/tests/test_primary_key_format.py new file mode 100644 index 0000000..4b9410c --- /dev/null +++ b/tests/test_primary_key_format.py @@ -0,0 +1,33 @@ +import uuid + +import pytest +from django.db import models + +from eav.logic.object_pk import get_pk_format + + +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