From 009580a8fee751b27fc0f2f69fe113917eb90b94 Mon Sep 17 00:00:00 2001 From: Anton M Date: Mon, 14 Sep 2026 00:32:06 +0200 Subject: [PATCH] add region to selector --- validity/api/serializers.py | 7 ++++++- validity/forms/bulk_import.py | 9 ++++++++- validity/forms/general.py | 4 +++- .../migrations/0014_add_region_to_selector.py | 18 ++++++++++++++++++ validity/models/selector.py | 5 ++++- validity/tables.py | 2 ++ .../templates/validity/complianceselector.html | 5 +++++ validity/tests/factories.py | 10 +++++++++- validity/tests/test_managers.py | 1 + validity/tests/test_models/test_selector.py | 18 ++++++++++++++++++ 10 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 validity/migrations/0014_add_region_to_selector.py diff --git a/validity/api/serializers.py b/validity/api/serializers.py index b28fd6c..22b1939 100644 --- a/validity/api/serializers.py +++ b/validity/api/serializers.py @@ -9,9 +9,10 @@ LocationSerializer, ManufacturerSerializer, PlatformSerializer, + RegionSerializer, SiteSerializer, ) -from dcim.models import Device, DeviceType, Location, Manufacturer, Platform, Site +from dcim.models import Device, DeviceType, Location, Manufacturer, Platform, Region, Site from django.utils import timezone from django.utils.translation import gettext_lazy as _ from extras.api.serializers import TagSerializer @@ -67,6 +68,9 @@ class ComplianceSelectorSerializer(NetBoxModelSerializer): site_filter = SerializedPKRelatedField( serializer=SiteSerializer, many=True, nested=True, required=False, queryset=Site.objects.all() ) + region_filter = SerializedPKRelatedField( + serializer=RegionSerializer, many=True, nested=True, required=False, queryset=Region.objects.all() + ) tenant_filter = SerializedPKRelatedField( serializer=TenantSerializer, many=True, nested=True, required=False, queryset=Tenant.objects.all() ) @@ -88,6 +92,7 @@ class Meta: "status_filter", "location_filter", "site_filter", + "region_filter", "tenant_filter", "dynamic_pairs", "dp_tag_prefix", diff --git a/validity/forms/bulk_import.py b/validity/forms/bulk_import.py index cbbb4fe..6b9c336 100644 --- a/validity/forms/bulk_import.py +++ b/validity/forms/bulk_import.py @@ -1,6 +1,6 @@ from core.models import DataFile, DataSource from dcim.choices import DeviceStatusChoices -from dcim.models import DeviceRole, DeviceType, Location, Manufacturer, Platform, Site +from dcim.models import DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site from django.forms import Form from django.utils.translation import gettext_lazy as _ from extras.models import Tag @@ -158,6 +158,12 @@ class ComplianceSelectorImportForm(NetBoxModelImportForm): help_text=_("Site slugs separated by commas, encased with double quotes"), required=False, ) + region_filter = CSVModelMultipleChoiceField( + queryset=Region.objects.all(), + to_field_name="slug", + help_text=_("Region slugs separated by commas, encased with double quotes"), + required=False, + ) tenant_filter = CSVModelMultipleChoiceField( queryset=Tenant.objects.all(), to_field_name="slug", @@ -182,6 +188,7 @@ class Meta: "status_filter", "location_filter", "site_filter", + "region_filter", "tenant_filter", "dynamic_pairs", "dp_tag_prefix", diff --git a/validity/forms/general.py b/validity/forms/general.py index 75add90..0074b86 100644 --- a/validity/forms/general.py +++ b/validity/forms/general.py @@ -1,6 +1,6 @@ from core.forms.mixins import SyncedDataMixin from core.models import DataSource -from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Site +from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site from django.forms import BooleanField, CharField, ChoiceField, IntegerField, Select, Textarea, ValidationError from django.utils.translation import gettext_lazy as _ from extras.forms import ScriptForm @@ -56,6 +56,7 @@ class ComplianceSelectorForm(NetBoxModelForm): platform_filter = DynamicModelMultipleChoiceField(queryset=Platform.objects.all(), required=False) location_filter = DynamicModelMultipleChoiceField(queryset=Location.objects.all(), required=False) site_filter = DynamicModelMultipleChoiceField(queryset=Site.objects.all(), required=False) + region_filter = DynamicModelMultipleChoiceField(queryset=Region.objects.all(), required=False) tenant_filter = DynamicModelMultipleChoiceField(queryset=Tenant.objects.all(), required=False) fieldsets = ( @@ -70,6 +71,7 @@ class ComplianceSelectorForm(NetBoxModelForm): "manufacturer_filter", "platform_filter", "site_filter", + "region_filter", "status_filter", "tag_filter", "tenant_filter", diff --git a/validity/migrations/0014_add_region_to_selector.py b/validity/migrations/0014_add_region_to_selector.py new file mode 100644 index 0000000..c1d9cf9 --- /dev/null +++ b/validity/migrations/0014_add_region_to_selector.py @@ -0,0 +1,18 @@ +# Generated by Django 6.1 on 2026-09-13 22:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('validity', '0013_compliancetest_enabled'), + ] + + operations = [ + migrations.AddField( + model_name='complianceselector', + name='region_filter', + field=models.ManyToManyField(blank=True, related_name='+', to='dcim.region'), + ), + ] diff --git a/validity/models/selector.py b/validity/models/selector.py index b15c4df..837a5e6 100644 --- a/validity/models/selector.py +++ b/validity/models/selector.py @@ -4,7 +4,7 @@ from typing import Generator from dcim.choices import DeviceStatusChoices -from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Site +from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import gettext_lazy as _ @@ -38,6 +38,7 @@ class ComplianceSelector(BaseModel): status_filter = models.CharField(max_length=50, choices=DeviceStatusChoices, blank=True) location_filter = models.ManyToManyField(Location, verbose_name=_("Location Filter"), blank=True, related_name="+") site_filter = models.ManyToManyField(Site, verbose_name=_("Site Filter"), blank=True, related_name="+") + region_filter = models.ManyToManyField(Region, verbose_name=_("Region Filter"), blank=True, related_name="+") tenant_filter = models.ManyToManyField(Tenant, verbose_name=_("Tenant Filter"), blank=True, related_name="+") dynamic_pairs = models.CharField( _("Dynamic Pairs"), max_length=20, choices=DynamicPairsChoices.choices, default="NO" @@ -57,6 +58,7 @@ class ComplianceSelector(BaseModel): "status_filter", "location_filter", "site_filter", + "region_filter", "tenant_filter", "dynamic_pairs", ) @@ -71,6 +73,7 @@ class ComplianceSelector(BaseModel): "status_filter": "status", "location_filter": "location", "site_filter": "site", + "region_filter": "site__region", "tenant_filter": "tenant", } diff --git a/validity/tables.py b/validity/tables.py index 0a13824..68f98b9 100644 --- a/validity/tables.py +++ b/validity/tables.py @@ -31,6 +31,7 @@ class SelectorTable(NetBoxTable): status_filter = BooleanColumn(empty_values=()) location_filter = BooleanColumn(accessor="location_filter__all") site_filter = BooleanColumn(accessor="site_filter__all") + region_filter = BooleanColumn(accessor="region_filter__all") tenant_filter = BooleanColumn(accessor="tenant_filter__all") dynamic_pairs = ChoiceFieldColumn() @@ -48,6 +49,7 @@ class Meta(NetBoxTable.Meta): "status_filter", "location_filter", "site_filter", + "region_filter", "tenant_filter", "dynamic_pairs", ) diff --git a/validity/templates/validity/complianceselector.html b/validity/templates/validity/complianceselector.html index 720c08c..5a1a668 100644 --- a/validity/templates/validity/complianceselector.html +++ b/validity/templates/validity/complianceselector.html @@ -63,6 +63,11 @@
Filters
Site Filter {{ object.site_filter.all | linkify_list }} + + + Region Filter + {{ object.region_filter.all | linkify_list }} + Status Filter {{ object | colored_choice:"status_filter" | placeholder }} diff --git a/validity/tests/factories.py b/validity/tests/factories.py index e7d34bf..41f32e7 100644 --- a/validity/tests/factories.py +++ b/validity/tests/factories.py @@ -4,7 +4,7 @@ import django import factory from core.models import Job -from dcim.models import DeviceRole, DeviceType, Location, Manufacturer, Platform, Site +from dcim.models import DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from extras.models import Tag @@ -168,6 +168,14 @@ class Meta: model = Site +class RegionFactory(DjangoModelFactory): + name = factory.Sequence(lambda n: f"region-{n}") + slug = factory.Sequence(lambda n: f"region-{n}") + + class Meta: + model = Region + + class TenantFactory(DjangoModelFactory): name = factory.Sequence(lambda n: f"tenant-{n}") slug = factory.Sequence(lambda n: f"tenant-{n}") diff --git a/validity/tests/test_managers.py b/validity/tests/test_managers.py index 5746d3c..ade7a1e 100644 --- a/validity/tests/test_managers.py +++ b/validity/tests/test_managers.py @@ -56,5 +56,6 @@ def test_prefetch_filters(monkeypatch): "platform_filter", "location_filter", "site_filter", + "region_filter", "tenant_filter", } diff --git a/validity/tests/test_models/test_selector.py b/validity/tests/test_models/test_selector.py index 9d01fe1..2d8e886 100644 --- a/validity/tests/test_models/test_selector.py +++ b/validity/tests/test_models/test_selector.py @@ -9,6 +9,7 @@ LocationFactory, ManufacturerFactory, PlatformFactory, + RegionFactory, SelectorFactory, SiteFactory, TagFactory, @@ -34,6 +35,12 @@ ("status_filter", "ACTIVE", "OR", "(AND: ('status', 'ACTIVE'))"), ("location_filter", [LocationFactory], "AND", "(AND: ('location', ))"), ("site_filter", [SiteFactory], "AND", "(AND: ('site', ))"), + ( + "region_filter", + [RegionFactory], + "AND", + "(AND: ('site__region', ))", + ), ], ) @pytest.mark.django_db(transaction=True, reset_sequences=True) @@ -49,6 +56,17 @@ def test_filter(attr, attr_value, filter_operation, expected_filter): assert str(model.filter) == expected_filter +@pytest.mark.django_db +def test_region_filter(): + reg1 = RegionFactory() + reg2 = RegionFactory() + sel = SelectorFactory() + sel.region_filter.set([reg1]) + d1 = DeviceFactory(name="d1", site__region=reg1) + DeviceFactory(name="d2", site__region=reg2) + assert list(sel.devices) == [d1] + + @pytest.mark.django_db def test_multi_filter(): model = SelectorFactory()