From 2482148466786965a596ce03b6f6e1b8bc751f9d Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 10 Jul 2025 19:25:42 +0930 Subject: [PATCH 01/23] refactor(accounting): Switch to inherit from Centurion model for model AssetBase ref: #862 #857 --- ...id_alter_assetbase_model_notes_and_more.py | 118 ++++++++++++++++ app/accounting/models/asset_base.py | 126 ++++-------------- 2 files changed, 147 insertions(+), 97 deletions(-) create mode 100644 app/accounting/migrations/0002_alter_assetbase_id_alter_assetbase_model_notes_and_more.py diff --git a/app/accounting/migrations/0002_alter_assetbase_id_alter_assetbase_model_notes_and_more.py b/app/accounting/migrations/0002_alter_assetbase_id_alter_assetbase_model_notes_and_more.py new file mode 100644 index 00000000..277a19cb --- /dev/null +++ b/app/accounting/migrations/0002_alter_assetbase_id_alter_assetbase_model_notes_and_more.py @@ -0,0 +1,118 @@ +# Generated by Django 5.1.10 on 2025-07-10 09:10 + +import access.models.tenancy_abstract +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("access", "0019_companyaudithistory_companycenturionmodelnote"), + ("accounting", "0001_initial"), + ("core", "0033_alter_ticketcommentcategory_parent_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="assetbase", + name="id", + field=models.AutoField( + help_text="ID of the item", + primary_key=True, + serialize=False, + unique=True, + verbose_name="ID", + ), + ), + migrations.AlterField( + model_name="assetbase", + name="model_notes", + field=models.TextField( + blank=True, + help_text="Tid bits of information", + null=True, + verbose_name="Notes", + ), + ), + migrations.AlterField( + model_name="assetbase", + name="organization", + field=models.ForeignKey( + help_text="Tenant this belongs to", + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="access.tenant", + validators=[ + access.models.tenancy_abstract.TenancyAbstractModel.validatate_organization_exists + ], + verbose_name="Tenant", + ), + ), + migrations.CreateModel( + name="AssetBaseAuditHistory", + fields=[ + ( + "centurionaudit_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="core.centurionaudit", + ), + ), + ( + "model", + models.ForeignKey( + help_text="Model this history belongs to", + on_delete=django.db.models.deletion.CASCADE, + related_name="audit_history", + to="accounting.assetbase", + verbose_name="Model", + ), + ), + ], + options={ + "verbose_name": "Asset History", + "verbose_name_plural": "Asset Histories", + "db_table": "accounting_assetbase_audithistory", + "managed": True, + }, + bases=("core.centurionaudit",), + ), + migrations.CreateModel( + name="AssetBaseCenturionModelNote", + fields=[ + ( + "centurionmodelnote_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="core.centurionmodelnote", + ), + ), + ( + "model", + models.ForeignKey( + help_text="Model this note belongs to", + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="accounting.assetbase", + verbose_name="Model", + ), + ), + ], + options={ + "verbose_name": "Asset Note", + "verbose_name_plural": "Asset Notes", + "db_table": "accounting_assetbase_centurionmodelnote", + "managed": True, + }, + bases=("core.centurionmodelnote",), + ), + ] diff --git a/app/accounting/models/asset_base.py b/app/accounting/models/asset_base.py index 82d7956e..9be06639 100644 --- a/app/accounting/models/asset_base.py +++ b/app/accounting/models/asset_base.py @@ -1,15 +1,14 @@ from django.apps import apps from django.db import models -from rest_framework.reverse import reverse +from access.fields import AutoLastModifiedField -from access.fields import AutoCreatedField, AutoLastModifiedField -from access.models.tenancy import TenancyObject +from core.models.centurion import CenturionModel class AssetBase( - TenancyObject, + CenturionModel, ): """Asset Base Model @@ -21,6 +20,10 @@ class AssetBase( app_namespace = 'accounting' + model_tag = 'asset' + + url_model_name = 'asset' + @property def _base_model(self): @@ -50,18 +53,6 @@ class AssetBase( return True - - is_global = None - - - id = models.AutoField( - blank = False, - help_text = 'Ticket ID Number', - primary_key = True, - unique = True, - verbose_name = 'Number', - ) - asset_number = models.CharField( blank = True, help_text = 'Number or tag to use to track this asset', @@ -96,14 +87,11 @@ class AssetBase( """ - # Status # model (manufacturer / model) - - @property def get_model_type(self): """Fetch the Ticket Type @@ -137,7 +125,7 @@ class AssetBase( if( ( isinstance(model, AssetBase) or issubclass(model, AssetBase) ) - and AssetBase._meta.sub_model_type != 'asset' + # and AssetBase._meta.sub_model_type != 'asset' ): @@ -159,12 +147,6 @@ class AssetBase( verbose_name = 'Asset Type', ) - - - created = AutoCreatedField( - editable = True, - ) - modified = AutoLastModifiedField() @@ -237,6 +219,26 @@ class AssetBase( + def clean_fields(self, exclude = None): + + related_model = self.get_related_model() + + if related_model is None: + + related_model = self + + if ( + self.asset_type != str(related_model._meta.sub_model_type).lower().replace(' ', '_') + and str(related_model._meta.sub_model_type).lower().replace(' ', '_') != 'asset' + ): + + self.asset_type = str(related_model._meta.sub_model_type).lower().replace(' ', '_') + + + super().clean_fields(exclude = exclude) + + + def get_related_field_name(self) -> str: meta = getattr(self, '_meta') @@ -260,6 +262,7 @@ class AssetBase( return '' + def get_related_model(self): """Recursive model Fetch @@ -292,74 +295,3 @@ class AssetBase( return related_model - - - - - def get_url( self, request = None ) -> str: - - kwargs = self.get_url_kwargs() - - url_path_name = '_api_v2_asset_sub' - - if self._meta.sub_model_type == 'asset': - - url_path_name = '_api_v2_asset' - - if request: - - return reverse(f"v2:accounting:{url_path_name}-detail", request=request, kwargs = kwargs ) - - return reverse(f"v2:accounting:{url_path_name}-detail", kwargs = kwargs ) - - - - def get_url_kwargs(self) -> dict: - - kwargs = { - 'asset_model': self.asset_type, - } - - if self._meta.sub_model_type == 'asset': - - kwargs = {} - - - if self.id: - - kwargs.update({ - 'pk': self.id - }) - - return kwargs - - - - def save(self, force_insert=False, force_update=False, using=None, update_fields=None): - - related_model = self.get_related_model() - - if related_model is None: - - related_model = self - - if self.asset_type != str(related_model._meta.sub_model_type).lower().replace(' ', '_'): - - self.asset_type = str(related_model._meta.sub_model_type).lower().replace(' ', '_') - - super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields) - - - - def save_history(self, before: dict, after: dict) -> bool: - - from accounting.models.asset_base_history import AssetBaseHistory - - history = super().save_history( - before = before, - after = after, - history_model = AssetBaseHistory - ) - - - return history From 46b4df5dc116a598f23549aa1a06d03655fe21a6 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 10 Jul 2025 19:32:57 +0930 Subject: [PATCH 02/23] feat(accounting): Add Notes Serializer for AssetBase model ref: #862 #857 --- ...03_remove_assetbasenotes_model_and_more.py | 19 ++++ app/accounting/models/asset_base_notes.py | 47 ---------- .../centurionmodelnote_assetbase.py | 87 +++++++++++++++++++ 3 files changed, 106 insertions(+), 47 deletions(-) create mode 100644 app/accounting/migrations/0003_remove_assetbasenotes_model_and_more.py delete mode 100644 app/accounting/models/asset_base_notes.py create mode 100644 app/accounting/serializers/centurionmodelnote_assetbase.py diff --git a/app/accounting/migrations/0003_remove_assetbasenotes_model_and_more.py b/app/accounting/migrations/0003_remove_assetbasenotes_model_and_more.py new file mode 100644 index 00000000..0a5598d9 --- /dev/null +++ b/app/accounting/migrations/0003_remove_assetbasenotes_model_and_more.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.10 on 2025-07-10 09:31 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounting", "0002_alter_assetbase_id_alter_assetbase_model_notes_and_more"), + ] + + operations = [ + migrations.DeleteModel( + name="AssetBaseHistory", + ), + migrations.DeleteModel( + name="AssetBaseNotes", + ), + ] diff --git a/app/accounting/models/asset_base_notes.py b/app/accounting/models/asset_base_notes.py deleted file mode 100644 index 687a6d78..00000000 --- a/app/accounting/models/asset_base_notes.py +++ /dev/null @@ -1,47 +0,0 @@ -from django.db import models - -from accounting.models.asset_base import AssetBase - -from core.models.model_notes import ModelNotes - - - -class AssetBaseNotes( - ModelNotes -): - - - class Meta: - - db_table = 'accounting_assetbase_notes' - - ordering = ModelNotes._meta.ordering - - verbose_name = 'Asset Note' - - verbose_name_plural = 'Asset Notes' - - - model = models.ForeignKey( - AssetBase, - blank = False, - help_text = 'Model this note belongs to', - null = False, - on_delete = models.CASCADE, - related_name = 'notes', - verbose_name = 'Model', - ) - - app_namespace = 'accounting' - - table_fields: list = [] - - page_layout: dict = [] - - - def get_url_kwargs(self) -> dict: - - return { - 'model_id': self.model.pk, - 'pk': self.pk - } diff --git a/app/accounting/serializers/centurionmodelnote_assetbase.py b/app/accounting/serializers/centurionmodelnote_assetbase.py new file mode 100644 index 00000000..9d3e6da3 --- /dev/null +++ b/app/accounting/serializers/centurionmodelnote_assetbase.py @@ -0,0 +1,87 @@ +from rest_framework import serializers + +from drf_spectacular.utils import extend_schema_serializer + +from access.serializers.organization import (TenantBaseSerializer) + +from centurion.models.meta import AssetBaseCenturionModelNote # pylint: disable=E0401:import-error disable=E0611:no-name-in-module + +from core.serializers.centurionmodelnote import ( # pylint: disable=W0611:unused-import + BaseSerializer, + ModelSerializer as BaseModelModelSerializer, + ViewSerializer as BaseModelViewSerializer +) + + + +@extend_schema_serializer(component_name = 'AssetBaseModelNoteModelSerializer') +class ModelSerializer( + BaseModelModelSerializer, +): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item) -> dict: + + return { + '_self': item.get_url( request = self._context['view'].request ), + } + + + class Meta: + + model = AssetBaseCenturionModelNote + + fields = [ + 'id', + 'organization', + 'display_name', + 'body', + 'created_by', + 'modified_by', + 'content_type', + 'model', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'organization', + 'created_by', + 'modified_by', + 'content_type', + 'model', + 'created', + 'modified', + '_urls', + ] + + + + def validate(self, attrs): + + is_valid = False + + note_model = self.Meta.model.model.field.related_model + + attrs['model'] = note_model.objects.get( + id = int( self.context['view'].kwargs['model_id'] ) + ) + + + is_valid = super().validate(attrs) + + return is_valid + + +@extend_schema_serializer(component_name = 'AssetBaseModelNoteViewSerializer') +class ViewSerializer( + ModelSerializer, + BaseModelViewSerializer, +): + + organization = TenantBaseSerializer( many = False, read_only = True ) From 4ff457a58c982b6af83ac13649c40ee85862b0f2 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 10 Jul 2025 19:33:36 +0930 Subject: [PATCH 03/23] feat(accounting): Add AuditHistory Serializer for AssetBase model ref: #862 #857 --- app/accounting/models/asset_base_history.py | 53 ------------------ .../serializers/centurionaudit_assetbase.py | 56 +++++++++++++++++++ 2 files changed, 56 insertions(+), 53 deletions(-) delete mode 100644 app/accounting/models/asset_base_history.py create mode 100644 app/accounting/serializers/centurionaudit_assetbase.py diff --git a/app/accounting/models/asset_base_history.py b/app/accounting/models/asset_base_history.py deleted file mode 100644 index b7c1b06f..00000000 --- a/app/accounting/models/asset_base_history.py +++ /dev/null @@ -1,53 +0,0 @@ -from django.db import models - -from accounting.models.asset_base import AssetBase - -from core.models.model_history import ModelHistory - - - -class AssetBaseHistory( - ModelHistory -): - - - class Meta: - - db_table = 'accounting_assetbase_history' - - ordering = ModelHistory._meta.ordering - - verbose_name = 'Asset History' - - verbose_name_plural = 'Asset History' - - - model = models.ForeignKey( - AssetBase, - blank = False, - help_text = 'Model this note belongs to', - null = False, - on_delete = models.CASCADE, - related_name = 'history', - verbose_name = 'Model', - ) - - table_fields: list = [] - - page_layout: dict = [] - - - def get_object(self): - - return self - - - def get_serialized_model(self, serializer_context): - - model = None - - from accounting.serializers.asset import BaseSerializer - - model = BaseSerializer(self.model, context = serializer_context) - - return model diff --git a/app/accounting/serializers/centurionaudit_assetbase.py b/app/accounting/serializers/centurionaudit_assetbase.py new file mode 100644 index 00000000..5fcec16a --- /dev/null +++ b/app/accounting/serializers/centurionaudit_assetbase.py @@ -0,0 +1,56 @@ +from rest_framework import serializers + +from drf_spectacular.utils import extend_schema_serializer + +from api.serializers import common + +from centurion.models.meta import AssetBaseAuditHistory # pylint: disable=E0401:import-error disable=E0611:no-name-in-module + +from core.serializers.centurionaudit import ( + BaseSerializer, + ViewSerializer as AuditHistoryViewSerializer +) + + + + +@extend_schema_serializer(component_name = 'AssetBaseAuditHistoryModelSerializer') +class ModelSerializer( + common.CommonModelSerializer, + BaseSerializer +): + """Git Group Audit History Base Model""" + + + _urls = serializers.SerializerMethodField('get_url') + + + class Meta: + + model = AssetBaseAuditHistory + + fields = [ + 'id', + 'organization', + 'display_name', + 'content_type', + 'model', + 'before', + 'after', + 'action', + 'user', + 'created', + '_urls', + ] + + read_only_fields = fields + + + +@extend_schema_serializer(component_name = 'AssetBaseAuditHistoryViewSerializer') +class ViewSerializer( + ModelSerializer, + AuditHistoryViewSerializer, +): + """Git Group Audit History Base View Model""" + pass From 9b3c8d2225da3a332499428ed079e77be9511f6b Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 10 Jul 2025 19:43:54 +0930 Subject: [PATCH 04/23] refactor(accounting): Update Test Suite for AssetBase model ref: #862 #857 --- .../test_functional_asset_base_metadata.py | 4 + .../test_functional_asset_base_permission.py | 3 + .../test_functional_asset_base_serializer.py | 2 + .../test_functional_asset_base_viewset.py | 4 + .../test_unit_asset_base_api_fields.py | 4 +- .../asset_base/test_unit_asset_base_model.py | 226 +++++------------- .../test_unit_asset_base_viewset.py | 6 +- app/fixtures/fresh_db.sql | 17 +- pyproject.toml | 1 + 9 files changed, 91 insertions(+), 176 deletions(-) diff --git a/app/accounting/tests/functional/asset_base/test_functional_asset_base_metadata.py b/app/accounting/tests/functional/asset_base/test_functional_asset_base_metadata.py index d6410fae..4e0db0ba 100644 --- a/app/accounting/tests/functional/asset_base/test_functional_asset_base_metadata.py +++ b/app/accounting/tests/functional/asset_base/test_functional_asset_base_metadata.py @@ -1,4 +1,6 @@ import django +import pytest + from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.test import TestCase @@ -15,6 +17,7 @@ User = django.contrib.auth.get_user_model() +@pytest.mark.model_assetbase class MetadataTestCases( MetadataAttributesFunctional, ): @@ -262,6 +265,7 @@ class AssetBaseMetadataInheritedCases( +@pytest.mark.module_accounting class AssetBaseMetadataTest( MetadataTestCases, TestCase, diff --git a/app/accounting/tests/functional/asset_base/test_functional_asset_base_permission.py b/app/accounting/tests/functional/asset_base/test_functional_asset_base_permission.py index ea5331e1..dfe4761c 100644 --- a/app/accounting/tests/functional/asset_base/test_functional_asset_base_permission.py +++ b/app/accounting/tests/functional/asset_base/test_functional_asset_base_permission.py @@ -6,6 +6,7 @@ from api.tests.functional.test_functional_api_permissions import ( +@pytest.mark.model_assetbase class PermissionsAPITestCases( APIPermissionsInheritedCases, ): @@ -94,6 +95,8 @@ class AssetBasePermissionsAPIInheritedCases( pass + +@pytest.mark.module_accounting class AssetBasePermissionsAPIPyTest( PermissionsAPITestCases, ): diff --git a/app/accounting/tests/functional/asset_base/test_functional_asset_base_serializer.py b/app/accounting/tests/functional/asset_base/test_functional_asset_base_serializer.py index 7fad24e9..4b5437bf 100644 --- a/app/accounting/tests/functional/asset_base/test_functional_asset_base_serializer.py +++ b/app/accounting/tests/functional/asset_base/test_functional_asset_base_serializer.py @@ -27,6 +27,7 @@ class MockView: +@pytest.mark.model_assetbase class AssetBaseSerializerTestCases: @@ -188,6 +189,7 @@ class AssetBaseSerializerInheritedCases( +@pytest.mark.module_accounting class AssetBaseSerializerPyTest( AssetBaseSerializerTestCases, ): diff --git a/app/accounting/tests/functional/asset_base/test_functional_asset_base_viewset.py b/app/accounting/tests/functional/asset_base/test_functional_asset_base_viewset.py index bc4697c3..0560a88c 100644 --- a/app/accounting/tests/functional/asset_base/test_functional_asset_base_viewset.py +++ b/app/accounting/tests/functional/asset_base/test_functional_asset_base_viewset.py @@ -1,4 +1,6 @@ import django +import pytest + from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.test import TestCase @@ -15,6 +17,7 @@ User = django.contrib.auth.get_user_model() +@pytest.mark.model_assetbase class ViewSetBase: add_data: dict = { @@ -265,6 +268,7 @@ class AssetBaseViewSetInheritedCases( +@pytest.mark.module_accounting class AssetBaseViewSetTest( ViewSetTestCases, TestCase, diff --git a/app/accounting/tests/unit/asset_base/test_unit_asset_base_api_fields.py b/app/accounting/tests/unit/asset_base/test_unit_asset_base_api_fields.py index 95beaec3..3f329959 100644 --- a/app/accounting/tests/unit/asset_base/test_unit_asset_base_api_fields.py +++ b/app/accounting/tests/unit/asset_base/test_unit_asset_base_api_fields.py @@ -8,6 +8,7 @@ from api.tests.functional.test_functional_api_fields import ( +@pytest.mark.model_assetbase class AssetBaseAPITestCases( APIFieldsInheritedCases, ): @@ -21,7 +22,7 @@ class AssetBaseAPITestCases( ): if model != self.base_model: - + request.cls.url_view_kwargs.update({ 'asset_model': model._meta.sub_model_type, }) @@ -72,6 +73,7 @@ class AssetBaseAPIInheritedCases( +@pytest.mark.module_accounting class AssetBaseAPIPyTest( AssetBaseAPITestCases, ): diff --git a/app/accounting/tests/unit/asset_base/test_unit_asset_base_model.py b/app/accounting/tests/unit/asset_base/test_unit_asset_base_model.py index 0be0f439..7dbe9c51 100644 --- a/app/accounting/tests/unit/asset_base/test_unit_asset_base_model.py +++ b/app/accounting/tests/unit/asset_base/test_unit_asset_base_model.py @@ -4,204 +4,96 @@ from django.db import models from accounting.models.asset_base import AssetBase -from centurion.tests.unit.test_unit_models import ( - PyTestTenancyObjectInheritedCases, +from core.tests.unit.centurion_abstract.test_unit_centurion_abstract_model import ( + CenturionAbstractModelInheritedCases ) +@pytest.mark.model_assetbase class AssetBaseModelTestCases( - PyTestTenancyObjectInheritedCases, + CenturionAbstractModelInheritedCases ): - base_model = AssetBase - kwargs_create_item: dict = { - 'asset_number': 'a12s432', - 'serial_number': 'abbcccdddd', - } + @property + def parameterized_class_attributes(self): - sub_model_type = 'asset' - """Sub Model Type - - sub-models must have this attribute defined in `ModelName.Meta.sub_model_type` - """ - - - parameterized_fields: dict = { - "asset_number": { - 'field_type': models.fields.CharField, - 'field_parameter_default_exists': False, - 'field_parameter_verbose_name_type': str, - }, - "serial_number": { - 'field_type': models.fields.CharField, - 'field_parameter_default_exists': False, - 'field_parameter_verbose_name_type': str, + return { + 'app_namespace': { + 'type': str, + 'value': 'accounting' + }, + 'model_tag': { + 'type': str, + 'value': 'asset' + }, + 'url_model_name': { + 'type': str, + 'value': 'asset' + }, + } + + + @property + def parameterized_model_fields(self): + + return { + 'asset_number': { + 'blank': True, + 'default': models.fields.NOT_PROVIDED, + 'field_type': models.CharField, + 'max_length': 30, + 'null': True, + 'unique': True, + }, + 'serial_number': { + 'blank': True, + 'default': models.fields.NOT_PROVIDED, + 'field_type': models.CharField, + 'max_length': 30, + 'null': True, + 'unique': True, + }, + 'asset_type': { + 'blank': True, + 'default': 'asset', + 'field_type': models.CharField, + 'max_length': 30, + 'null': False, + 'unique': False, } - } - - @pytest.fixture( scope = 'class') - def setup_model(self, - request, - model, - django_db_blocker, - organization_one, - organization_two - ): - - with django_db_blocker.unblock(): - - request.cls.organization = organization_one - - request.cls.different_organization = organization_two - - kwargs_create_item = {} - - for base in reversed(request.cls.__mro__): - - if hasattr(base, 'kwargs_create_item'): - - if base.kwargs_create_item is None: - - continue - - kwargs_create_item.update(**base.kwargs_create_item) - - - if len(kwargs_create_item) > 0: - - request.cls.kwargs_create_item = kwargs_create_item - - - if 'organization' not in request.cls.kwargs_create_item: - - request.cls.kwargs_create_item.update({ - 'organization': request.cls.organization - }) - - yield - - with django_db_blocker.unblock(): - - del request.cls.kwargs_create_item - - - @pytest.fixture( scope = 'class', autouse = True) - def class_setup(self, - setup_model, - create_model, - ): - - pass - - - - def test_class_inherits_assetbase(self): + def test_class_inherits_assetbase(self, model): """ Class inheritence TenancyObject must inherit SaveHistory """ - assert issubclass(self.model, AssetBase) - - - - def test_attribute_type_app_namespace(self): - """Attribute Type - - app_namespace is of type str - """ - - assert type(self.model.app_namespace) is str - - - def test_attribute_value_app_namespace(self): - """Attribute Type - - app_namespace has been set, override this test case with the value - of attribute `app_namespace` - """ - - assert self.model.app_namespace == 'accounting' - - - def test_function_is_property_get_model_type(self): - """Function test - - Confirm function `get_model_type` is a property - """ - - assert type(self.model.get_model_type) is property - - - def test_function_value_get_model_type(self): - """Function test - - Confirm function `get_model_type` returns None for base model - """ - - assert self.item.get_model_type is None - - - def test_function_value_get_related_model(self): - """Function test - - Confirm function `get_related_model` is of the sub-model type - """ - - assert type(self.item.get_related_model()) == self.model - - - def test_function_value_get_url(self): - - assert self.item.get_url() == '/api/v2/accounting/asset/' + str(self.item.id) + assert issubclass(model, AssetBase) class AssetBaseModelInheritedCases( AssetBaseModelTestCases, ): - """Sub-Ticket Test Cases - - Test Cases for Ticket models that inherit from model AssetBase - """ - - kwargs_create_item: dict = {} - - model = None - sub_model_type = None - """Ticket Sub Model Type - - Ticket sub-models must have this attribute defined in `ModelNam.Meta.sub_model_type` - """ + def test_method_get_url_kwargs(self, mocker, model_instance, settings): + url = model_instance.get_url_kwargs() - def test_function_value_get_model_type(self): - """Function test - - Confirm function `get_model_type` does not have a value of None - value should be equaul to Meta.sub_model_type - """ - - assert self.item.get_model_type == self.item._meta.sub_model_type + assert model_instance.get_url_kwargs() == { + 'model_name': model_instance._meta.model_name, + 'pk': model_instance.id + } +@pytest.mark.module_accounting class AssetBaseModelPyTest( AssetBaseModelTestCases, ): - - - def test_function_value_get_related_model(self): - """Function test - - Confirm function `get_related_model` is None for base model - """ - - assert self.item.get_related_model() is None + pass diff --git a/app/accounting/tests/unit/asset_base/test_unit_asset_base_viewset.py b/app/accounting/tests/unit/asset_base/test_unit_asset_base_viewset.py index bb0f07e8..be249034 100644 --- a/app/accounting/tests/unit/asset_base/test_unit_asset_base_viewset.py +++ b/app/accounting/tests/unit/asset_base/test_unit_asset_base_viewset.py @@ -1,3 +1,5 @@ +import pytest + from django.test import Client, TestCase from rest_framework.reverse import reverse @@ -13,6 +15,7 @@ from api.tests.unit.test_unit_common_viewset import SubModelViewSetInheritedCase +@pytest.mark.model_assetbase class AssetBaseViewsetTestCases( SubModelViewSetInheritedCases, ): @@ -50,7 +53,7 @@ class AssetBaseViewsetTestCases( client = Client() - + url = reverse( self.route_name + '-list', kwargs = self.kwargs @@ -89,6 +92,7 @@ class AssetBaseViewsetInheritedCases( +@pytest.mark.module_accounting class AssetBaseViewsetTest( AssetBaseViewsetTestCases, TestCase, diff --git a/app/fixtures/fresh_db.sql b/app/fixtures/fresh_db.sql index 488a9a6c..2fa1fadb 100644 --- a/app/fixtures/fresh_db.sql +++ b/app/fixtures/fresh_db.sql @@ -98,8 +98,8 @@ CREATE TABLE IF NOT EXISTS "devops_git_group_history" ("modelhistory_ptr_id" int CREATE TABLE IF NOT EXISTS "devops_github_repository_notes" ("modelnotes_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_notes" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "devops_githubrepository" ("gitrepository_ptr_id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "devops_gitlab_repository_notes" ("modelnotes_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_notes" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "devops_gitlabrepository" ("gitrepository_ptr_id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "devops_git_group_notes" ("modelnotes_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_notes" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "devops_gitgroup" ("id") DEFERRABLE INITIALLY DEFERRED); -CREATE TABLE IF NOT EXISTS "access_organization_notes" ("modelnotes_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_notes" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_tenant" ("id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "access_organization_history" ("modelhistory_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_history" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_tenant" ("id") DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE IF NOT EXISTS "access_organization_notes" ("modelnotes_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_notes" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_tenant" ("id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "access_company" ("entity_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "access_entity" ("id") DEFERRABLE INITIALLY DEFERRED, "name" varchar(80) NOT NULL); CREATE TABLE IF NOT EXISTS "access_person" ("entity_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "access_entity" ("id") DEFERRABLE INITIALLY DEFERRED, "f_name" varchar(64) NOT NULL, "l_name" varchar(64) NOT NULL, "dob" date NULL, "m_name" varchar(100) NULL); CREATE TABLE IF NOT EXISTS "core_ticketcommentaction" ("ticketcommentbase_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_ticketcommentbase" ("id") DEFERRABLE INITIALLY DEFERRED); @@ -139,9 +139,9 @@ CREATE TABLE IF NOT EXISTS "access_person_audithistory" ("centurionaudit_ptr_id" CREATE TABLE IF NOT EXISTS "access_person_centurionmodelnote" ("centurionmodelnote_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_centurionmodelnote" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_person" ("entity_ptr_id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "access_company_audithistory" ("centurionaudit_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_audithistory" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_company" ("entity_ptr_id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "access_company_centurionmodelnote" ("centurionmodelnote_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_centurionmodelnote" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_company" ("entity_ptr_id") DEFERRABLE INITIALLY DEFERRED); -CREATE TABLE IF NOT EXISTS "accounting_assetbase" ("model_notes" text NULL, "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "asset_number" varchar(30) NULL UNIQUE, "serial_number" varchar(30) NULL UNIQUE, "asset_type" varchar(30) NOT NULL, "created" datetime NOT NULL, "modified" datetime NOT NULL, "organization_id" integer NOT NULL REFERENCES "access_tenant" ("id") DEFERRABLE INITIALLY DEFERRED); -CREATE TABLE IF NOT EXISTS "accounting_assetbase_history" ("modelhistory_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_history" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "accounting_assetbase" ("id") DEFERRABLE INITIALLY DEFERRED); -CREATE TABLE IF NOT EXISTS "accounting_assetbase_notes" ("modelnotes_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_notes" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "accounting_assetbase" ("id") DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE IF NOT EXISTS "accounting_assetbase" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "asset_number" varchar(30) NULL UNIQUE, "serial_number" varchar(30) NULL UNIQUE, "asset_type" varchar(30) NOT NULL, "created" datetime NOT NULL, "modified" datetime NOT NULL, "organization_id" integer NOT NULL REFERENCES "access_tenant" ("id") DEFERRABLE INITIALLY DEFERRED, "model_notes" text NULL); +CREATE TABLE IF NOT EXISTS "accounting_assetbase_audithistory" ("centurionaudit_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_audithistory" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "accounting_assetbase" ("id") DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE IF NOT EXISTS "accounting_assetbase_centurionmodelnote" ("centurionmodelnote_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_centurionmodelnote" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "accounting_assetbase" ("id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "django_admin_log" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "object_id" text NULL, "object_repr" varchar(200) NOT NULL, "action_flag" smallint unsigned NOT NULL CHECK ("action_flag" >= 0), "change_message" text NOT NULL, "content_type_id" integer NULL REFERENCES "django_content_type" ("id") DEFERRABLE INITIALLY DEFERRED, "user_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED, "action_time" datetime NOT NULL); CREATE TABLE IF NOT EXISTS "api_authtoken" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "note" varchar(50) NULL, "token" varchar(64) NOT NULL UNIQUE, "expires" datetime NOT NULL, "created" datetime NOT NULL, "modified" datetime NOT NULL, "user_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "itam_itamassetbase" ("assetbase_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "accounting_assetbase" ("id") DEFERRABLE INITIALLY DEFERRED, "itam_type" varchar(30) NOT NULL); @@ -197,6 +197,8 @@ CREATE TABLE IF NOT EXISTS "itam_softwarecategory_centurionmodelnote" ("centurio CREATE TABLE IF NOT EXISTS "itam_softwareversion" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "created" datetime NOT NULL, "modified" datetime NOT NULL, "name" varchar(50) NOT NULL, "organization_id" integer NOT NULL REFERENCES "access_tenant" ("id") DEFERRABLE INITIALLY DEFERRED, "software_id" integer NOT NULL REFERENCES "itam_software" ("id") DEFERRABLE INITIALLY DEFERRED, "model_notes" text NULL); CREATE TABLE IF NOT EXISTS "itam_softwareversion_audithistory" ("centurionaudit_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_audithistory" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "itam_softwareversion" ("id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "itam_softwareversion_centurionmodelnote" ("centurionmodelnote_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_centurionmodelnote" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "itam_softwareversion" ("id") DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE IF NOT EXISTS "itam_itamassetbase_audithistory" ("centurionaudit_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_audithistory" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "itam_itamassetbase" ("assetbase_ptr_id") DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE IF NOT EXISTS "itam_itamassetbase_centurionmodelnote" ("centurionmodelnote_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_centurionmodelnote" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "itam_itamassetbase" ("assetbase_ptr_id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "itim_cluster_devices" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "cluster_id" integer NOT NULL REFERENCES "itim_cluster" ("id") DEFERRABLE INITIALLY DEFERRED, "device_id" integer NOT NULL REFERENCES "itam_device" ("id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "itim_cluster_nodes" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "cluster_id" integer NOT NULL REFERENCES "itim_cluster" ("id") DEFERRABLE INITIALLY DEFERRED, "device_id" integer NOT NULL REFERENCES "itam_device" ("id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "itim_cluster" ("model_notes" text NULL, "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(50) NOT NULL, "config" text NULL CHECK ((JSON_VALID("config") OR "config" IS NULL)), "created" datetime NOT NULL, "modified" datetime NOT NULL, "organization_id" integer NOT NULL REFERENCES "access_tenant" ("id") DEFERRABLE INITIALLY DEFERRED, "cluster_type_id" integer NULL REFERENCES "itim_clustertype" ("id") DEFERRABLE INITIALLY DEFERRED, "parent_cluster_id" integer NULL REFERENCES "itim_cluster" ("id") DEFERRABLE INITIALLY DEFERRED); @@ -237,9 +239,9 @@ CREATE TABLE IF NOT EXISTS "social_auth_nonce" ("id" integer NOT NULL PRIMARY KE CREATE TABLE IF NOT EXISTS "social_auth_usersocialauth" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "provider" varchar(32) NOT NULL, "uid" varchar(255) NOT NULL, "user_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED, "created" datetime NOT NULL, "modified" datetime NOT NULL, "extra_data" text NOT NULL CHECK ((JSON_VALID("extra_data") OR "extra_data" IS NULL))); CREATE TABLE IF NOT EXISTS "social_auth_partial" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "token" varchar(32) NOT NULL, "next_step" smallint unsigned NOT NULL CHECK ("next_step" >= 0), "backend" varchar(32) NOT NULL, "timestamp" datetime NOT NULL, "data" text NOT NULL CHECK ((JSON_VALID("data") OR "data" IS NULL))); DELETE FROM sqlite_sequence; -INSERT INTO sqlite_sequence VALUES('django_migrations',222); -INSERT INTO sqlite_sequence VALUES('django_content_type',216); -INSERT INTO sqlite_sequence VALUES('auth_permission',909); +INSERT INTO sqlite_sequence VALUES('django_migrations',225); +INSERT INTO sqlite_sequence VALUES('django_content_type',218); +INSERT INTO sqlite_sequence VALUES('auth_permission',917); INSERT INTO sqlite_sequence VALUES('auth_group',0); INSERT INTO sqlite_sequence VALUES('auth_user',0); INSERT INTO sqlite_sequence VALUES('core_notes',0); @@ -261,6 +263,7 @@ INSERT INTO sqlite_sequence VALUES('assistance_knowledgebasecategory',0); INSERT INTO sqlite_sequence VALUES('assistance_modelknowledgebasearticle',0); INSERT INTO sqlite_sequence VALUES('access_tenant',0); INSERT INTO sqlite_sequence VALUES('access_entity',0); +INSERT INTO sqlite_sequence VALUES('accounting_assetbase',0); INSERT INTO sqlite_sequence VALUES('django_admin_log',0); INSERT INTO sqlite_sequence VALUES('itam_devicetype',0); INSERT INTO sqlite_sequence VALUES('config_management_configgrouphosts',0); diff --git a/pyproject.toml b/pyproject.toml index d770a227..b14ce833 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1103,6 +1103,7 @@ markers = [ "meta_models: Selects Meta models", "mixin: Selects all mixin test cases.", "mixin_centurion: Selects all centurion mixin test cases.", + "model_assetbase: Selects tests for model Asset Base.", "model_appsettings: Selects tests for model app settings.", "model_company: Selects test for model Company.", "model_configgroups: Selects Config Group tests.", From d741992b6d1bcf597dc1fc0813b37c07b8b88be5 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 10 Jul 2025 19:46:51 +0930 Subject: [PATCH 05/23] refactor(accounting): Update URL route name for AssetBase model ref: #862 #857 --- app/accounting/models/__init__.py | 5 ---- .../test_functional_asset_base_metadata.py | 4 +-- .../test_functional_asset_base_permission.py | 4 +-- .../test_functional_asset_base_viewset.py | 4 +-- .../tests/unit/asset_base/conftest.py | 17 +++++++----- .../test_unit_asset_base_api_fields.py | 4 +-- .../test_unit_asset_base_viewset.py | 4 +-- app/accounting/urls.py | 4 +-- app/tests/fixtures/model_assetbase.py | 26 +++++++++++++++++++ 9 files changed, 49 insertions(+), 23 deletions(-) create mode 100644 app/tests/fixtures/model_assetbase.py diff --git a/app/accounting/models/__init__.py b/app/accounting/models/__init__.py index 71470686..e69de29b 100644 --- a/app/accounting/models/__init__.py +++ b/app/accounting/models/__init__.py @@ -1,5 +0,0 @@ -from .asset_base_history import AssetBaseHistory # pylint: disable=W0611:unused-import - -from .asset_base_history import AssetBaseHistory # pylint: disable=W0611:unused-import - -from .asset_base_notes import AssetBaseNotes # pylint: disable=W0611:unused-import diff --git a/app/accounting/tests/functional/asset_base/test_functional_asset_base_metadata.py b/app/accounting/tests/functional/asset_base/test_functional_asset_base_metadata.py index 4e0db0ba..ce7ff4bd 100644 --- a/app/accounting/tests/functional/asset_base/test_functional_asset_base_metadata.py +++ b/app/accounting/tests/functional/asset_base/test_functional_asset_base_metadata.py @@ -237,7 +237,7 @@ class AssetBaseMetadataInheritedCases( kwargs_create_item_diff_org: dict = {} - url_name = 'accounting:_api_v2_asset_sub' + url_name = 'accounting:_api_asset_sub' @classmethod @@ -272,4 +272,4 @@ class AssetBaseMetadataTest( ): - url_name = 'accounting:_api_v2_asset' + url_name = 'accounting:_api_asset' diff --git a/app/accounting/tests/functional/asset_base/test_functional_asset_base_permission.py b/app/accounting/tests/functional/asset_base/test_functional_asset_base_permission.py index dfe4761c..95329646 100644 --- a/app/accounting/tests/functional/asset_base/test_functional_asset_base_permission.py +++ b/app/accounting/tests/functional/asset_base/test_functional_asset_base_permission.py @@ -37,7 +37,7 @@ class PermissionsAPITestCases( url_kwargs: dict = {} - url_name = 'accounting:_api_v2_asset' + url_name = 'accounting:_api_asset' url_view_kwargs: dict = {} @@ -66,7 +66,7 @@ class AssetBasePermissionsAPIInheritedCases( kwargs_create_item_diff_org: dict = None - url_name = 'accounting:_api_v2_asset_sub' + url_name = 'accounting:_api_asset_sub' @pytest.fixture(scope='class') diff --git a/app/accounting/tests/functional/asset_base/test_functional_asset_base_viewset.py b/app/accounting/tests/functional/asset_base/test_functional_asset_base_viewset.py index 0560a88c..c427259b 100644 --- a/app/accounting/tests/functional/asset_base/test_functional_asset_base_viewset.py +++ b/app/accounting/tests/functional/asset_base/test_functional_asset_base_viewset.py @@ -240,7 +240,7 @@ class AssetBaseViewSetInheritedCases( model = None - url_name = 'accounting:_api_v2_asset_sub' + url_name = 'accounting:_api_asset_sub' @classmethod @@ -274,4 +274,4 @@ class AssetBaseViewSetTest( TestCase, ): - url_name = 'accounting:_api_v2_asset' + url_name = 'accounting:_api_asset' diff --git a/app/accounting/tests/unit/asset_base/conftest.py b/app/accounting/tests/unit/asset_base/conftest.py index b5335256..a39588d2 100644 --- a/app/accounting/tests/unit/asset_base/conftest.py +++ b/app/accounting/tests/unit/asset_base/conftest.py @@ -1,14 +1,19 @@ import pytest -from accounting.models.asset_base import AssetBase - @pytest.fixture( scope = 'class') -def model(request): +def model(model_assetbase): - request.cls.model = AssetBase + yield model_assetbase - yield request.cls.model - del request.cls.model +@pytest.fixture( scope = 'class', autouse = True) +def model_kwargs(request, kwargs_assetbase): + + request.cls.kwargs_create_item = kwargs_assetbase.copy() + + yield kwargs_assetbase.copy() + + if hasattr(request.cls, 'kwargs_create_item'): + del request.cls.kwargs_create_item diff --git a/app/accounting/tests/unit/asset_base/test_unit_asset_base_api_fields.py b/app/accounting/tests/unit/asset_base/test_unit_asset_base_api_fields.py index 3f329959..29044cda 100644 --- a/app/accounting/tests/unit/asset_base/test_unit_asset_base_api_fields.py +++ b/app/accounting/tests/unit/asset_base/test_unit_asset_base_api_fields.py @@ -56,7 +56,7 @@ class AssetBaseAPITestCases( 'serial_number': '65756756756', } - url_ns_name = 'accounting:_api_v2_asset' + url_ns_name = 'accounting:_api_asset' """Url namespace (optional, if not required) and url name""" @@ -69,7 +69,7 @@ class AssetBaseAPIInheritedCases( model = None - url_ns_name = 'accounting:_api_v2_asset_sub' + url_ns_name = 'accounting:_api_asset_sub' diff --git a/app/accounting/tests/unit/asset_base/test_unit_asset_base_viewset.py b/app/accounting/tests/unit/asset_base/test_unit_asset_base_viewset.py index be249034..e1ba6496 100644 --- a/app/accounting/tests/unit/asset_base/test_unit_asset_base_viewset.py +++ b/app/accounting/tests/unit/asset_base/test_unit_asset_base_viewset.py @@ -88,7 +88,7 @@ class AssetBaseViewsetInheritedCases( model: str = None """name of the model to test""" - route_name = 'v2:accounting:_api_v2_asset_sub' + route_name = 'v2:accounting:_api_asset_sub' @@ -100,6 +100,6 @@ class AssetBaseViewsetTest( kwargs = {} - route_name = 'v2:accounting:_api_v2_asset' + route_name = 'v2:accounting:_api_asset' viewset = NoDocsViewSet diff --git a/app/accounting/urls.py b/app/accounting/urls.py index 9136c804..364d002a 100644 --- a/app/accounting/urls.py +++ b/app/accounting/urls.py @@ -42,7 +42,7 @@ asset_type_names = str(asset_type_names)[:-1] if not asset_type_names: asset_type_names = 'none' -router.register(f'asset/(?P[{asset_type_names}]+)?', asset.ViewSet, feature_flag = '2025-00004', basename='_api_v2_asset_sub') -router.register('asset', asset.NoDocsViewSet, feature_flag = '2025-00004', basename='_api_v2_asset') +router.register(f'asset/(?P[{asset_type_names}]+)?', asset.ViewSet, feature_flag = '2025-00004', basename='_api_asset_sub') +router.register('asset', asset.NoDocsViewSet, feature_flag = '2025-00004', basename='_api_asset') urlpatterns = router.urls diff --git a/app/tests/fixtures/model_assetbase.py b/app/tests/fixtures/model_assetbase.py new file mode 100644 index 00000000..21e58e89 --- /dev/null +++ b/app/tests/fixtures/model_assetbase.py @@ -0,0 +1,26 @@ +import datetime +import pytest + +from accounting.models.asset_base import AssetBase + +@pytest.fixture( scope = 'class') +def model_assetbase(): + + yield AssetBase + + +@pytest.fixture( scope = 'class') +def kwargs_assetbase( kwargs_centurionmodel, model_assetbase ): + + random_str = str(datetime.datetime.now(tz=datetime.timezone.utc)) + random_str = str(random_str).replace( + ' ', '').replace(':', '').replace('+', '').replace('.', '') + + kwargs = { + **kwargs_centurionmodel.copy(), + 'asset_number': 'ab_' + random_str, + 'serial_number': 'ab_' + random_str, + # 'asset_type': (model_assetbase._meta.sub_model_type, model_assetbase._meta.verbose_name), + } + + yield kwargs.copy() From de7c52d73304a186732517eecf2850953c679a6b Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 10 Jul 2025 19:48:21 +0930 Subject: [PATCH 06/23] refactor(accounting): Switch to inherit from Centurion model for model AssetBase ref: #862 #857 --- app/accounting/serializers/{asset.py => assetbase.py} | 0 app/accounting/tests/functional/asset_base/conftest.py | 2 +- app/accounting/viewsets/asset.py | 10 +++++----- 3 files changed, 6 insertions(+), 6 deletions(-) rename app/accounting/serializers/{asset.py => assetbase.py} (100%) diff --git a/app/accounting/serializers/asset.py b/app/accounting/serializers/assetbase.py similarity index 100% rename from app/accounting/serializers/asset.py rename to app/accounting/serializers/assetbase.py diff --git a/app/accounting/tests/functional/asset_base/conftest.py b/app/accounting/tests/functional/asset_base/conftest.py index b71e31a0..d548ad57 100644 --- a/app/accounting/tests/functional/asset_base/conftest.py +++ b/app/accounting/tests/functional/asset_base/conftest.py @@ -18,7 +18,7 @@ def model(request): @pytest.fixture(scope='function') def create_serializer(): - from accounting.serializers.asset import ModelSerializer + from accounting.serializers.assetbase import ModelSerializer yield ModelSerializer diff --git a/app/accounting/viewsets/asset.py b/app/accounting/viewsets/asset.py index 516f7b69..db4abdb5 100644 --- a/app/accounting/viewsets/asset.py +++ b/app/accounting/viewsets/asset.py @@ -12,7 +12,7 @@ from drf_spectacular.utils import ( from accounting.models.asset_base import AssetBase -from api.viewsets.common import SubModelViewSet +from api.viewsets.common import SubModelViewSet_ReWrite @@ -25,7 +25,7 @@ def spectacular_request_serializers( serializer_type = 'Model'): if issubclass(model, AssetBase): - serializer_name = 'asset' + serializer_name = 'assetbase' if( model._meta.model_name == 'assetbase' @@ -34,7 +34,7 @@ def spectacular_request_serializers( serializer_type = 'Model'): continue - serializer_name += '_' + model._meta.sub_model_type + serializer_name += '_' + model._meta.model_name serializer_module = importlib.import_module( model._meta.app_label + '.serializers.' + str( @@ -214,7 +214,7 @@ def spectacular_request_serializers( serializer_type = 'Model'): } ), ) -class ViewSet( SubModelViewSet ): +class ViewSet( SubModelViewSet_ReWrite ): _has_purge: bool = False """User Permission @@ -232,7 +232,7 @@ class ViewSet( SubModelViewSet ): # 'is_deleted' ] - model_kwarg = 'asset_model' + model_kwarg = 'model_name' search_fields = [ 'asset_number', From e4e7ad915bae969455b24c9d5df38c58c49676bd Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 10 Jul 2025 19:50:04 +0930 Subject: [PATCH 07/23] refactor(itam): Switch to inherit from Centurion model for model ITAMAssetBase ref: #862 #858 --- ...0025_itamassetbaseaudithistory_and_more.py | 81 +++++++++++++++++++ app/itam/models/itam_asset_base.py | 80 +++++++++++++----- ...it_asset.py => assetbase_itamassetbase.py} | 2 +- .../centurion_erp/user/core/markdown.md | 2 +- 4 files changed, 142 insertions(+), 23 deletions(-) create mode 100644 app/itam/migrations/0025_itamassetbaseaudithistory_and_more.py rename app/itam/serializers/{asset_it_asset.py => assetbase_itamassetbase.py} (97%) diff --git a/app/itam/migrations/0025_itamassetbaseaudithistory_and_more.py b/app/itam/migrations/0025_itamassetbaseaudithistory_and_more.py new file mode 100644 index 00000000..01a79dfd --- /dev/null +++ b/app/itam/migrations/0025_itamassetbaseaudithistory_and_more.py @@ -0,0 +1,81 @@ +# Generated by Django 5.1.10 on 2025-07-10 09:10 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0033_alter_ticketcommentcategory_parent_and_more"), + ("itam", "0024_alter_software_organization"), + ] + + operations = [ + migrations.CreateModel( + name="ITAMAssetBaseAuditHistory", + fields=[ + ( + "centurionaudit_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="core.centurionaudit", + ), + ), + ( + "model", + models.ForeignKey( + help_text="Model this history belongs to", + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="itam.itamassetbase", + verbose_name="Model", + ), + ), + ], + options={ + "verbose_name": "IT Asset History", + "verbose_name_plural": "IT Asset Histories", + "db_table": "itam_itamassetbase_audithistory", + "managed": True, + }, + bases=("core.centurionaudit",), + ), + migrations.CreateModel( + name="ITAMAssetBaseCenturionModelNote", + fields=[ + ( + "centurionmodelnote_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="core.centurionmodelnote", + ), + ), + ( + "model", + models.ForeignKey( + help_text="Model this note belongs to", + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="itam.itamassetbase", + verbose_name="Model", + ), + ), + ], + options={ + "verbose_name": "IT Asset Note", + "verbose_name_plural": "IT Asset Notes", + "db_table": "itam_itamassetbase_centurionmodelnote", + "managed": True, + }, + bases=("core.centurionmodelnote",), + ), + ] diff --git a/app/itam/models/itam_asset_base.py b/app/itam/models/itam_asset_base.py index b79533be..ea5054ff 100644 --- a/app/itam/models/itam_asset_base.py +++ b/app/itam/models/itam_asset_base.py @@ -1,4 +1,5 @@ from django.apps import apps +from django.conf import settings from django.db import models from rest_framework.reverse import reverse @@ -18,9 +19,13 @@ class ITAMAssetBase( **Don't** use this model directly, it should be used via a sub-model. """ + _is_submodel = True + app_namespace = None - note_basename = 'accounting:_api_v2_asset_note' + model_tag = 'it_asset' + + url_model_name = 'itamassetbase' class Meta: @@ -69,7 +74,7 @@ class ITAMAssetBase( if( ( isinstance(model, ITAMAssetBase) or issubclass(model, ITAMAssetBase) ) - and ITAMAssetBase._meta.itam_sub_model_type != 'itam_base' + # and ITAMAssetBase._meta.itam_sub_model_type != 'itam_base' ): @@ -89,7 +94,7 @@ class ITAMAssetBase( ) - + page_layout: list = [ { "name": "Details", @@ -159,24 +164,9 @@ class ITAMAssetBase( return self._meta.verbose_name + ' - ' + self.asset_number - - - def get_url( self, request = None ) -> str: - - kwargs = self.get_url_kwargs() - - url_path_name = '_api_v2_itam_asset' - - if request: - - return reverse(f"v2:{url_path_name}-detail", request=request, kwargs = kwargs ) - return reverse(f"v2:{url_path_name}-detail", kwargs = kwargs ) - - - - def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + def clean_fields(self, exclude = None): related_model = self.get_related_model() @@ -184,8 +174,56 @@ class ITAMAssetBase( related_model = self - if self.itam_type != str(related_model._meta.itam_sub_model_type).lower().replace(' ', '_'): + if( + self.itam_type != str(related_model._meta.itam_sub_model_type).lower().replace(' ', '_') + and str(related_model._meta.sub_model_type).lower().replace(' ', '_') != 'itam_base' + ): self.itam_type = str(related_model._meta.itam_sub_model_type).lower().replace(' ', '_') - super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields) + + super().clean_fields(exclude = exclude) + + + + def get_url( + self, relative: bool = False, api_version: int = 2, many = False, request: any = None + ) -> str: + + namespace = f'v{api_version}' + + if self.get_app_namespace(): + namespace = namespace + ':' + self.get_app_namespace() + + + url_basename = f'{namespace}:_api_{self._meta.model_name}' + + if self.url_model_name: + + url_basename = f'{namespace}:_api_{self.url_model_name}' + + if ( + self._is_submodel + and self._meta.sub_model_type != 'it_asset' + ): + + url_basename += '_sub' + + + if many: + + url_basename += '-list' + + else: + + url_basename += '-detail' + + + url = reverse( viewname = url_basename, kwargs = self.get_url_kwargs( many = many ) ) + + if not relative: + + url = settings.SITE_URL + url + + + return url diff --git a/app/itam/serializers/asset_it_asset.py b/app/itam/serializers/assetbase_itamassetbase.py similarity index 97% rename from app/itam/serializers/asset_it_asset.py rename to app/itam/serializers/assetbase_itamassetbase.py index 650e531d..809af8fb 100644 --- a/app/itam/serializers/asset_it_asset.py +++ b/app/itam/serializers/assetbase_itamassetbase.py @@ -4,7 +4,7 @@ from drf_spectacular.utils import extend_schema_serializer from access.serializers.organization import TenantBaseSerializer -from accounting.serializers.asset import ( +from accounting.serializers.assetbase import ( BaseSerializer, ModelSerializer as AssetBaseModelSerializer, ViewSerializer as AssetBaseViewSerializer, diff --git a/docs/projects/centurion_erp/user/core/markdown.md b/docs/projects/centurion_erp/user/core/markdown.md index b0d0e86e..d50c0f0a 100644 --- a/docs/projects/centurion_erp/user/core/markdown.md +++ b/docs/projects/centurion_erp/user/core/markdown.md @@ -87,7 +87,7 @@ A Model link is a reference to an item within the database. Supported model link | gitrepository| `$git_repository-` | | gitgroup| `$git_group-` | | group| `$-` | -| it_asset | `$it_asset-` | +| it_asset | `$asset-` or `$it_asset-` | | knowledgebase| `$kb-` | | knowledgebasecategory| `$kb_category-` | | manufacturer| `$manufacturer-` | From 3b5673ae4b1e2721f8fe300ccf27333da72bfe06 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 10 Jul 2025 19:50:53 +0930 Subject: [PATCH 08/23] feat(itam): Add Notes Serializer for ITAMAssetBase model ref: #862 #858 --- .../centurionmodelnote_itamassetbase.py | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 app/itam/serializers/centurionmodelnote_itamassetbase.py diff --git a/app/itam/serializers/centurionmodelnote_itamassetbase.py b/app/itam/serializers/centurionmodelnote_itamassetbase.py new file mode 100644 index 00000000..f1b6adac --- /dev/null +++ b/app/itam/serializers/centurionmodelnote_itamassetbase.py @@ -0,0 +1,87 @@ +from rest_framework import serializers + +from drf_spectacular.utils import extend_schema_serializer + +from access.serializers.organization import (TenantBaseSerializer) + +from centurion.models.meta import ITAMAssetBaseCenturionModelNote # pylint: disable=E0401:import-error disable=E0611:no-name-in-module + +from core.serializers.centurionmodelnote import ( # pylint: disable=W0611:unused-import + BaseSerializer, + ModelSerializer as BaseModelModelSerializer, + ViewSerializer as BaseModelViewSerializer +) + + + +@extend_schema_serializer(component_name = 'ITAMAssetBaseModelNoteModelSerializer') +class ModelSerializer( + BaseModelModelSerializer, +): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item) -> dict: + + return { + '_self': item.get_url( request = self._context['view'].request ), + } + + + class Meta: + + model = ITAMAssetBaseCenturionModelNote + + fields = [ + 'id', + 'organization', + 'display_name', + 'body', + 'created_by', + 'modified_by', + 'content_type', + 'model', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'organization', + 'created_by', + 'modified_by', + 'content_type', + 'model', + 'created', + 'modified', + '_urls', + ] + + + + def validate(self, attrs): + + is_valid = False + + note_model = self.Meta.model.model.field.related_model + + attrs['model'] = note_model.objects.get( + id = int( self.context['view'].kwargs['model_id'] ) + ) + + + is_valid = super().validate(attrs) + + return is_valid + + +@extend_schema_serializer(component_name = 'ITAMAssetBaseModelNoteViewSerializer') +class ViewSerializer( + ModelSerializer, + BaseModelViewSerializer, +): + + organization = TenantBaseSerializer( many = False, read_only = True ) From 38b4542a584595e652e95cc6fa8977183dd8c095 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 10 Jul 2025 19:51:13 +0930 Subject: [PATCH 09/23] feat(itam): Add AuditHistory Serializer for ITAMAssetBase model ref: #862 #858 --- .../centurionaudit_itamassetbase.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 app/itam/serializers/centurionaudit_itamassetbase.py diff --git a/app/itam/serializers/centurionaudit_itamassetbase.py b/app/itam/serializers/centurionaudit_itamassetbase.py new file mode 100644 index 00000000..1f9e4a6f --- /dev/null +++ b/app/itam/serializers/centurionaudit_itamassetbase.py @@ -0,0 +1,56 @@ +from rest_framework import serializers + +from drf_spectacular.utils import extend_schema_serializer + +from api.serializers import common + +from centurion.models.meta import ITAMAssetBaseAuditHistory # pylint: disable=E0401:import-error disable=E0611:no-name-in-module + +from core.serializers.centurionaudit import ( + BaseSerializer, + ViewSerializer as AuditHistoryViewSerializer +) + + + + +@extend_schema_serializer(component_name = 'ITAMAssetBaseAuditHistoryModelSerializer') +class ModelSerializer( + common.CommonModelSerializer, + BaseSerializer +): + """Git Group Audit History Base Model""" + + + _urls = serializers.SerializerMethodField('get_url') + + + class Meta: + + model = ITAMAssetBaseAuditHistory + + fields = [ + 'id', + 'organization', + 'display_name', + 'content_type', + 'model', + 'before', + 'after', + 'action', + 'user', + 'created', + '_urls', + ] + + read_only_fields = fields + + + +@extend_schema_serializer(component_name = 'ITAMAssetBaseAuditHistoryViewSerializer') +class ViewSerializer( + ModelSerializer, + AuditHistoryViewSerializer, +): + """Git Group Audit History Base View Model""" + pass From b851388d3747cf5499bd3aa40c55ad11caa0f67d Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 10 Jul 2025 19:53:38 +0930 Subject: [PATCH 10/23] refactor(itam): Update Test Suite for ITAMAssetBase model ref: #862 #858 --- ...test_functional_itamasset_base_metadata.py | 4 + ...st_functional_itamasset_base_permission.py | 4 + ...st_functional_itamasset_base_serializer.py | 3 + .../test_functional_itamasset_base_viewset.py | 4 + .../test_unit_itam_asset_base_api_fields.py | 4 + .../test_unit_itam_asset_base_model.py | 194 +++++------------- .../test_unit_itam_asset_base_viewset.py | 4 + app/tests/fixtures/__init__.py | 10 + app/tests/fixtures/model_itamassetbase.py | 24 +++ pyproject.toml | 1 + 10 files changed, 105 insertions(+), 147 deletions(-) create mode 100644 app/tests/fixtures/model_itamassetbase.py diff --git a/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_metadata.py b/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_metadata.py index a8f68f76..5050458d 100644 --- a/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_metadata.py +++ b/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_metadata.py @@ -1,3 +1,5 @@ +import pytest + from django.test import TestCase from accounting.tests.functional.asset_base.test_functional_asset_base_metadata import AssetBaseMetadataInheritedCases @@ -6,6 +8,7 @@ from itam.models.itam_asset_base import ITAMAssetBase +@pytest.mark.model_itamassetbase class MetadataTestCases( AssetBaseMetadataInheritedCases, ): @@ -38,6 +41,7 @@ class ITAMAssetBaseMetadataInheritedCases( +@pytest.mark.module_accounting class ITAMAssetBaseMetadataTest( MetadataTestCases, TestCase, diff --git a/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_permission.py b/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_permission.py index 4b6a2000..46097288 100644 --- a/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_permission.py +++ b/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_permission.py @@ -1,7 +1,10 @@ +import pytest + from accounting.tests.functional.asset_base.test_functional_asset_base_permission import AssetBasePermissionsAPIInheritedCases +@pytest.mark.model_itamassetbase class PermissionsAPITestCases( AssetBasePermissionsAPIInheritedCases, ): @@ -40,6 +43,7 @@ class ITAMAssetBasePermissionsAPIInheritedCases( +@pytest.mark.module_accounting class ITAMAssetBasePermissionsAPIPyTest( PermissionsAPITestCases, ): diff --git a/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_serializer.py b/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_serializer.py index d53aed12..63805667 100644 --- a/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_serializer.py +++ b/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_serializer.py @@ -1,3 +1,4 @@ +import pytest from accounting.tests.functional.asset_base.test_functional_asset_base_serializer import AssetBaseSerializerInheritedCases @@ -25,6 +26,7 @@ class MockView: +@pytest.mark.model_itamassetbase class ITAMAssetBaseSerializerTestCases( AssetBaseSerializerInheritedCases ): @@ -54,6 +56,7 @@ class ITAMAssetBaseSerializerInheritedCases( +@pytest.mark.module_accounting class ITAMAssetBaseSerializerPyTest( ITAMAssetBaseSerializerTestCases, ): diff --git a/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_viewset.py b/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_viewset.py index a5babd89..5c6247fd 100644 --- a/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_viewset.py +++ b/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_viewset.py @@ -1,3 +1,5 @@ +import pytest + from django.test import TestCase from accounting.tests.functional.asset_base.test_functional_asset_base_viewset import AssetBaseViewSetInheritedCases @@ -6,6 +8,7 @@ from itam.models.itam_asset_base import ITAMAssetBase +@pytest.mark.model_itamassetbase class ViewSetTestCases( AssetBaseViewSetInheritedCases ): @@ -42,6 +45,7 @@ class ITAMAssetBaseViewSetInheritedCases( +@pytest.mark.module_accounting class ITAMAssetBaseViewSetTest( ViewSetTestCases, TestCase, diff --git a/app/itam/tests/unit/itam_asset_base/test_unit_itam_asset_base_api_fields.py b/app/itam/tests/unit/itam_asset_base/test_unit_itam_asset_base_api_fields.py index c6060e55..6a535538 100644 --- a/app/itam/tests/unit/itam_asset_base/test_unit_itam_asset_base_api_fields.py +++ b/app/itam/tests/unit/itam_asset_base/test_unit_itam_asset_base_api_fields.py @@ -1,9 +1,12 @@ +import pytest + from accounting.tests.unit.asset_base.test_unit_asset_base_api_fields import ( AssetBaseAPIInheritedCases ) +@pytest.mark.model_itamassetbase class ITAMAssetBaseAPITestCases( AssetBaseAPIInheritedCases, ): @@ -29,6 +32,7 @@ class ITAMAssetBaseAPIInheritedCases( +@pytest.mark.module_accounting class ITAMAssetBaseAPIPyTest( ITAMAssetBaseAPITestCases, ): diff --git a/app/itam/tests/unit/itam_asset_base/test_unit_itam_asset_base_model.py b/app/itam/tests/unit/itam_asset_base/test_unit_itam_asset_base_model.py index 3e7b735c..9e412e80 100644 --- a/app/itam/tests/unit/itam_asset_base/test_unit_itam_asset_base_model.py +++ b/app/itam/tests/unit/itam_asset_base/test_unit_itam_asset_base_model.py @@ -1,177 +1,77 @@ +import pytest + from django.db import models -from accounting.tests.unit.asset_base.test_unit_asset_base_model import AssetBaseModelInheritedCases +from accounting.tests.unit.asset_base.test_unit_asset_base_model import ( + AssetBaseModelInheritedCases, +) from itam.models.itam_asset_base import ITAMAssetBase -class ITAMAssetBaseModelTestCases( - AssetBaseModelInheritedCases, +@pytest.mark.model_itamassetbase +class ITAMAssetModelTestCases( + AssetBaseModelInheritedCases ): - kwargs_create_item: dict = {} - it_asset_base_model = ITAMAssetBase + @property + def parameterized_class_attributes(self): - sub_model_type = 'itam_base' - """Sub Model Type - - sub-models must have this attribute defined in `ModelName.Meta.sub_model_type` - """ + return { + '_is_submodel': { + 'value': True + }, + 'app_namespace': { + 'type': type(None), + 'value': None + }, + 'model_tag': { + 'type': str, + 'value': 'it_asset' + }, + 'url_model_name': { + 'type': str, + 'value': 'itamassetbase' + }, + } - parameterized_fields: dict = { - "itam_type": { - 'field_type': models.fields.CharField, - 'field_parameter_default_exists': True, - 'field_parameter_default_value': ITAMAssetBase._meta.itam_sub_model_type, - 'field_parameter_verbose_name_type': str + @property + def parameterized_model_fields(self): + + return { + 'itam_type': { + 'blank': True, + 'default': 'itam_base', + 'field_type': models.CharField, + 'max_length': 30, + 'null': False, + 'unique': False, } } - - def test_class_inherits_itam_assetbase(self): + def test_class_inherits_itamassetbase(self, model): """ Class inheritence TenancyObject must inherit SaveHistory """ - assert issubclass(self.model, ITAMAssetBase) - - - def test_attribute_meta_exists_itam_sub_model_type(self): - """Attribute check - - meta.itam_sub_model_type must exist - """ - - assert hasattr(self.model()._meta, 'itam_sub_model_type') - - - def test_sanity_is_it_asset_sub_model(self): - """Sanity Test - - This test ensures that the model being tested `self.model` is a - sub-model of `self.it_asset_base_model`. - This test is required as the same viewset is used for all sub-models - of `ITAMAssetBase` - """ - - assert issubclass(self.model, self.it_asset_base_model) - - - def test_attribute_meta_type_itam_sub_model_type(self): - """Attribute type - - meta.itam_sub_model_type must be of type str - """ - - assert type(self.model()._meta.itam_sub_model_type) is str + assert issubclass(model, ITAMAssetBase) - def test_attribute_type_app_namespace(self): - """Attribute Type - - app_namespace is of type str - """ - - assert self.model.app_namespace is None - - - def test_attribute_value_app_namespace(self): - """Attribute Type - - app_namespace has been set, override this test case with the value - of attribute `app_namespace` - """ - - assert self.model.app_namespace is None - - - def test_attribute_type_note_basename(self): - """Attribute Type - - note_basename is of type str - """ - - assert type(self.model.note_basename) is str - - - def test_attribute_value_note_basename(self): - """Attribute Type - - note_basename has been set, override this test case with the value - of attribute `note_basename` - """ - - assert self.model.note_basename == 'accounting:_api_v2_asset_note' - - - def test_function_is_property_get_itam_model_type(self): - """Function test - - Confirm function `get_itam_model_type` is a property - """ - - assert type(self.model.get_itam_model_type) is property - - - def test_function_value_get_itam_model_type(self): - """Function test - - Confirm function `get_itam_model_type` is a property - """ - - assert self.item.get_itam_model_type is None - - - def test_function_value_get_url(self): - - assert self.item.get_url() == '/api/v2/itam/it_asset/' + str(self.item.id) - - - -class ITAMAssetBaseModelInheritedCases( - ITAMAssetBaseModelTestCases, +class ITAMAssetModelInheritedCases( + ITAMAssetModelTestCases, ): - """Sub-Ticket Test Cases - - Test Cases for Ticket models that inherit from model AssetBase - """ - - kwargs_create_item: dict = {} - - model = None - - - sub_model_type = None - """Ticket Sub Model Type - - Ticket sub-models must have this attribute defined in `ModelNam.Meta.sub_model_type` - """ - - - def test_function_value_not_None_get_itam_model_type(self): - """Function test - - Confirm function `get_itam_model_type` is a property - """ - - assert self.item.get_itam_model_type is not None + pass -class ITAMAssetBaseModelPyTest( - ITAMAssetBaseModelTestCases, +@pytest.mark.module_accounting +class ITAMAssetModelPyTest( + ITAMAssetModelTestCases, ): - - def test_function_value_get_related_model(self): - """Function test - - Confirm function `get_related_model` is None for base model - """ - - assert self.item.get_related_model() is None + pass diff --git a/app/itam/tests/unit/itam_asset_base/test_unit_itam_asset_base_viewset.py b/app/itam/tests/unit/itam_asset_base/test_unit_itam_asset_base_viewset.py index af0f76b3..71d263f9 100644 --- a/app/itam/tests/unit/itam_asset_base/test_unit_itam_asset_base_viewset.py +++ b/app/itam/tests/unit/itam_asset_base/test_unit_itam_asset_base_viewset.py @@ -1,3 +1,5 @@ +import pytest + from django.test import Client, TestCase from rest_framework.reverse import reverse @@ -22,6 +24,7 @@ from itam.models.itam_asset_base import ITAMAssetBase +@pytest.mark.model_itamassetbase class ITAMAssetBaseViewsetTestCases( AssetBaseViewsetInheritedCases, ): @@ -45,6 +48,7 @@ class ITAMAssetBaseViewsetInheritedCases( +@pytest.mark.module_accounting class ITAMAssetBaseViewsetTest( ITAMAssetBaseViewsetTestCases, TestCase, diff --git a/app/tests/fixtures/__init__.py b/app/tests/fixtures/__init__.py index 95ae41ae..09f3e196 100644 --- a/app/tests/fixtures/__init__.py +++ b/app/tests/fixtures/__init__.py @@ -18,6 +18,11 @@ from .model_appsettings import ( model_appsettings, ) +from .model_assetbase import ( + kwargs_assetbase, + model_assetbase, +) + from .model_centurionaudit import ( kwargs_centurionaudit, model_centurionaudit, @@ -160,6 +165,11 @@ from .model_instance import ( model_instance ) +from .model_itamassetbase import ( + kwargs_itamassetbase, + model_itamassetbase, +) + from .model_knowledgebase import ( kwargs_knowledgebase, model_knowledgebase, diff --git a/app/tests/fixtures/model_itamassetbase.py b/app/tests/fixtures/model_itamassetbase.py new file mode 100644 index 00000000..b45d8fbc --- /dev/null +++ b/app/tests/fixtures/model_itamassetbase.py @@ -0,0 +1,24 @@ +import datetime +import pytest + +from itam.models.itam_asset_base import ITAMAssetBase + +@pytest.fixture( scope = 'class') +def model_itamassetbase(): + + yield ITAMAssetBase + + +@pytest.fixture( scope = 'class') +def kwargs_itamassetbase( kwargs_assetbase, model_itamassetbase ): + + random_str = str(datetime.datetime.now(tz=datetime.timezone.utc)) + random_str = str(random_str).replace( + ' ', '').replace(':', '').replace('+', '').replace('.', '') + + kwargs = { + **kwargs_assetbase.copy(), + # 'asset_type': (model_itamassetbase._meta.sub_model_type, model_itamassetbase._meta.verbose_name), + } + + yield kwargs.copy() diff --git a/pyproject.toml b/pyproject.toml index b14ce833..ddd85910 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1125,6 +1125,7 @@ markers = [ "model_githubrepository: Selects tests for model `github repository`", "model_gitlabrepository: Selects tests for model `gitlab repository`", "model_gitrepository: Selects tests for model `git repository`", + "model_itamassetbase: Selects tests for model ITAM Asset Base.", "model_knowledgebase: Selects Knowledge base tests.", "model_knowledgebasecategory: Selects Knowledge base category tests.", "model_manufacturer: Select all manufacturer tests.", From 018004ccb9eba2d5b493e3399bedc5a62b9d63a1 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 10 Jul 2025 19:54:28 +0930 Subject: [PATCH 11/23] refactor(itam): Update URL route name for ITAMAssetBase model ref: #862 #858 --- app/api/react_ui_metadata.py | 4 ++-- .../test_functional_itamasset_base_metadata.py | 2 +- ...test_functional_itamasset_base_permission.py | 2 +- .../test_functional_itamasset_base_viewset.py | 2 +- app/itam/tests/unit/itam_asset_base/conftest.py | 17 +++++++++++------ .../test_unit_itam_asset_base_viewset.py | 2 +- app/itam/urls_api.py | 4 ++-- 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/app/api/react_ui_metadata.py b/app/api/react_ui_metadata.py index beec133c..83991a86 100644 --- a/app/api/react_ui_metadata.py +++ b/app/api/react_ui_metadata.py @@ -373,7 +373,7 @@ class ReactUIMetadata(OverRideJSONAPIMetadata): def get_nav_items(self, request) -> dict: - + nav = { 'access': { "display_name": "Access", @@ -638,7 +638,7 @@ class ReactUIMetadata(OverRideJSONAPIMetadata): 'view_itamassetbase': { "display_name": "IT Assets", "name": "itasset", - "link": "/itam/it_asset" + "link": "/itam/itamassetbase" }, **nav['itam']['pages'] } diff --git a/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_metadata.py b/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_metadata.py index 5050458d..66d9efea 100644 --- a/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_metadata.py +++ b/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_metadata.py @@ -25,7 +25,7 @@ class MetadataTestCases( url_view_kwargs: dict = {} - url_name = '_api_v2_itam_asset' + url_name = '_api_itamassetbase' diff --git a/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_permission.py b/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_permission.py index 46097288..6f694893 100644 --- a/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_permission.py +++ b/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_permission.py @@ -23,7 +23,7 @@ class PermissionsAPITestCases( 'asset_model': 'it_asset', } - url_name = '_api_v2_itam_asset' + url_name = '_api_itamassetbase' url_view_kwargs: dict = { 'asset_model': 'it_asset', diff --git a/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_viewset.py b/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_viewset.py index 5c6247fd..bbbe52cd 100644 --- a/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_viewset.py +++ b/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_viewset.py @@ -31,7 +31,7 @@ class ViewSetTestCases( 'asset_model': 'it_asset', } - url_name = '_api_v2_itam_asset' + url_name = '_api_itamassetbase' diff --git a/app/itam/tests/unit/itam_asset_base/conftest.py b/app/itam/tests/unit/itam_asset_base/conftest.py index eeae4944..e46a5291 100644 --- a/app/itam/tests/unit/itam_asset_base/conftest.py +++ b/app/itam/tests/unit/itam_asset_base/conftest.py @@ -1,14 +1,19 @@ import pytest -from itam.models.itam_asset_base import ITAMAssetBase - @pytest.fixture( scope = 'class') -def model(request): +def model(model_itamassetbase): - request.cls.model = ITAMAssetBase + yield model_itamassetbase - yield request.cls.model - del request.cls.model +@pytest.fixture( scope = 'class', autouse = True) +def model_kwargs(request, kwargs_itamassetbase): + + request.cls.kwargs_create_item = kwargs_itamassetbase.copy() + + yield kwargs_itamassetbase.copy() + + if hasattr(request.cls, 'kwargs_create_item'): + del request.cls.kwargs_create_item diff --git a/app/itam/tests/unit/itam_asset_base/test_unit_itam_asset_base_viewset.py b/app/itam/tests/unit/itam_asset_base/test_unit_itam_asset_base_viewset.py index 71d263f9..103c13f9 100644 --- a/app/itam/tests/unit/itam_asset_base/test_unit_itam_asset_base_viewset.py +++ b/app/itam/tests/unit/itam_asset_base/test_unit_itam_asset_base_viewset.py @@ -44,7 +44,7 @@ class ITAMAssetBaseViewsetInheritedCases( model: str = None """name of the model to test""" - route_name = 'v2:accounting:_api_v2_asset_sub' + route_name = 'v2:accounting:_api_asset_sub' diff --git a/app/itam/urls_api.py b/app/itam/urls_api.py index ed15bc5e..46760edd 100644 --- a/app/itam/urls_api.py +++ b/app/itam/urls_api.py @@ -34,8 +34,8 @@ router.register( basename = '_api_v2_itam_home' ) router.register( - prefix = '(?P[it_asset]+)', viewset = asset.ViewSet, - feature_flag = '2025-00007', basename = '_api_v2_itam_asset' + prefix = '(?P[itamassetbase]+)', viewset = asset.ViewSet, + feature_flag = '2025-00007', basename = '_api_itamassetbase' ) router.register( prefix = 'device', viewset = device.ViewSet, From 4f36769fbadcc1a4e5d0d36ec4dd579586c38df8 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 10 Jul 2025 20:22:43 +0930 Subject: [PATCH 12/23] refactor(accounting): Update existing tests to work due to model inheritance changes ref: #862 #857 --- .../tests/unit/asset_base/conftest.py | 5 ++- .../test_unit_asset_base_viewset.py | 33 +++++++++++++++++-- .../functional/test_functional_api_fields.py | 2 +- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/app/accounting/tests/unit/asset_base/conftest.py b/app/accounting/tests/unit/asset_base/conftest.py index a39588d2..e8cb6891 100644 --- a/app/accounting/tests/unit/asset_base/conftest.py +++ b/app/accounting/tests/unit/asset_base/conftest.py @@ -16,4 +16,7 @@ def model_kwargs(request, kwargs_assetbase): yield kwargs_assetbase.copy() if hasattr(request.cls, 'kwargs_create_item'): - del request.cls.kwargs_create_item + try: + del request.cls.kwargs_create_item + except: + pass diff --git a/app/accounting/tests/unit/asset_base/test_unit_asset_base_viewset.py b/app/accounting/tests/unit/asset_base/test_unit_asset_base_viewset.py index e1ba6496..9de4f208 100644 --- a/app/accounting/tests/unit/asset_base/test_unit_asset_base_viewset.py +++ b/app/accounting/tests/unit/asset_base/test_unit_asset_base_viewset.py @@ -13,6 +13,10 @@ from accounting.viewsets.asset import ( from api.tests.unit.test_unit_common_viewset import SubModelViewSetInheritedCases +from centurion.tests.abstract.mock_view import MockRequest + +from settings.models.app_settings import AppSettings + @pytest.mark.model_assetbase @@ -46,7 +50,7 @@ class AssetBaseViewsetTestCases( if self.model is not AssetBase: self.kwargs = { - 'asset_model': self.model._meta.sub_model_type + 'model_name': self.model._meta.sub_model_type } self.viewset.kwargs = self.kwargs @@ -63,6 +67,8 @@ class AssetBaseViewsetTestCases( self.http_options_response_list = client.options(url) + a = 'a' + def test_view_attr_value_model_kwarg(self): @@ -73,7 +79,30 @@ class AssetBaseViewsetTestCases( view_set = self.viewset() - assert view_set.model_kwarg == 'asset_model' + assert view_set.model_kwarg == 'model_name' + + + + def test_view_attr_model_value(self): + """Attribute Test + + Attribute `model` must return the correct sub-model + """ + + view_set = self.viewset() + + + app_settings = AppSettings.objects.select_related('global_organization').get( + owner_organization = None + ) + + + view_set.request = MockRequest( + user = self.view_user, + app_settings = app_settings, + ) + + assert view_set.model == self.model diff --git a/app/api/tests/functional/test_functional_api_fields.py b/app/api/tests/functional/test_functional_api_fields.py index 4a984e31..c2d60117 100644 --- a/app/api/tests/functional/test_functional_api_fields.py +++ b/app/api/tests/functional/test_functional_api_fields.py @@ -169,7 +169,7 @@ class APIFieldsTestCases: view_team.permissions.set([view_permissions]) - request.cls.view_user = User.objects.create_user(username="cafs_test_user_view" + str(random_str), password="password") + request.cls.view_user = User.objects.create_user(username="cafs_test_user_view" + str(random_str), password="password", is_superuser = True) team_user = TeamUsers.objects.create( team = view_team, From d6eb33141ad57c8c5938caaef86211f76cd367d7 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Jul 2025 14:20:10 +0930 Subject: [PATCH 13/23] refactor: Asset and ITAM Asset must use url kwarg model_name not asset_model ref: #862 closes #857 #858 --- .../asset_base/test_functional_asset_base_metadata.py | 4 ++-- .../asset_base/test_functional_asset_base_permission.py | 4 ++-- .../asset_base/test_functional_asset_base_viewset.py | 4 ++-- .../unit/asset_base/test_unit_asset_base_api_fields.py | 2 +- app/accounting/urls.py | 4 ++-- app/accounting/viewsets/asset.py | 8 ++++---- app/itam/tests/functional/itamasset_base/conftest.py | 2 +- .../test_functional_itamasset_base_permission.py | 4 ++-- .../test_functional_itamasset_base_viewset.py | 4 ++-- app/itam/tests/unit/itam_asset_base/conftest.py | 5 ++++- app/tests/fixtures/model_itamassetbase.py | 1 + 11 files changed, 23 insertions(+), 19 deletions(-) diff --git a/app/accounting/tests/functional/asset_base/test_functional_asset_base_metadata.py b/app/accounting/tests/functional/asset_base/test_functional_asset_base_metadata.py index ce7ff4bd..77ada79e 100644 --- a/app/accounting/tests/functional/asset_base/test_functional_asset_base_metadata.py +++ b/app/accounting/tests/functional/asset_base/test_functional_asset_base_metadata.py @@ -254,11 +254,11 @@ class AssetBaseMetadataInheritedCases( } self.url_kwargs = { - 'asset_model': self.model._meta.sub_model_type + 'model_name': self.model._meta.model_name } self.url_view_kwargs = { - 'asset_model': self.model._meta.sub_model_type + 'model_name': self.model._meta.model_name } super().setUpTestData() diff --git a/app/accounting/tests/functional/asset_base/test_functional_asset_base_permission.py b/app/accounting/tests/functional/asset_base/test_functional_asset_base_permission.py index 95329646..36cdbdfa 100644 --- a/app/accounting/tests/functional/asset_base/test_functional_asset_base_permission.py +++ b/app/accounting/tests/functional/asset_base/test_functional_asset_base_permission.py @@ -73,11 +73,11 @@ class AssetBasePermissionsAPIInheritedCases( def inherited_var_setup(self, request): request.cls.url_kwargs.update({ - 'asset_model': self.model._meta.sub_model_type + 'model_name': self.model._meta.model_name }) request.cls.url_view_kwargs.update({ - 'asset_model': self.model._meta.sub_model_type + 'model_name': self.model._meta.model_name }) diff --git a/app/accounting/tests/functional/asset_base/test_functional_asset_base_viewset.py b/app/accounting/tests/functional/asset_base/test_functional_asset_base_viewset.py index c427259b..957ce886 100644 --- a/app/accounting/tests/functional/asset_base/test_functional_asset_base_viewset.py +++ b/app/accounting/tests/functional/asset_base/test_functional_asset_base_viewset.py @@ -257,11 +257,11 @@ class AssetBaseViewSetInheritedCases( } self.url_kwargs = { - 'asset_model': self.model._meta.sub_model_type + 'model_name': self.model._meta.model_name } self.url_view_kwargs = { - 'asset_model': self.model._meta.sub_model_type + 'model_name': self.model._meta.model_name } super().setUpTestData() diff --git a/app/accounting/tests/unit/asset_base/test_unit_asset_base_api_fields.py b/app/accounting/tests/unit/asset_base/test_unit_asset_base_api_fields.py index 29044cda..98506a70 100644 --- a/app/accounting/tests/unit/asset_base/test_unit_asset_base_api_fields.py +++ b/app/accounting/tests/unit/asset_base/test_unit_asset_base_api_fields.py @@ -24,7 +24,7 @@ class AssetBaseAPITestCases( if model != self.base_model: request.cls.url_view_kwargs.update({ - 'asset_model': model._meta.sub_model_type, + 'model_name': model._meta.model_name, }) diff --git a/app/accounting/urls.py b/app/accounting/urls.py index 364d002a..408c9d86 100644 --- a/app/accounting/urls.py +++ b/app/accounting/urls.py @@ -33,7 +33,7 @@ for model in apps.get_models(): if model._meta.sub_model_type == 'asset': continue - asset_type_names += model._meta.sub_model_type + '|' + asset_type_names += model._meta.model_name + '|' @@ -42,7 +42,7 @@ asset_type_names = str(asset_type_names)[:-1] if not asset_type_names: asset_type_names = 'none' -router.register(f'asset/(?P[{asset_type_names}]+)?', asset.ViewSet, feature_flag = '2025-00004', basename='_api_asset_sub') +router.register(f'asset/(?P[{asset_type_names}]+)?', asset.ViewSet, feature_flag = '2025-00004', basename='_api_asset_sub') router.register('asset', asset.NoDocsViewSet, feature_flag = '2025-00004', basename='_api_asset') urlpatterns = router.urls diff --git a/app/accounting/viewsets/asset.py b/app/accounting/viewsets/asset.py index db4abdb5..4034fafa 100644 --- a/app/accounting/viewsets/asset.py +++ b/app/accounting/viewsets/asset.py @@ -56,7 +56,7 @@ def spectacular_request_serializers( serializer_type = 'Model'): description='.', parameters = [ OpenApiParameter( - name = 'asset_model', + name = 'model_name', description = 'Enter the asset type. This is the name of the asset sub-model.', location = OpenApiParameter.PATH, type = str, @@ -97,7 +97,7 @@ def spectacular_request_serializers( serializer_type = 'Model'): description = '.', parameters =[ OpenApiParameter( - name = 'asset_model', + name = 'model_name', description = 'Enter the asset type. This is the name of the asset sub-model.', location = OpenApiParameter.PATH, type = str, @@ -121,7 +121,7 @@ def spectacular_request_serializers( serializer_type = 'Model'): description='.', parameters = [ OpenApiParameter( - name = 'asset_model', + name = 'model_name', description = 'Enter the asset model. This is the name of the asset sub-model.', location = OpenApiParameter.PATH, type = str, @@ -153,7 +153,7 @@ def spectacular_request_serializers( serializer_type = 'Model'): description='.', parameters = [ OpenApiParameter( - name = 'asset_model', + name = 'model_name', description = 'Enter the asset model. This is the name of the Asset sub-model.', location = OpenApiParameter.PATH, type = str, diff --git a/app/itam/tests/functional/itamasset_base/conftest.py b/app/itam/tests/functional/itamasset_base/conftest.py index 8a12f4c4..dcec13c4 100644 --- a/app/itam/tests/functional/itamasset_base/conftest.py +++ b/app/itam/tests/functional/itamasset_base/conftest.py @@ -18,7 +18,7 @@ def model(request): @pytest.fixture(scope='function') def create_serializer(): - from itam.serializers.asset_it_asset import ModelSerializer + from itam.serializers.assetbase_itamassetbase import ModelSerializer yield ModelSerializer diff --git a/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_permission.py b/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_permission.py index 6f694893..0889d757 100644 --- a/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_permission.py +++ b/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_permission.py @@ -20,13 +20,13 @@ class PermissionsAPITestCases( kwargs_create_item_diff_org: dict = {} url_kwargs: dict = { - 'asset_model': 'it_asset', + 'model_name': 'itamassetbase', } url_name = '_api_itamassetbase' url_view_kwargs: dict = { - 'asset_model': 'it_asset', + 'model_name': 'itamassetbase', } diff --git a/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_viewset.py b/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_viewset.py index bbbe52cd..5a7b52b2 100644 --- a/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_viewset.py +++ b/app/itam/tests/functional/itamasset_base/test_functional_itamasset_base_viewset.py @@ -24,11 +24,11 @@ class ViewSetTestCases( model = ITAMAssetBase url_kwargs: dict = { - 'asset_model': 'it_asset', + 'model_name': 'itamassetbase', } url_view_kwargs: dict = { - 'asset_model': 'it_asset', + 'model_name': 'itamassetbase', } url_name = '_api_itamassetbase' diff --git a/app/itam/tests/unit/itam_asset_base/conftest.py b/app/itam/tests/unit/itam_asset_base/conftest.py index e46a5291..6fdb089d 100644 --- a/app/itam/tests/unit/itam_asset_base/conftest.py +++ b/app/itam/tests/unit/itam_asset_base/conftest.py @@ -16,4 +16,7 @@ def model_kwargs(request, kwargs_itamassetbase): yield kwargs_itamassetbase.copy() if hasattr(request.cls, 'kwargs_create_item'): - del request.cls.kwargs_create_item + try: + del request.cls.kwargs_create_item + except: + pass diff --git a/app/tests/fixtures/model_itamassetbase.py b/app/tests/fixtures/model_itamassetbase.py index b45d8fbc..717581cb 100644 --- a/app/tests/fixtures/model_itamassetbase.py +++ b/app/tests/fixtures/model_itamassetbase.py @@ -19,6 +19,7 @@ def kwargs_itamassetbase( kwargs_assetbase, model_itamassetbase ): kwargs = { **kwargs_assetbase.copy(), # 'asset_type': (model_itamassetbase._meta.sub_model_type, model_itamassetbase._meta.verbose_name), + 'itam_type': "it_asset" } yield kwargs.copy() From 548051df6da8188f1c7fa50cad72a3790770480f Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Jul 2025 16:42:59 +0930 Subject: [PATCH 14/23] refactor(access): Switch to inherit from Centurion model for model Role ref: #862 #844 --- app/access/models/role.py | 43 ++++++++------------------------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/app/access/models/role.py b/app/access/models/role.py index 36b20a8f..eff3c7ea 100644 --- a/app/access/models/role.py +++ b/app/access/models/role.py @@ -1,15 +1,20 @@ from django.contrib.auth.models import Permission from django.db import models -from access.fields import AutoCreatedField, AutoLastModifiedField -from access.models.tenancy import TenancyObject +from access.fields import AutoLastModifiedField + +from core.models.centurion import CenturionModel class Role( - TenancyObject + CenturionModel ): + documentation = '' + + model_tag = 'role' + class Meta: @@ -28,14 +33,6 @@ class Role( verbose_name_plural = 'Roles' - id = models.AutoField( - blank=False, - help_text = 'Primary key of the entry', - primary_key=True, - unique=True, - verbose_name = 'ID' - ) - name = models.CharField( blank = False, help_text = 'Name of this role', @@ -53,21 +50,15 @@ class Role( verbose_name = 'Permissions' ) - created = AutoCreatedField() - modified = AutoLastModifiedField() - is_global = None - def __str__(self) -> str: - + return str( self.organization ) + ' / ' + self.name - documentation = '' - page_layout: dict = [ { "name": "Details", @@ -156,19 +147,3 @@ class Role( return self._permissions_int return self._permissions_int - - - - - - def save_history(self, before: dict, after: dict) -> bool: - - from access.models.role_history import RoleHistory - - history = super().save_history( - before = before, - after = after, - history_model = RoleHistory - ) - - return history From e0c8ef46f59ad2d62ddec842b330cf711b6d8ff3 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Jul 2025 16:43:58 +0930 Subject: [PATCH 15/23] feat(access): Add Notes Serializer for Role model ref: #862 #844 --- app/access/models/__init__.py | 1 - app/access/models/role_notes.py | 45 ---------- .../serializers/centurionmodelnote_role.py | 87 +++++++++++++++++++ 3 files changed, 87 insertions(+), 46 deletions(-) delete mode 100644 app/access/models/role_notes.py create mode 100644 app/access/serializers/centurionmodelnote_role.py diff --git a/app/access/models/__init__.py b/app/access/models/__init__.py index 6c463b72..ad194733 100644 --- a/app/access/models/__init__.py +++ b/app/access/models/__init__.py @@ -2,4 +2,3 @@ from .organization_history import OrganizationHistory # pylint: disable=W0611 from .role_history import RoleHistory # pylint: disable=W0611:unused-import from .organization_notes import OrganizationNotes # pylint: disable=W0611:unused-import -from .role_notes import RoleNotes # pylint: disable=W0611:unused-import diff --git a/app/access/models/role_notes.py b/app/access/models/role_notes.py deleted file mode 100644 index 00e6811f..00000000 --- a/app/access/models/role_notes.py +++ /dev/null @@ -1,45 +0,0 @@ -from django.db import models - -from access.models.role import Role - -from core.models.model_notes import ModelNotes - - - -class RoleNotes( - ModelNotes -): - - - class Meta: - - db_table = 'access_role_notes' - - ordering = ModelNotes._meta.ordering - - verbose_name = 'Role Note' - - verbose_name_plural = 'Role Notes' - - - model = models.ForeignKey( - Role, - blank = False, - help_text = 'Model this note belongs to', - null = False, - on_delete = models.CASCADE, - related_name = 'notes', - verbose_name = 'Model', - ) - - table_fields: list = [] - - page_layout: dict = [] - - - def get_url_kwargs(self) -> dict: - - return { - 'model_id': self.model.pk, - 'pk': self.pk - } diff --git a/app/access/serializers/centurionmodelnote_role.py b/app/access/serializers/centurionmodelnote_role.py new file mode 100644 index 00000000..1750bacb --- /dev/null +++ b/app/access/serializers/centurionmodelnote_role.py @@ -0,0 +1,87 @@ +from rest_framework import serializers + +from drf_spectacular.utils import extend_schema_serializer + +from access.serializers.organization import (TenantBaseSerializer) + +from centurion.models.meta import RoleCenturionModelNote # pylint: disable=E0401:import-error disable=E0611:no-name-in-module + +from core.serializers.centurionmodelnote import ( # pylint: disable=W0611:unused-import + BaseSerializer, + ModelSerializer as BaseModelModelSerializer, + ViewSerializer as BaseModelViewSerializer +) + + + +@extend_schema_serializer(component_name = 'RoleModelNoteModelSerializer') +class ModelSerializer( + BaseModelModelSerializer, +): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item) -> dict: + + return { + '_self': item.get_url( request = self._context['view'].request ), + } + + + class Meta: + + model = RoleCenturionModelNote + + fields = [ + 'id', + 'organization', + 'display_name', + 'body', + 'created_by', + 'modified_by', + 'content_type', + 'model', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'organization', + 'created_by', + 'modified_by', + 'content_type', + 'model', + 'created', + 'modified', + '_urls', + ] + + + + def validate(self, attrs): + + is_valid = False + + note_model = self.Meta.model.model.field.related_model + + attrs['model'] = note_model.objects.get( + id = int( self.context['view'].kwargs['model_id'] ) + ) + + + is_valid = super().validate(attrs) + + return is_valid + + +@extend_schema_serializer(component_name = 'RoleModelNoteViewSerializer') +class ViewSerializer( + ModelSerializer, + BaseModelViewSerializer, +): + + organization = TenantBaseSerializer( many = False, read_only = True ) From 953dce29843ac7c6235f959fc89cbc1e093e3c6e Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Jul 2025 16:44:43 +0930 Subject: [PATCH 16/23] feat(access): Add AuditHistory Serializer for Role model ref: #862 #844 --- app/access/models/__init__.py | 1 - app/access/models/role_history.py | 53 ------------------ app/access/serializers/centurionaudit_role.py | 56 +++++++++++++++++++ 3 files changed, 56 insertions(+), 54 deletions(-) delete mode 100644 app/access/models/role_history.py create mode 100644 app/access/serializers/centurionaudit_role.py diff --git a/app/access/models/__init__.py b/app/access/models/__init__.py index ad194733..3bd409fe 100644 --- a/app/access/models/__init__.py +++ b/app/access/models/__init__.py @@ -1,4 +1,3 @@ from .organization_history import OrganizationHistory # pylint: disable=W0611:unused-import -from .role_history import RoleHistory # pylint: disable=W0611:unused-import from .organization_notes import OrganizationNotes # pylint: disable=W0611:unused-import diff --git a/app/access/models/role_history.py b/app/access/models/role_history.py deleted file mode 100644 index 7cfc39b4..00000000 --- a/app/access/models/role_history.py +++ /dev/null @@ -1,53 +0,0 @@ -from django.db import models - -from core.models.model_history import ModelHistory - -from access.models.role import Role - - - -class RoleHistory( - ModelHistory -): - - - class Meta: - - db_table = 'access_role_history' - - ordering = ModelHistory._meta.ordering - - verbose_name = 'Role History' - - verbose_name_plural = 'Role History' - - - model = models.ForeignKey( - Role, - blank = False, - help_text = 'Model this note belongs to', - null = False, - on_delete = models.CASCADE, - related_name = 'history', - verbose_name = 'Model', - ) - - table_fields: list = [] - - page_layout: dict = [] - - - def get_object(self): - - return self - - - def get_serialized_model(self, serializer_context): - - model = None - - from access.serializers.role import BaseSerializer - - model = BaseSerializer(self.model, context = serializer_context) - - return model diff --git a/app/access/serializers/centurionaudit_role.py b/app/access/serializers/centurionaudit_role.py new file mode 100644 index 00000000..a53f569d --- /dev/null +++ b/app/access/serializers/centurionaudit_role.py @@ -0,0 +1,56 @@ +from rest_framework import serializers + +from drf_spectacular.utils import extend_schema_serializer + +from api.serializers import common + +from centurion.models.meta import RoleAuditHistory # pylint: disable=E0401:import-error disable=E0611:no-name-in-module + +from core.serializers.centurionaudit import ( + BaseSerializer, + ViewSerializer as AuditHistoryViewSerializer +) + + + + +@extend_schema_serializer(component_name = 'RoleAuditHistoryModelSerializer') +class ModelSerializer( + common.CommonModelSerializer, + BaseSerializer +): + """Git Group Audit History Base Model""" + + + _urls = serializers.SerializerMethodField('get_url') + + + class Meta: + + model = RoleAuditHistory + + fields = [ + 'id', + 'organization', + 'display_name', + 'content_type', + 'model', + 'before', + 'after', + 'action', + 'user', + 'created', + '_urls', + ] + + read_only_fields = fields + + + +@extend_schema_serializer(component_name = 'RoleAuditHistoryViewSerializer') +class ViewSerializer( + ModelSerializer, + AuditHistoryViewSerializer, +): + """Git Group Audit History Base View Model""" + pass From 3b7e88969e695f561c4f7f6c3fdff1344c436fab Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Jul 2025 16:56:07 +0930 Subject: [PATCH 17/23] refactor(access): Update Test Suite for Role model ref: #862 #844 --- .../role/test_functional_role_serializer.py | 2 + .../role/test_functional_role_viewset.py | 4 ++ app/access/tests/unit/role/conftest.py | 19 +++++ .../tests/unit/role/test_unit_role_api_v2.py | 5 +- .../tests/unit/role/test_unit_role_model.py | 72 ++++++++++++++----- .../unit/role/test_unit_role_serializer.py | 2 + .../tests/unit/role/test_unit_role_viewset.py | 4 ++ app/access/viewsets/role.py | 2 - app/fixtures/fresh_db.sql | 11 ++- app/tests/fixtures/__init__.py | 5 ++ app/tests/fixtures/model_role.py | 29 ++++++++ pyproject.toml | 1 + 12 files changed, 131 insertions(+), 25 deletions(-) create mode 100644 app/access/tests/unit/role/conftest.py create mode 100644 app/tests/fixtures/model_role.py diff --git a/app/access/tests/functional/role/test_functional_role_serializer.py b/app/access/tests/functional/role/test_functional_role_serializer.py index 62597500..27029edb 100644 --- a/app/access/tests/functional/role/test_functional_role_serializer.py +++ b/app/access/tests/functional/role/test_functional_role_serializer.py @@ -10,6 +10,8 @@ from access.serializers.role import Role, ModelSerializer +@pytest.mark.model_role +@pytest.mark.module_role class ValidationSerializer( TestCase, ): diff --git a/app/access/tests/functional/role/test_functional_role_viewset.py b/app/access/tests/functional/role/test_functional_role_viewset.py index b0b8ef18..243d84b6 100644 --- a/app/access/tests/functional/role/test_functional_role_viewset.py +++ b/app/access/tests/functional/role/test_functional_role_viewset.py @@ -1,4 +1,6 @@ import django +import pytest + from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.test import Client, TestCase @@ -20,6 +22,8 @@ User = django.contrib.auth.get_user_model() +@pytest.mark.model_role +@pytest.mark.module_role class ViewSetBase: add_data: dict = None diff --git a/app/access/tests/unit/role/conftest.py b/app/access/tests/unit/role/conftest.py new file mode 100644 index 00000000..0c5f8144 --- /dev/null +++ b/app/access/tests/unit/role/conftest.py @@ -0,0 +1,19 @@ +import pytest + + + +@pytest.fixture( scope = 'class') +def model(model_role): + + yield model_role + + +@pytest.fixture( scope = 'class', autouse = True) +def model_kwargs(request, kwargs_role): + + request.cls.kwargs_create_item = kwargs_role.copy() + + yield kwargs_role.copy() + + if hasattr(request.cls, 'kwargs_create_item'): + del request.cls.kwargs_create_item diff --git a/app/access/tests/unit/role/test_unit_role_api_v2.py b/app/access/tests/unit/role/test_unit_role_api_v2.py index 2efcac9e..4424f10d 100644 --- a/app/access/tests/unit/role/test_unit_role_api_v2.py +++ b/app/access/tests/unit/role/test_unit_role_api_v2.py @@ -1,3 +1,4 @@ +import pytest import django from django.contrib.auth.models import Permission @@ -18,6 +19,7 @@ User = django.contrib.auth.get_user_model() +@pytest.mark.model_role class APITestCases( APITenancyObject, ): @@ -48,7 +50,7 @@ class APITestCases( **self.kwargs_item_create ) - + self.url_view_kwargs = { 'pk': self.item.id } @@ -154,6 +156,7 @@ class APITestCases( +@pytest.mark.module_role class RoleAPITest( APITestCases, TestCase, diff --git a/app/access/tests/unit/role/test_unit_role_model.py b/app/access/tests/unit/role/test_unit_role_model.py index d60bb2a0..9ea6ee09 100644 --- a/app/access/tests/unit/role/test_unit_role_model.py +++ b/app/access/tests/unit/role/test_unit_role_model.py @@ -1,30 +1,70 @@ -from django.test import TestCase +import pytest -from access.models.role import Role +from django.db import models -from centurion.tests.unit.test_unit_models import ( - TenancyObjectInheritedCases + +from core.tests.unit.centurion_abstract.test_unit_centurion_abstract_model import ( + CenturionAbstractModelInheritedCases ) +@pytest.mark.model_role class RoleModelTestCases( - TenancyObjectInheritedCases, + CenturionAbstractModelInheritedCases ): - model = None - kwargs_item_create: dict = None + @property + def parameterized_class_attributes(self): + + return { + 'model_tag': { + 'type': str, + 'value': 'role' + }, + } + @property + def parameterized_model_fields(self): -class RoleModelTest( - RoleModelTestCases, - TestCase, -): - - model = Role - - kwargs_item_create: dict = { - 'name': 'a role' + return { + 'name': { + 'blank': False, + 'default': models.fields.NOT_PROVIDED, + 'field_type': models.CharField, + 'max_length': 30, + 'null': False, + 'unique': False, + }, + 'permissions': { + 'blank': True, + 'default': models.fields.NOT_PROVIDED, + 'field_type': models.ManyToManyField, + 'null': False, + 'unique': False, + }, + 'modified': { + 'blank': False, + 'default': models.fields.NOT_PROVIDED, + 'field_type': models.DateTimeField, + 'null': False, + 'unique': False, + }, } + + + +class RoleModelInheritedCases( + RoleModelTestCases, +): + pass + + + +@pytest.mark.module_role +class RoleModelPyTest( + RoleModelTestCases, +): + pass diff --git a/app/access/tests/unit/role/test_unit_role_serializer.py b/app/access/tests/unit/role/test_unit_role_serializer.py index b169a1c0..2ab91b9b 100644 --- a/app/access/tests/unit/role/test_unit_role_serializer.py +++ b/app/access/tests/unit/role/test_unit_role_serializer.py @@ -20,6 +20,8 @@ import pytest ############################################################################### +@pytest.mark.model_role +@pytest.mark.module_role @pytest.mark.skip( reason = 'figure out how to isolate so entirety of unit tests can run without this test failing' ) # @pytest.mark.forked # @pytest.mark.django_db diff --git a/app/access/tests/unit/role/test_unit_role_viewset.py b/app/access/tests/unit/role/test_unit_role_viewset.py index f6c70283..1fc610cc 100644 --- a/app/access/tests/unit/role/test_unit_role_viewset.py +++ b/app/access/tests/unit/role/test_unit_role_viewset.py @@ -1,3 +1,5 @@ +import pytest + from django.test import Client, TestCase from rest_framework.reverse import reverse @@ -9,6 +11,7 @@ from api.tests.unit.test_unit_common_viewset import ModelViewSetInheritedCases +@pytest.mark.model_role class ViewsetTestCases( ModelViewSetInheritedCases, ): @@ -44,6 +47,7 @@ class ViewsetTestCases( +@pytest.mark.module_role class RoleViewsetTest( ViewsetTestCases, TestCase, diff --git a/app/access/viewsets/role.py b/app/access/viewsets/role.py index 96f4e3b6..864ea02f 100644 --- a/app/access/viewsets/role.py +++ b/app/access/viewsets/role.py @@ -1,7 +1,5 @@ from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse -# THis import only exists so that the migrations can be created -from access.models.role_history import RoleHistory # pylint: disable=W0611:unused-import from access.serializers.role import ( Role, ModelSerializer, diff --git a/app/fixtures/fresh_db.sql b/app/fixtures/fresh_db.sql index 2fa1fadb..4e8fd728 100644 --- a/app/fixtures/fresh_db.sql +++ b/app/fixtures/fresh_db.sql @@ -76,10 +76,7 @@ CREATE TABLE IF NOT EXISTS "assistance_knowledge_base_history" ("modelhistory_pt CREATE TABLE IF NOT EXISTS "access_team_history" ("modelhistory_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_history" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_team" ("group_ptr_id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "core_ticketcategory_notes" ("modelnotes_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_notes" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "core_ticketcategory" ("id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "core_ticketcommentcategory_notes" ("modelnotes_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_notes" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "core_ticketcommentcategory" ("id") DEFERRABLE INITIALLY DEFERRED); -CREATE TABLE IF NOT EXISTS "access_role" ("model_notes" text NULL, "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL, "created" datetime NOT NULL, "modified" datetime NOT NULL, "organization_id" integer NOT NULL REFERENCES "access_tenant" ("id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "access_role_permissions" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "role_id" integer NOT NULL REFERENCES "access_role" ("id") DEFERRABLE INITIALLY DEFERRED, "permission_id" integer NOT NULL REFERENCES "auth_permission" ("id") DEFERRABLE INITIALLY DEFERRED); -CREATE TABLE IF NOT EXISTS "access_role_history" ("modelhistory_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_history" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_role" ("id") DEFERRABLE INITIALLY DEFERRED); -CREATE TABLE IF NOT EXISTS "access_role_notes" ("modelnotes_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_notes" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_role" ("id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "access_contact" ("person_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "access_person" ("entity_ptr_id") DEFERRABLE INITIALLY DEFERRED, "directory" bool NOT NULL, "email" varchar(254) NOT NULL UNIQUE); CREATE TABLE IF NOT EXISTS "core_ticketbase_assigned_to" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "ticketbase_id" integer NOT NULL REFERENCES "core_ticketbase" ("id") DEFERRABLE INITIALLY DEFERRED, "entity_id" integer NOT NULL REFERENCES "access_entity" ("id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "core_ticketbase_subscribed_to" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "ticketbase_id" integer NOT NULL REFERENCES "core_ticketbase" ("id") DEFERRABLE INITIALLY DEFERRED, "entity_id" integer NOT NULL REFERENCES "access_entity" ("id") DEFERRABLE INITIALLY DEFERRED); @@ -139,6 +136,7 @@ CREATE TABLE IF NOT EXISTS "access_person_audithistory" ("centurionaudit_ptr_id" CREATE TABLE IF NOT EXISTS "access_person_centurionmodelnote" ("centurionmodelnote_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_centurionmodelnote" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_person" ("entity_ptr_id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "access_company_audithistory" ("centurionaudit_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_audithistory" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_company" ("entity_ptr_id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "access_company_centurionmodelnote" ("centurionmodelnote_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_centurionmodelnote" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_company" ("entity_ptr_id") DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE IF NOT EXISTS "access_role" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL, "created" datetime NOT NULL, "modified" datetime NOT NULL, "organization_id" integer NOT NULL REFERENCES "access_tenant" ("id") DEFERRABLE INITIALLY DEFERRED, "model_notes" text NULL); CREATE TABLE IF NOT EXISTS "accounting_assetbase" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "asset_number" varchar(30) NULL UNIQUE, "serial_number" varchar(30) NULL UNIQUE, "asset_type" varchar(30) NOT NULL, "created" datetime NOT NULL, "modified" datetime NOT NULL, "organization_id" integer NOT NULL REFERENCES "access_tenant" ("id") DEFERRABLE INITIALLY DEFERRED, "model_notes" text NULL); CREATE TABLE IF NOT EXISTS "accounting_assetbase_audithistory" ("centurionaudit_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_audithistory" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "accounting_assetbase" ("id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "accounting_assetbase_centurionmodelnote" ("centurionmodelnote_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_centurionmodelnote" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "accounting_assetbase" ("id") DEFERRABLE INITIALLY DEFERRED); @@ -239,9 +237,9 @@ CREATE TABLE IF NOT EXISTS "social_auth_nonce" ("id" integer NOT NULL PRIMARY KE CREATE TABLE IF NOT EXISTS "social_auth_usersocialauth" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "provider" varchar(32) NOT NULL, "uid" varchar(255) NOT NULL, "user_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED, "created" datetime NOT NULL, "modified" datetime NOT NULL, "extra_data" text NOT NULL CHECK ((JSON_VALID("extra_data") OR "extra_data" IS NULL))); CREATE TABLE IF NOT EXISTS "social_auth_partial" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "token" varchar(32) NOT NULL, "next_step" smallint unsigned NOT NULL CHECK ("next_step" >= 0), "backend" varchar(32) NOT NULL, "timestamp" datetime NOT NULL, "data" text NOT NULL CHECK ((JSON_VALID("data") OR "data" IS NULL))); DELETE FROM sqlite_sequence; -INSERT INTO sqlite_sequence VALUES('django_migrations',225); -INSERT INTO sqlite_sequence VALUES('django_content_type',218); -INSERT INTO sqlite_sequence VALUES('auth_permission',917); +INSERT INTO sqlite_sequence VALUES('django_migrations',226); +INSERT INTO sqlite_sequence VALUES('django_content_type',216); +INSERT INTO sqlite_sequence VALUES('auth_permission',909); INSERT INTO sqlite_sequence VALUES('auth_group',0); INSERT INTO sqlite_sequence VALUES('auth_user',0); INSERT INTO sqlite_sequence VALUES('core_notes',0); @@ -263,6 +261,7 @@ INSERT INTO sqlite_sequence VALUES('assistance_knowledgebasecategory',0); INSERT INTO sqlite_sequence VALUES('assistance_modelknowledgebasearticle',0); INSERT INTO sqlite_sequence VALUES('access_tenant',0); INSERT INTO sqlite_sequence VALUES('access_entity',0); +INSERT INTO sqlite_sequence VALUES('access_role',0); INSERT INTO sqlite_sequence VALUES('accounting_assetbase',0); INSERT INTO sqlite_sequence VALUES('django_admin_log',0); INSERT INTO sqlite_sequence VALUES('itam_devicetype',0); diff --git a/app/tests/fixtures/__init__.py b/app/tests/fixtures/__init__.py index 09f3e196..466a6155 100644 --- a/app/tests/fixtures/__init__.py +++ b/app/tests/fixtures/__init__.py @@ -233,6 +233,11 @@ from .model_projecttype import ( model_projecttype, ) +from .model_role import ( + kwargs_role, + model_role, +) + from .model_service import ( kwargs_service, model_service, diff --git a/app/tests/fixtures/model_role.py b/app/tests/fixtures/model_role.py new file mode 100644 index 00000000..8e9ea832 --- /dev/null +++ b/app/tests/fixtures/model_role.py @@ -0,0 +1,29 @@ +import datetime +import pytest + +from access.models.role import Role + + + +@pytest.fixture( scope = 'class') +def model_role(): + + yield Role + + +@pytest.fixture( scope = 'class') +def kwargs_role( + kwargs_centurionmodel +): + + random_str = str(datetime.datetime.now(tz=datetime.timezone.utc)) + random_str = str(random_str).replace( + ' ', '').replace(':', '').replace('-', '').replace('+', '').replace('.', '') + + kwargs = { + **kwargs_centurionmodel.copy(), + 'name': 'r_' + random_str, + 'modified': '2024-06-03T23:00:00Z', + } + + yield kwargs.copy() diff --git a/pyproject.toml b/pyproject.toml index ddd85910..dffe2c2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1137,6 +1137,7 @@ markers = [ "model_projectmilestone: Selects tests for model Project Milestone.", "model_projectstate: Selects tests for model Project State.", "model_projecttype: Selects tests for model Project Type.", + "model_role: Selects tests for model Role.", "model_service: Selects tests for model Service.", "model_software: Selects tests for model Software.", "model_softwarecategory: Selects tests for model Software Category.", From ca63e4d6fce09eb6dabb11bada331985686e3dcd Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Jul 2025 16:56:41 +0930 Subject: [PATCH 18/23] refactor(access): Update URL route name for Role model ref: #862 #844 --- .../tests/functional/role/test_functional_role_viewset.py | 6 +++--- app/access/tests/unit/role/test_unit_role_api_v2.py | 2 +- app/access/tests/unit/role/test_unit_role_viewset.py | 2 +- app/access/urls_api.py | 7 +------ app/access/viewsets/index.py | 2 +- 5 files changed, 7 insertions(+), 12 deletions(-) diff --git a/app/access/tests/functional/role/test_functional_role_viewset.py b/app/access/tests/functional/role/test_functional_role_viewset.py index 243d84b6..5fe25816 100644 --- a/app/access/tests/functional/role/test_functional_role_viewset.py +++ b/app/access/tests/functional/role/test_functional_role_viewset.py @@ -242,7 +242,7 @@ class RolePermissionsAPITest( url_view_kwargs: dict = {} - url_name = '_api_v2_role' + url_name = '_api_role' @@ -264,7 +264,7 @@ class RoleViewSetTest( url_view_kwargs: dict = {} - url_name = '_api_v2_role' + url_name = '_api_role' @@ -287,4 +287,4 @@ class RoleMetadataTest( url_view_kwargs: dict = {} - url_name = '_api_v2_role' + url_name = '_api_role' diff --git a/app/access/tests/unit/role/test_unit_role_api_v2.py b/app/access/tests/unit/role/test_unit_role_api_v2.py index 4424f10d..81d3cd86 100644 --- a/app/access/tests/unit/role/test_unit_role_api_v2.py +++ b/app/access/tests/unit/role/test_unit_role_api_v2.py @@ -166,7 +166,7 @@ class RoleAPITest( model = Role - url_ns_name = '_api_v2_role' + url_ns_name = '_api_role' @classmethod diff --git a/app/access/tests/unit/role/test_unit_role_viewset.py b/app/access/tests/unit/role/test_unit_role_viewset.py index 1fc610cc..465d5437 100644 --- a/app/access/tests/unit/role/test_unit_role_viewset.py +++ b/app/access/tests/unit/role/test_unit_role_viewset.py @@ -55,6 +55,6 @@ class RoleViewsetTest( kwargs = {} - route_name = 'v2:_api_v2_role' + route_name = 'v2:_api_role' viewset = ViewSet diff --git a/app/access/urls_api.py b/app/access/urls_api.py index f89d39fd..bd9a96cf 100644 --- a/app/access/urls_api.py +++ b/app/access/urls_api.py @@ -82,12 +82,7 @@ router.register( router.register( prefix = 'role', viewset = role.ViewSet, - feature_flag = '2025-00003', basename = '_api_v2_role' + feature_flag = '2025-00003', basename = '_api_role' ) -# router.register( -# prefix = 'role/(?P[0-9]+)/notes', viewset = role_notes.ViewSet, -# feature_flag = '2025-00003', basename = '_api_v2_role_note' -# ) - urlpatterns = router.urls diff --git a/app/access/viewsets/index.py b/app/access/viewsets/index.py index 9d30f58b..a46a4738 100644 --- a/app/access/viewsets/index.py +++ b/app/access/viewsets/index.py @@ -37,7 +37,7 @@ class Index(IndexViewset): if self.request.feature_flag['2025-00003']: response.update({ - "role": reverse( 'v2:_api_v2_role-list', request=request ), + "role": reverse( 'v2:_api_role-list', request=request ), }) From b2e0982b4ca425c55d29cadb1bc41bf9be5be1b5 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Jul 2025 16:57:13 +0930 Subject: [PATCH 19/23] refactor(access): Migrations for Inheritance change for Role model ref: #862 closes #844 --- .../0020_remove_rolenotes_model_and_more.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 app/access/migrations/0020_remove_rolenotes_model_and_more.py diff --git a/app/access/migrations/0020_remove_rolenotes_model_and_more.py b/app/access/migrations/0020_remove_rolenotes_model_and_more.py new file mode 100644 index 00000000..1c8a651e --- /dev/null +++ b/app/access/migrations/0020_remove_rolenotes_model_and_more.py @@ -0,0 +1,56 @@ +# Generated by Django 5.1.10 on 2025-07-12 07:20 + +import access.models.tenancy_abstract +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("access", "0019_companyaudithistory_companycenturionmodelnote"), + ] + + operations = [ + migrations.AlterField( + model_name="role", + name="id", + field=models.AutoField( + help_text="ID of the item", + primary_key=True, + serialize=False, + unique=True, + verbose_name="ID", + ), + ), + migrations.AlterField( + model_name="role", + name="model_notes", + field=models.TextField( + blank=True, + help_text="Tid bits of information", + null=True, + verbose_name="Notes", + ), + ), + migrations.AlterField( + model_name="role", + name="organization", + field=models.ForeignKey( + help_text="Tenant this belongs to", + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="access.tenant", + validators=[ + access.models.tenancy_abstract.TenancyAbstractModel.validatate_organization_exists + ], + verbose_name="Tenant", + ), + ), + migrations.DeleteModel( + name="RoleHistory", + ), + migrations.DeleteModel( + name="RoleNotes", + ), + ] From 957052da84550fb1943c152df12f8d808af6882a Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Jul 2025 17:30:58 +0930 Subject: [PATCH 20/23] refactor(access): Adjust add permission test for model Role ref: #862 #844 --- .../additional_role_permissions_api.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 app/access/tests/functional/additional_role_permissions_api.py diff --git a/app/access/tests/functional/additional_role_permissions_api.py b/app/access/tests/functional/additional_role_permissions_api.py new file mode 100644 index 00000000..0286f1ea --- /dev/null +++ b/app/access/tests/functional/additional_role_permissions_api.py @@ -0,0 +1,29 @@ +from django.test import Client + + +class AdditionalTestCases: + + + def test_permission_add(self, model_instance, api_request_permissions, + model_kwargs, kwargs_api_create + ): + """ Check correct permission for add + + Attempt to add as user with permission + """ + + client = Client() + + client.force_login( api_request_permissions['user']['add'] ) + + the_model = model_instance( kwargs_create = model_kwargs ) + + url = the_model.get_url( many = True ) + + response = client.post( + path = url, + data = kwargs_api_create, + content_type = 'application/json' + ) + + assert response.status_code == 200, response.content From b643347c01aa05f3d282d59abad884b246beeb80 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Jul 2025 18:21:51 +0930 Subject: [PATCH 21/23] test(access): Model Role is not usable within global org, remove test ref: #862 #844 --- .../functional/additional_role_permissions_api.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/access/tests/functional/additional_role_permissions_api.py b/app/access/tests/functional/additional_role_permissions_api.py index 0286f1ea..6a01cd3e 100644 --- a/app/access/tests/functional/additional_role_permissions_api.py +++ b/app/access/tests/functional/additional_role_permissions_api.py @@ -1,3 +1,5 @@ +import pytest + from django.test import Client @@ -27,3 +29,16 @@ class AdditionalTestCases: ) assert response.status_code == 200, response.content + + + + def test_returned_data_from_user_and_global_organizations_only( + self + ): + """Check items returned + + Items returned from the query Must be from the users organization and + global ONLY! + """ + + pytest.mark.xfail( reason = 'model is not for global use' ) From 2f7865bb836322af874c7a01353def0d719b833a Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Jul 2025 18:27:03 +0930 Subject: [PATCH 22/23] feat(access): Add Audit and notes tables for model Role ref: #862 #844 --- ...roleaudithistory_rolecenturionmodelnote.py | 81 +++++++++++++++++++ app/core/models/meta.py | 3 +- app/fixtures/fresh_db.sql | 10 ++- 3 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 app/access/migrations/0021_roleaudithistory_rolecenturionmodelnote.py diff --git a/app/access/migrations/0021_roleaudithistory_rolecenturionmodelnote.py b/app/access/migrations/0021_roleaudithistory_rolecenturionmodelnote.py new file mode 100644 index 00000000..20b9b638 --- /dev/null +++ b/app/access/migrations/0021_roleaudithistory_rolecenturionmodelnote.py @@ -0,0 +1,81 @@ +# Generated by Django 5.1.10 on 2025-07-12 08:50 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("access", "0020_remove_rolenotes_model_and_more"), + ("core", "0033_alter_ticketcommentcategory_parent_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="RoleAuditHistory", + fields=[ + ( + "centurionaudit_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="core.centurionaudit", + ), + ), + ( + "model", + models.ForeignKey( + help_text="Model this history belongs to", + on_delete=django.db.models.deletion.CASCADE, + related_name="audit_history", + to="access.role", + verbose_name="Model", + ), + ), + ], + options={ + "verbose_name": "Role History", + "verbose_name_plural": "Role Histories", + "db_table": "access_role_audithistory", + "managed": True, + }, + bases=("core.centurionaudit",), + ), + migrations.CreateModel( + name="RoleCenturionModelNote", + fields=[ + ( + "centurionmodelnote_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="core.centurionmodelnote", + ), + ), + ( + "model", + models.ForeignKey( + help_text="Model this note belongs to", + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="access.role", + verbose_name="Model", + ), + ), + ], + options={ + "verbose_name": "Role Note", + "verbose_name_plural": "Role Notes", + "db_table": "access_role_centurionmodelnote", + "managed": True, + }, + bases=("core.centurionmodelnote",), + ), + ] diff --git a/app/core/models/meta.py b/app/core/models/meta.py index 3c261fb7..6eabcf20 100644 --- a/app/core/models/meta.py +++ b/app/core/models/meta.py @@ -8,7 +8,8 @@ from django.utils.module_loading import import_string # Note: Only included so that it can be picked up. # in future when model referenced, this include statement may be repoved. from access.models.company_base import Company # pylint: disable=W0611:unused-import - +from access.models.role import Role # pylint: disable=W0611:unused-import +## EoF Include block module_path = f'centurion.models.meta' diff --git a/app/fixtures/fresh_db.sql b/app/fixtures/fresh_db.sql index 4e8fd728..1935cbf6 100644 --- a/app/fixtures/fresh_db.sql +++ b/app/fixtures/fresh_db.sql @@ -95,8 +95,8 @@ CREATE TABLE IF NOT EXISTS "devops_git_group_history" ("modelhistory_ptr_id" int CREATE TABLE IF NOT EXISTS "devops_github_repository_notes" ("modelnotes_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_notes" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "devops_githubrepository" ("gitrepository_ptr_id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "devops_gitlab_repository_notes" ("modelnotes_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_notes" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "devops_gitlabrepository" ("gitrepository_ptr_id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "devops_git_group_notes" ("modelnotes_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_notes" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "devops_gitgroup" ("id") DEFERRABLE INITIALLY DEFERRED); -CREATE TABLE IF NOT EXISTS "access_organization_history" ("modelhistory_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_history" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_tenant" ("id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "access_organization_notes" ("modelnotes_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_notes" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_tenant" ("id") DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE IF NOT EXISTS "access_organization_history" ("modelhistory_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_model_history" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_tenant" ("id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "access_company" ("entity_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "access_entity" ("id") DEFERRABLE INITIALLY DEFERRED, "name" varchar(80) NOT NULL); CREATE TABLE IF NOT EXISTS "access_person" ("entity_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "access_entity" ("id") DEFERRABLE INITIALLY DEFERRED, "f_name" varchar(64) NOT NULL, "l_name" varchar(64) NOT NULL, "dob" date NULL, "m_name" varchar(100) NULL); CREATE TABLE IF NOT EXISTS "core_ticketcommentaction" ("ticketcommentbase_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_ticketcommentbase" ("id") DEFERRABLE INITIALLY DEFERRED); @@ -137,6 +137,8 @@ CREATE TABLE IF NOT EXISTS "access_person_centurionmodelnote" ("centurionmodelno CREATE TABLE IF NOT EXISTS "access_company_audithistory" ("centurionaudit_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_audithistory" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_company" ("entity_ptr_id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "access_company_centurionmodelnote" ("centurionmodelnote_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_centurionmodelnote" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_company" ("entity_ptr_id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "access_role" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL, "created" datetime NOT NULL, "modified" datetime NOT NULL, "organization_id" integer NOT NULL REFERENCES "access_tenant" ("id") DEFERRABLE INITIALLY DEFERRED, "model_notes" text NULL); +CREATE TABLE IF NOT EXISTS "access_role_audithistory" ("centurionaudit_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_audithistory" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_role" ("id") DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE IF NOT EXISTS "access_role_centurionmodelnote" ("centurionmodelnote_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_centurionmodelnote" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "access_role" ("id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "accounting_assetbase" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "asset_number" varchar(30) NULL UNIQUE, "serial_number" varchar(30) NULL UNIQUE, "asset_type" varchar(30) NOT NULL, "created" datetime NOT NULL, "modified" datetime NOT NULL, "organization_id" integer NOT NULL REFERENCES "access_tenant" ("id") DEFERRABLE INITIALLY DEFERRED, "model_notes" text NULL); CREATE TABLE IF NOT EXISTS "accounting_assetbase_audithistory" ("centurionaudit_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_audithistory" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "accounting_assetbase" ("id") DEFERRABLE INITIALLY DEFERRED); CREATE TABLE IF NOT EXISTS "accounting_assetbase_centurionmodelnote" ("centurionmodelnote_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "core_centurionmodelnote" ("id") DEFERRABLE INITIALLY DEFERRED, "model_id" integer NOT NULL REFERENCES "accounting_assetbase" ("id") DEFERRABLE INITIALLY DEFERRED); @@ -237,9 +239,9 @@ CREATE TABLE IF NOT EXISTS "social_auth_nonce" ("id" integer NOT NULL PRIMARY KE CREATE TABLE IF NOT EXISTS "social_auth_usersocialauth" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "provider" varchar(32) NOT NULL, "uid" varchar(255) NOT NULL, "user_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED, "created" datetime NOT NULL, "modified" datetime NOT NULL, "extra_data" text NOT NULL CHECK ((JSON_VALID("extra_data") OR "extra_data" IS NULL))); CREATE TABLE IF NOT EXISTS "social_auth_partial" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "token" varchar(32) NOT NULL, "next_step" smallint unsigned NOT NULL CHECK ("next_step" >= 0), "backend" varchar(32) NOT NULL, "timestamp" datetime NOT NULL, "data" text NOT NULL CHECK ((JSON_VALID("data") OR "data" IS NULL))); DELETE FROM sqlite_sequence; -INSERT INTO sqlite_sequence VALUES('django_migrations',226); -INSERT INTO sqlite_sequence VALUES('django_content_type',216); -INSERT INTO sqlite_sequence VALUES('auth_permission',909); +INSERT INTO sqlite_sequence VALUES('django_migrations',227); +INSERT INTO sqlite_sequence VALUES('django_content_type',218); +INSERT INTO sqlite_sequence VALUES('auth_permission',917); INSERT INTO sqlite_sequence VALUES('auth_group',0); INSERT INTO sqlite_sequence VALUES('auth_user',0); INSERT INTO sqlite_sequence VALUES('core_notes',0); From be708b12447a5f990ac7aba36ab774e40e4660d4 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Jul 2025 21:11:33 +0930 Subject: [PATCH 23/23] test(fixture): if item already exists, when fetching remove modified field from query if not found with field is an auto field and most likely wont match the existing item ref: #862 --- app/tests/fixtures/model_kwarg_data.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/app/tests/fixtures/model_kwarg_data.py b/app/tests/fixtures/model_kwarg_data.py index 06080975..8cf02338 100644 --- a/app/tests/fixtures/model_kwarg_data.py +++ b/app/tests/fixtures/model_kwarg_data.py @@ -1,5 +1,5 @@ import datetime -from django.core.exceptions import ValidationError +from django.core.exceptions import ValidationError, ObjectDoesNotExist import pytest from django.db import models @@ -97,9 +97,22 @@ def model_kwarg_data(): if 'unique' in e.error_dict['__all__'][0].code: - instance = model.objects.get( - **kwargs - ) + try: + + instance = model.objects.get( + **kwargs + ) + + except ObjectDoesNotExist as e: + + if 'modified' in kwargs: + + no_modified_in_kwargs = kwargs.copy() + del no_modified_in_kwargs['modified'] + + instance = model.objects.get( + **no_modified_in_kwargs + ) for field, values in many_field.items():