From 8716ed34a16fa0cd8ae6f8e41245d3090bc3b2a9 Mon Sep 17 00:00:00 2001 From: kvncampos Date: Fri, 15 Nov 2024 15:50:53 -0600 Subject: [PATCH 1/5] enchancement for status field for contracts --- changes/337.enhancement | 1 + nautobot_device_lifecycle_mgmt/filters.py | 2 +- nautobot_device_lifecycle_mgmt/forms.py | 20 ++-- .../migrations/0023_contractlcm_status.py | 22 +++++ nautobot_device_lifecycle_mgmt/models.py | 7 ++ nautobot_device_lifecycle_mgmt/tables.py | 3 +- .../tests/conftest.py | 81 ++++++++++++++- .../tests/test_filters.py | 91 ++++++++++++++++- .../tests/test_forms.py | 98 ++++++++++++++++++- .../tests/test_model.py | 85 ++++++++++++++++ 10 files changed, 397 insertions(+), 13 deletions(-) create mode 100644 changes/337.enhancement create mode 100644 nautobot_device_lifecycle_mgmt/migrations/0023_contractlcm_status.py diff --git a/changes/337.enhancement b/changes/337.enhancement new file mode 100644 index 00000000..0213aabd --- /dev/null +++ b/changes/337.enhancement @@ -0,0 +1 @@ +Enchanced ContractLCM to include Status Field for lifecycle purposes. \ No newline at end of file diff --git a/nautobot_device_lifecycle_mgmt/filters.py b/nautobot_device_lifecycle_mgmt/filters.py index e204b9bb..28b4e67e 100644 --- a/nautobot_device_lifecycle_mgmt/filters.py +++ b/nautobot_device_lifecycle_mgmt/filters.py @@ -593,7 +593,7 @@ def _sw_missing_only(self, queryset, name, value): # pylint: disable=unused-arg return queryset -class ContractLCMFilterSet(NautobotFilterSet): +class ContractLCMFilterSet(NautobotFilterSet, StatusModelFilterSetMixin): """Filter for ContractLCMFilter.""" q = django_filters.CharFilter(method="search", label="Search") diff --git a/nautobot_device_lifecycle_mgmt/forms.py b/nautobot_device_lifecycle_mgmt/forms.py index 2af18eaa..54ec0e39 100644 --- a/nautobot_device_lifecycle_mgmt/forms.py +++ b/nautobot_device_lifecycle_mgmt/forms.py @@ -627,6 +627,7 @@ def get_form_kwargs(self): class ContractLCMBulkEditForm(NautobotBulkEditForm): """Device Lifecycle Contrcts bulk edit form.""" + model = ContractLCM pk = forms.ModelMultipleChoiceField(queryset=ContractLCM.objects.all(), widget=forms.MultipleHiddenInput) provider = forms.ModelChoiceField(queryset=ProviderLCM.objects.all(), required=False) start = forms.DateField(widget=DatePicker(), required=False) @@ -635,18 +636,14 @@ class ContractLCMBulkEditForm(NautobotBulkEditForm): currency = forms.ChoiceField(required=False, choices=CurrencyChoices.CHOICES) contract_type = forms.ChoiceField(choices=ContractTypeChoices.CHOICES, required=False) support_level = forms.CharField(required=False) + status = DynamicModelChoiceField( + queryset=Status.objects.all(), required=False, query_params={"content_types": model._meta.label_lower} + ) class Meta: """Meta attributes for the ContractLCMBulkEditForm class.""" - nullable_fields = [ - "start", - "end", - "cost", - "currency", - "support_level", - "contract_type", - ] + nullable_fields = ["start", "end", "cost", "currency", "support_level", "contract_type", "status"] class ContractLCMFilterForm(NautobotFilterForm): @@ -663,6 +660,12 @@ class ContractLCMFilterForm(NautobotFilterForm): ) name = forms.CharField(required=False) tags = TagFilterField(model) + status = DynamicModelMultipleChoiceField( + queryset=Status.objects.all(), + required=False, + query_params={"content_types": model._meta.label_lower}, + to_field_name="name", + ) class Meta: """Meta attributes for the ContractLCMFilterForm class.""" @@ -672,6 +675,7 @@ class Meta: fields = [ "q", "provider", + "status", "name", "start", "end", diff --git a/nautobot_device_lifecycle_mgmt/migrations/0023_contractlcm_status.py b/nautobot_device_lifecycle_mgmt/migrations/0023_contractlcm_status.py new file mode 100644 index 00000000..0baa9b7c --- /dev/null +++ b/nautobot_device_lifecycle_mgmt/migrations/0023_contractlcm_status.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.16 on 2024-11-13 19:52 + +import django.db.models.deletion +import nautobot.extras.models.statuses +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("extras", "0114_computedfield_grouping"), + ("nautobot_device_lifecycle_mgmt", "0022_alter_softwareimagelcm_inventory_items_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="contractlcm", + name="status", + field=nautobot.extras.models.statuses.StatusField( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to="extras.status" + ), + ), + ] diff --git a/nautobot_device_lifecycle_mgmt/models.py b/nautobot_device_lifecycle_mgmt/models.py index 380f56ff..08472668 100644 --- a/nautobot_device_lifecycle_mgmt/models.py +++ b/nautobot_device_lifecycle_mgmt/models.py @@ -417,6 +417,7 @@ def __str__(self): "graphql", "relationships", "webhooks", + "statuses", ) class ContractLCM(PrimaryModel): """ContractLCM model for app.""" @@ -442,6 +443,12 @@ class ContractLCM(PrimaryModel): verbose_name="Contract Type", max_length=CHARFIELD_MAX_LENGTH, blank=True, default="" ) devices = models.ManyToManyField(to="dcim.Device", related_name="device_contracts", blank=True) + status = StatusField( + null=True, + blank=True, + on_delete=models.PROTECT, + to="extras.status", + ) comments = models.TextField(blank=True, default="") class Meta: diff --git a/nautobot_device_lifecycle_mgmt/tables.py b/nautobot_device_lifecycle_mgmt/tables.py index 73ece33a..85c14b4f 100644 --- a/nautobot_device_lifecycle_mgmt/tables.py +++ b/nautobot_device_lifecycle_mgmt/tables.py @@ -427,7 +427,7 @@ class Meta(BaseTable.Meta): ] -class ContractLCMTable(BaseTable): +class ContractLCMTable(StatusTableMixin, BaseTable): """Table for list view.""" pk = ToggleColumn() @@ -452,6 +452,7 @@ class Meta(BaseTable.Meta): fields = ( "pk", "name", + "status", "start", "end", "cost", diff --git a/nautobot_device_lifecycle_mgmt/tests/conftest.py b/nautobot_device_lifecycle_mgmt/tests/conftest.py index 5f174d6d..a38e458f 100644 --- a/nautobot_device_lifecycle_mgmt/tests/conftest.py +++ b/nautobot_device_lifecycle_mgmt/tests/conftest.py @@ -6,7 +6,14 @@ from nautobot.dcim.models import Device, DeviceType, InventoryItem, Location, LocationType, Manufacturer, Platform from nautobot.extras.models import Role, Status -from nautobot_device_lifecycle_mgmt.models import CVELCM, HardwareLCM, SoftwareLCM, ValidatedSoftwareLCM +from nautobot_device_lifecycle_mgmt.models import ( + CVELCM, + ContractLCM, + HardwareLCM, + ProviderLCM, + SoftwareLCM, + ValidatedSoftwareLCM, +) def create_devices(): @@ -201,3 +208,75 @@ def create_inventory_item_hardware_notices(): documentation_url="https://test.com", ), ) + + +def create_contracts(): + """Create DeviceLifecycle Contracts for tests.""" + contract_ct = ContentType.objects.get_for_model(ContractLCM) + not_supported = Status.objects.create( + name="End of Support", color="f44336", description="Contract no longer supported." + ) + not_supported.content_types.set([contract_ct]) + supported = Status.objects.create(name="Active Support", color="4caf50", description="Active Contract.") + supported.content_types.set([contract_ct]) + hero_provider = ProviderLCM.objects.create( + name="Skyrim Merchant", + description="Whiteruns Merchant", + country="USA", + ) + villain_provider = ProviderLCM.objects.create( + name="Skyrim Villain Merchant", + description="Whiteruns Villain Merchant", + country="USA", + ) + + return ( + ContractLCM.objects.create( + provider=hero_provider, + name="Hero Discounts 1", + number="1234567890", + start="2022-01-01", + end="2022-12-31", + cost=5000.00, + support_level="Silver", + currency="USD", + contract_type="Hardware", + status=not_supported, + ), + ContractLCM.objects.create( + provider=hero_provider, + name="Hero Discounts 2", + number="1234567890", + start="2022-01-01", + end="2022-12-31", + cost=10000.00, + support_level="Gold", + currency="USD", + contract_type="Hardware", + status=not_supported, + ), + ContractLCM.objects.create( + provider=villain_provider, + name="Villain Discounts 1", + number="1234567890", + start="2021-01-01", + end="2060-12-31", + cost=5000.00, + support_level="Silver", + currency="USD", + contract_type="Hardware", + status=supported, + ), + ContractLCM.objects.create( + provider=villain_provider, + name="Villain Discounts 2", + number="1234567890", + start="2021-01-01", + end="2060-12-31", + cost=10000.00, + support_level="Gold", + currency="USD", + contract_type="Hardware", + status=supported, + ), + ) diff --git a/nautobot_device_lifecycle_mgmt/tests/test_filters.py b/nautobot_device_lifecycle_mgmt/tests/test_filters.py index bc2ab157..6f7cffec 100644 --- a/nautobot_device_lifecycle_mgmt/tests/test_filters.py +++ b/nautobot_device_lifecycle_mgmt/tests/test_filters.py @@ -11,6 +11,7 @@ from nautobot_device_lifecycle_mgmt.choices import CVESeverityChoices from nautobot_device_lifecycle_mgmt.filters import ( + ContractLCMFilterSet, CVELCMFilterSet, DeviceSoftwareValidationResultFilterSet, HardwareLCMFilterSet, @@ -22,6 +23,7 @@ ) from nautobot_device_lifecycle_mgmt.models import ( CVELCM, + ContractLCM, DeviceSoftwareValidationResult, HardwareLCM, InventoryItemSoftwareValidationResult, @@ -31,7 +33,7 @@ VulnerabilityLCM, ) -from .conftest import create_cves, create_devices, create_inventory_items, create_softwares +from .conftest import create_contracts, create_cves, create_devices, create_inventory_items, create_softwares class HardwareLCMTestCase(TestCase): @@ -811,3 +813,90 @@ def test_device_types(self): """Test device_types filter.""" params = {"device_types": [self.devicetype_2.model]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + +class ContractLCMFilterSetTest(TestCase): + """Tests for ContractLCMFilterSet.""" + + queryset = ContractLCM.objects.all() + filterset = ContractLCMFilterSet + + def setUp(self): + self.ch1, self.ch2, self.cv1, self.cv2 = create_contracts() + + # Test Q + def test_q_multiple_records(self): + """Test q filter to find multiple records based on name.""" + params = {"q": ""} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + def test_q_name(self): + """Test q filter to find single record based on name.""" + params = {"q": "Hero Discounts"} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_q_cost(self): + """Test q filter to find multiple records based on name.""" + params = {"q": "5000"} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_q_support_level(self): + """Test q filter to find multiple records based on name.""" + params = {"q": "Gold"} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_q_contract_type(self): + """Test q filter to find multiple records based on name.""" + params = {"q": "Hardware"} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + # Test Queries + def test_provider(self): + """Test provider filter to find two records.""" + params = {"provider": [self.ch1.provider]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {"provider": [self.cv1.provider]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_contract_expiration(self): + """Test expired filter to find two records based on year.""" + params = {"expired": True} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {"expired": False} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_contract_start_gte(self): + """Test start date range filter gte to find records based on year.""" + params = {"start__gte": "2022-01-01"} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {"start__gte": "2021-01-01"} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {"start__gte": "2060-12-31"} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0) + + def test_contract_start_lte(self): + """Test start date range filter lte to find records based on year.""" + params = {"start__lte": "2022-01-01"} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {"start__lte": "2021-01-01"} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {"start__lte": "2020-01-01"} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0) + + def test_contract_end_gte(self): + """Test end date range filter gte to find records based on year.""" + params = {"end__gte": "2022-12-31"} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {"end__gte": "2060-12-31"} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {"end__gte": "2070-12-31"} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0) + + def test_contract_end_lte(self): + """Test end date range filter lte to find records based on year.""" + params = {"end__lte": "2060-12-31"} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {"end__lte": "2022-12-31"} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {"end__lte": "2020-12-31"} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0) diff --git a/nautobot_device_lifecycle_mgmt/tests/test_forms.py b/nautobot_device_lifecycle_mgmt/tests/test_forms.py index 192edde8..37f1cfcc 100644 --- a/nautobot_device_lifecycle_mgmt/tests/test_forms.py +++ b/nautobot_device_lifecycle_mgmt/tests/test_forms.py @@ -6,13 +6,14 @@ from nautobot.extras.models import Role, Status, Tag from nautobot_device_lifecycle_mgmt.forms import ( + ContractLCMForm, CVELCMForm, HardwareLCMForm, SoftwareImageLCMForm, SoftwareLCMForm, ValidatedSoftwareLCMForm, ) -from nautobot_device_lifecycle_mgmt.models import CVELCM, SoftwareImageLCM, SoftwareLCM +from nautobot_device_lifecycle_mgmt.models import CVELCM, ContractLCM, ProviderLCM, SoftwareImageLCM, SoftwareLCM class HardwareLCMFormTest(TestCase): @@ -601,3 +602,98 @@ def test_validation_error_download_url(self): self.assertFalse(form.is_valid()) self.assertIn("download_url", form.errors) self.assertIn("Enter a valid URL.", form.errors["download_url"]) + + +class ContractLCMFormTest(TestCase): + """Test class for ContractLCMFormTest forms.""" + + contract_form_class = ContractLCMForm + + def setUp(self): + # Build Contract Status + self.contract_ct = ContentType.objects.get_for_model(ContractLCM) + self.contract_status = Status.objects.create( + name="End-of-Support", color="4caf50", description="Contract no longer supported." + ) + self.contract_status.content_types.set([self.contract_ct]) + # Build New Vendor/Provider for Contract + self.provider = ProviderLCM.objects.create( + name="Skyrim Merchant", + description="Whiteruns Merchant", + country="USA", + ) + # Build test tag + self.test_tag, _ = Tag.objects.get_or_create(name="Dragonborn") + # Build Test Device for Contract + location_type_location_a, _ = LocationType.objects.get_or_create(name="City") + location_type_location_a.content_types.add( + ContentType.objects.get_for_model(Device), + ) + location_status = Status.objects.get_for_model(Location).first() + device_status = Status.objects.get_for_model(Device).first() + location1, _ = Location.objects.get_or_create( + name="Whiterun", location_type=location_type_location_a, status=location_status + ) + manufacturer_cisco, _ = Manufacturer.objects.get_or_create(name="Septim Empire") + self.devicetype, _ = DeviceType.objects.get_or_create(manufacturer=manufacturer_cisco, model="Sword") + devicerole, _ = Role.objects.get_or_create(name="Blade", defaults={"color": "ff0000"}) + self.test_device, _ = Device.objects.get_or_create( + device_type=self.devicetype, role=devicerole, name="Dragonbane", location=location1, status=device_status + ) + + # Verify setUp is correct. + def test_provider_creation(self): + """Test that a provider is correctly created.""" + self.assertEqual(ProviderLCM.objects.count(), 1) + self.assertEqual(ProviderLCM.objects.first().name, "Skyrim Merchant") + + def test_device_creation(self): + """Test that a device is correctly created.""" + self.assertEqual(Device.objects.count(), 1) + self.assertEqual(Device.objects.first().name, "Dragonbane") + self.assertEqual(Device.objects.first().device_type.manufacturer.name, "Septim Empire") + self.assertEqual(Device.objects.first().location.name, "Whiterun") + + def test_status_assignment_to_contract(self): + """Test that the contract status is assigned correctly to the contract content type.""" + self.contract_ct.statuses.set([self.contract_status]) + self.assertEqual(self.contract_ct.statuses.all()[0].id, self.contract_status.id) + + # Actual Tests Begin + def test_form_initiation(self): + form = self.contract_form_class(data={}) + self.assertIsNotNone(form) + self.assertIsInstance(form, ContractLCMForm) + + def test_form_all_fields(self): + form = self.contract_form_class( + data={ + "provider": self.provider, + "name": "Hero Discount", + "number": "111-111-1111", + "start": "2018-03-05", + "end": "2019-03-04", + "cost": "1000.00", + "support_level": "high support", + "currency": "USD", + "contract_type": "Hardware", + "devices": [self.test_device], + "status": self.contract_status, + "comments": "Hero Discount for saving the city.", + "tags": [self.test_tag], + } + ) + self.assertTrue(form.is_valid()) + self.assertTrue(form.save()) + + def test_required_fields_missing(self): + form = self.contract_form_class(data={"cost": "0"}) + self.assertFalse(form.is_valid()) + self.assertDictEqual( + { + "provider": ["This field is required."], + "name": ["This field is required."], + "contract_type": ["This field is required."], + }, + form.errors, + ) diff --git a/nautobot_device_lifecycle_mgmt/tests/test_model.py b/nautobot_device_lifecycle_mgmt/tests/test_model.py index 3b75b1b6..47c2bcf7 100644 --- a/nautobot_device_lifecycle_mgmt/tests/test_model.py +++ b/nautobot_device_lifecycle_mgmt/tests/test_model.py @@ -681,3 +681,88 @@ def test_provider_assignment(self): self.assertEqual(cisco_contract.currency, "USD") self.assertEqual(cisco_contract.contract_type, "Hardware") self.assertEqual(cisco_contract.comments, "Cisco gave us discount") + + +class ContractLCMTest(TestCase): + """Tests for the ContractLCMTest models.""" + + def setUp(self): + # Create a provider to associate with the contract + self.provider = ProviderLCM.objects.create( + name="Test Vendor", + description="Test Vendor", + country="USA", + ) + self.content_type_contract = ContentType.objects.get( + app_label="nautobot_device_lifecycle_mgmt", model="contractlcm" + ) + self.active_status = Status.objects.get(name="Active") + self.active_status.content_types.add(self.content_type_contract) + + def test_contract_creation(self): + """Test that a new contract can be created.""" + contract = ContractLCM.objects.create( + name="Test Contract", + number="1234567890", + start="2022-01-01", + end="2025-12-31", + cost=10000.00, + support_level="Gold", + currency="USD", + contract_type="Hardware", + ) + contract.save() + + # Assert that the new contract has been saved to the database + self.assertEqual(ContractLCM.objects.count(), 1) + + def test_contract_fields(self): + """Test that all required fields are present.""" + contract = ContractLCM.objects.create( + name="Test Contract", + number="1234567890", + start="2022-01-01", + end="2025-12-31", + cost=10000.00, + support_level="Gold", + currency="USD", + contract_type="Hardware", + ) + contract.save() + + # Assert that all required fields are present + self.assertTrue(contract.name) + self.assertTrue(contract.number) + self.assertTrue(contract.start) + self.assertTrue(contract.end) + self.assertTrue(contract.cost) + self.assertTrue(contract.support_level) + self.assertTrue(contract.currency) + self.assertTrue(contract.contract_type) + + def test_contract_status(self): + """Test that the contract's status is properly set.""" + # Create a new contract + contract = ContractLCM.objects.create( + name="Test Contract", + number="1234567890", + start="2022-01-01", + end="2025-12-31", + cost=10000.00, + support_level="Gold", + currency="USD", + contract_type="Hardware", + ) + + # Save the contract with no status set + contract.save() + + # Assert that the contract's status is not set + self.assertIsNone(contract.status) + + # Set a new status for the contract and save it again + contract.status = self.active_status + contract.save() + + # Assert that the contract's status has been updated correctly + self.assertEqual(contract.status.name, "Active") From 04b5456f0b7d1f0f329440c9b6dac97863e35056 Mon Sep 17 00:00:00 2001 From: kvncampos Date: Fri, 15 Nov 2024 16:10:32 -0600 Subject: [PATCH 2/5] invoke tests/migrations/re-ran --- .../migrations/0023_contractlcm_status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautobot_device_lifecycle_mgmt/migrations/0023_contractlcm_status.py b/nautobot_device_lifecycle_mgmt/migrations/0023_contractlcm_status.py index 0baa9b7c..a8ff0147 100644 --- a/nautobot_device_lifecycle_mgmt/migrations/0023_contractlcm_status.py +++ b/nautobot_device_lifecycle_mgmt/migrations/0023_contractlcm_status.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.16 on 2024-11-13 19:52 +# Generated by Django 4.2.16 on 2024-11-15 22:08 import django.db.models.deletion import nautobot.extras.models.statuses From 180325efc8652869e26e2f218eb823ec377fd888 Mon Sep 17 00:00:00 2001 From: kvncampos Date: Tue, 19 Nov 2024 12:00:03 -0600 Subject: [PATCH 3/5] changelog updated --- changes/337.added | 1 + changes/337.enhancement | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changes/337.added delete mode 100644 changes/337.enhancement diff --git a/changes/337.added b/changes/337.added new file mode 100644 index 00000000..3dae435e --- /dev/null +++ b/changes/337.added @@ -0,0 +1 @@ +Added a Status Field to ContractLCM for lifecycle purposes. \ No newline at end of file diff --git a/changes/337.enhancement b/changes/337.enhancement deleted file mode 100644 index 0213aabd..00000000 --- a/changes/337.enhancement +++ /dev/null @@ -1 +0,0 @@ -Enchanced ContractLCM to include Status Field for lifecycle purposes. \ No newline at end of file From 750331c75613d36c84dda6cfc527b66b55d2fb24 Mon Sep 17 00:00:00 2001 From: kvncampos Date: Tue, 19 Nov 2024 12:49:37 -0600 Subject: [PATCH 4/5] updated migration to match ci/cd --- .../migrations/0023_contractlcm_status.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/nautobot_device_lifecycle_mgmt/migrations/0023_contractlcm_status.py b/nautobot_device_lifecycle_mgmt/migrations/0023_contractlcm_status.py index a8ff0147..65ce16b2 100644 --- a/nautobot_device_lifecycle_mgmt/migrations/0023_contractlcm_status.py +++ b/nautobot_device_lifecycle_mgmt/migrations/0023_contractlcm_status.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.16 on 2024-11-15 22:08 +# Generated by Django 3.2.25 on 2024-11-19 18:33 import django.db.models.deletion import nautobot.extras.models.statuses @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ("extras", "0114_computedfield_grouping"), + ("extras", "0098_rename_data_jobresult_result"), ("nautobot_device_lifecycle_mgmt", "0022_alter_softwareimagelcm_inventory_items_and_more"), ] @@ -16,7 +16,11 @@ class Migration(migrations.Migration): model_name="contractlcm", name="status", field=nautobot.extras.models.statuses.StatusField( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to="extras.status" + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="contracts", + to="extras.status", ), ), ] From 1dbc70f8e5a623631a445399b73c061a52733373 Mon Sep 17 00:00:00 2001 From: Campos <940778@ac44498.corpaa.aa.com> Date: Thu, 23 Jan 2025 11:31:46 -0600 Subject: [PATCH 5/5] Removed Status field from forms/filter and utilized NautobotMixins --- nautobot_device_lifecycle_mgmt/forms.py | 89 +++++++++++++++++++------ 1 file changed, 67 insertions(+), 22 deletions(-) diff --git a/nautobot_device_lifecycle_mgmt/forms.py b/nautobot_device_lifecycle_mgmt/forms.py index 54ec0e39..e23aadd1 100644 --- a/nautobot_device_lifecycle_mgmt/forms.py +++ b/nautobot_device_lifecycle_mgmt/forms.py @@ -17,8 +17,20 @@ add_blank_choice, ) from nautobot.core.forms.constants import BOOLEAN_WITH_BLANK_CHOICES -from nautobot.dcim.models import Device, DeviceType, InventoryItem, Location, Manufacturer, Platform -from nautobot.extras.forms import CustomFieldModelBulkEditFormMixin, NautobotFilterForm +from nautobot.dcim.models import ( + Device, + DeviceType, + InventoryItem, + Location, + Manufacturer, + Platform, +) +from nautobot.extras.forms import ( + CustomFieldModelBulkEditFormMixin, + NautobotFilterForm, + StatusModelBulkEditFormMixin, + StatusModelFilterFormMixin, +) from nautobot.extras.models import Role, Status, Tag from nautobot_device_lifecycle_mgmt.choices import ( @@ -75,7 +87,10 @@ class HardwareLCMForm(NautobotModelForm): device_type = DynamicModelChoiceField(queryset=DeviceType.objects.all(), required=False) inventory_item = HardwareLCMDynamicModelChoiceField( queryset=InventoryItem.objects.without_tree_fields().order_by().distinct("part_id"), - query_params={"part_id__nre": "^$", "nautobot_device_lifecycle_mgmt_distinct_part_id": "true"}, + query_params={ + "part_id__nre": "^$", + "nautobot_device_lifecycle_mgmt_distinct_part_id": "true", + }, label="Inventory Part ID", display_field="part_id", to_field_name="part_id", @@ -354,7 +369,9 @@ class ValidatedSoftwareLCMForm(NautobotModelForm): devices = DynamicModelMultipleChoiceField(queryset=Device.objects.all(), required=False) device_types = DynamicModelMultipleChoiceField(queryset=DeviceType.objects.all(), required=False) device_roles = DynamicModelMultipleChoiceField( - queryset=Role.objects.all(), query_params={"content_types": "dcim.device"}, required=False + queryset=Role.objects.all(), + query_params={"content_types": "dcim.device"}, + required=False, ) inventory_items = DynamicModelMultipleChoiceField(queryset=InventoryItem.objects.all(), required=False) @@ -383,7 +400,19 @@ def clean(self): inventory_items = self.cleaned_data.get("inventory_items") object_tags = self.cleaned_data.get("object_tags") - if sum(obj.count() for obj in (devices, device_types, device_roles, inventory_items, object_tags)) == 0: + if ( + sum( + obj.count() + for obj in ( + devices, + device_types, + device_roles, + inventory_items, + object_tags, + ) + ) + == 0 + ): msg = "You need to assign to at least one object." self.add_error(None, msg) @@ -482,7 +511,10 @@ class DeviceSoftwareValidationResultFilterForm(NautobotFilterForm): required=False, ) device_role = DynamicModelMultipleChoiceField( - queryset=Role.objects.all(), query_params={"content_types": "dcim.device"}, to_field_name="name", required=False + queryset=Role.objects.all(), + query_params={"content_types": "dcim.device"}, + to_field_name="name", + required=False, ) exclude_sw_missing = forms.BooleanField( required=False, @@ -561,7 +593,10 @@ class InventoryItemSoftwareValidationResultFilterForm(NautobotFilterForm): required=False, ) device_role = DynamicModelMultipleChoiceField( - queryset=Role.objects.all(), query_params={"content_types": "dcim.device"}, to_field_name="name", required=False + queryset=Role.objects.all(), + query_params={"content_types": "dcim.device"}, + to_field_name="name", + required=False, ) exclude_sw_missing = forms.BooleanField( required=False, @@ -624,7 +659,7 @@ def get_form_kwargs(self): return {"provider": self.request.GET.get("provider")} # pylint: disable=E1101 -class ContractLCMBulkEditForm(NautobotBulkEditForm): +class ContractLCMBulkEditForm(NautobotBulkEditForm, StatusModelBulkEditFormMixin): """Device Lifecycle Contrcts bulk edit form.""" model = ContractLCM @@ -636,17 +671,22 @@ class ContractLCMBulkEditForm(NautobotBulkEditForm): currency = forms.ChoiceField(required=False, choices=CurrencyChoices.CHOICES) contract_type = forms.ChoiceField(choices=ContractTypeChoices.CHOICES, required=False) support_level = forms.CharField(required=False) - status = DynamicModelChoiceField( - queryset=Status.objects.all(), required=False, query_params={"content_types": model._meta.label_lower} - ) class Meta: """Meta attributes for the ContractLCMBulkEditForm class.""" - nullable_fields = ["start", "end", "cost", "currency", "support_level", "contract_type", "status"] + nullable_fields = [ + "start", + "end", + "cost", + "currency", + "support_level", + "contract_type", + "status", + ] -class ContractLCMFilterForm(NautobotFilterForm): +class ContractLCMFilterForm(NautobotFilterForm, StatusModelFilterFormMixin): """Filter form to filter searches.""" model = ContractLCM @@ -656,16 +696,12 @@ class ContractLCMFilterForm(NautobotFilterForm): required=False, choices=CurrencyChoices.CHOICES, widget=StaticSelect2Multiple() ) contract_type = forms.ChoiceField( - required=False, widget=StaticSelect2, choices=add_blank_choice(ContractTypeChoices.CHOICES) + required=False, + widget=StaticSelect2, + choices=add_blank_choice(ContractTypeChoices.CHOICES), ) name = forms.CharField(required=False) tags = TagFilterField(model) - status = DynamicModelMultipleChoiceField( - queryset=Status.objects.all(), - required=False, - query_params={"content_types": model._meta.label_lower}, - to_field_name="name", - ) class Meta: """Meta attributes for the ContractLCMFilterForm class.""" @@ -793,7 +829,14 @@ class ContactLCMBulkEditForm(NautobotBulkEditForm): class Meta: """Meta attributes for the ContactLCMBulkEditForm class.""" - nullable_fields = ["address", "phone", "email", "comments", "priority", "contract"] + nullable_fields = [ + "address", + "phone", + "email", + "comments", + "priority", + "contract", + ] class ContactLCMFilterForm(NautobotFilterForm): @@ -850,7 +893,9 @@ class CVELCMBulkEditForm(NautobotBulkEditForm, CustomFieldModelBulkEditFormMixin comments = forms.CharField(required=False) tags = DynamicModelMultipleChoiceField(queryset=Tag.objects.all(), required=False) status = DynamicModelChoiceField( - queryset=Status.objects.all(), required=False, query_params={"content_types": model._meta.label_lower} + queryset=Status.objects.all(), + required=False, + query_params={"content_types": model._meta.label_lower}, ) class Meta: