From 5c022f0ae2e977886b3ec486891265eb69bcf947 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Wed, 30 Oct 2024 11:53:51 -0700 Subject: [PATCH] 7699 refactor mixins --- netbox/dcim/constants.py | 5 ++ netbox/dcim/forms/mixins.py | 58 +++++++++++++++++++ netbox/dcim/models/mixins.py | 21 ++++++- .../api/serializers_/clusters.py | 4 +- netbox/virtualization/constants.py | 4 -- netbox/virtualization/forms/bulk_edit.py | 39 ++----------- netbox/virtualization/forms/bulk_import.py | 12 +--- netbox/virtualization/forms/model_forms.py | 15 ----- netbox/virtualization/models/clusters.py | 19 +----- 9 files changed, 94 insertions(+), 83 deletions(-) delete mode 100644 netbox/virtualization/constants.py diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index ba3e6464b..df7c18b32 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -123,3 +123,8 @@ COMPATIBLE_TERMINATION_TYPES = { 'powerport': ['poweroutlet', 'powerfeed'], 'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'], } + +# models values for ContentTypes which may be Cluster scope types +LOCATION_SCOPE_TYPES = ( + 'region', 'sitegroup', 'site', 'location', +) diff --git a/netbox/dcim/forms/mixins.py b/netbox/dcim/forms/mixins.py index 57aeeedeb..f15bc12d4 100644 --- a/netbox/dcim/forms/mixins.py +++ b/netbox/dcim/forms/mixins.py @@ -2,15 +2,36 @@ from django import forms from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.utils.translation import gettext_lazy as _ + +from dcim.constants import LOCATION_SCOPE_TYPES +from dcim.models import Site from utilities.forms import get_field_value +from utilities.forms.fields import ( + ContentTypeChoiceField, CSVContentTypeField, DynamicModelChoiceField, +) from utilities.templatetags.builtins.filters import bettertitle +from utilities.forms.widgets import HTMXSelect __all__ = ( + 'ScopedBulkEditForm', 'ScopedForm', ) class ScopedForm(forms.Form): + scope_type = ContentTypeChoiceField( + queryset=ContentType.objects.filter(model__in=LOCATION_SCOPE_TYPES), + widget=HTMXSelect(), + required=False, + label=_('Scope type') + ) + scope = DynamicModelChoiceField( + label=_('Scope'), + queryset=Site.objects.none(), # Initial queryset + required=False, + disabled=True, + selector=True + ) def __init__(self, *args, **kwargs): instance = kwargs.get('instance') @@ -43,3 +64,40 @@ class ScopedForm(forms.Form): if self.instance and scope_type_id != self.instance.scope_type_id: self.initial['scope'] = None + + +class ScopedBulkEditForm(forms.Form): + scope_type = ContentTypeChoiceField( + queryset=ContentType.objects.filter(model__in=LOCATION_SCOPE_TYPES), + widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}), + required=False, + label=_('Scope type') + ) + scope = DynamicModelChoiceField( + label=_('Scope'), + queryset=Site.objects.none(), # Initial queryset + required=False, + disabled=True, + selector=True + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if scope_type_id := get_field_value(self, 'scope_type'): + try: + scope_type = ContentType.objects.get(pk=scope_type_id) + model = scope_type.model_class() + self.fields['scope'].queryset = model.objects.all() + self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower + self.fields['scope'].disabled = False + self.fields['scope'].label = _(bettertitle(model._meta.verbose_name)) + except ObjectDoesNotExist: + pass + +class ScopedImportForm(forms.Form): + scope_type = CSVContentTypeField( + queryset=ContentType.objects.filter(model__in=LOCATION_SCOPE_TYPES), + required=False, + label=_('Scope type (app & model)') + ) diff --git a/netbox/dcim/models/mixins.py b/netbox/dcim/models/mixins.py index ba9050e10..9575a9e96 100644 --- a/netbox/dcim/models/mixins.py +++ b/netbox/dcim/models/mixins.py @@ -1,5 +1,7 @@ from django.apps import apps +from django.contrib.contenttypes.fields import GenericForeignKey from django.db import models +from dcim.constants import LOCATION_SCOPE_TYPES __all__ = ( 'CachedScopeMixin', @@ -33,8 +35,25 @@ class RenderConfigMixin(models.Model): class CachedScopeMixin(models.Model): """ - Cached associations for scope to enable efficient filtering - must define scope and scope_type on model + Cached associations for scope to enable efficient filtering """ + scope_type = models.ForeignKey( + to='contenttypes.ContentType', + on_delete=models.PROTECT, + limit_choices_to=models.Q(model__in=LOCATION_SCOPE_TYPES), + related_name='+', + blank=True, + null=True + ) + scope_id = models.PositiveBigIntegerField( + blank=True, + null=True + ) + scope = GenericForeignKey( + ct_field='scope_type', + fk_field='scope_id' + ) + _location = models.ForeignKey( to='dcim.Location', on_delete=models.CASCADE, diff --git a/netbox/virtualization/api/serializers_/clusters.py b/netbox/virtualization/api/serializers_/clusters.py index adc31a73c..101a5b5a3 100644 --- a/netbox/virtualization/api/serializers_/clusters.py +++ b/netbox/virtualization/api/serializers_/clusters.py @@ -1,3 +1,4 @@ +from dcim.constants import LOCATION_SCOPE_TYPES from django.contrib.contenttypes.models import ContentType from drf_spectacular.utils import extend_schema_field from rest_framework import serializers @@ -5,7 +6,6 @@ from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountF from netbox.api.serializers import NetBoxModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer from virtualization.choices import * -from virtualization.constants import CLUSTER_SCOPE_TYPES from virtualization.models import Cluster, ClusterGroup, ClusterType from utilities.api import get_serializer_for_model @@ -51,7 +51,7 @@ class ClusterSerializer(NetBoxModelSerializer): tenant = TenantSerializer(nested=True, required=False, allow_null=True) scope_type = ContentTypeField( queryset=ContentType.objects.filter( - model__in=CLUSTER_SCOPE_TYPES + model__in=LOCATION_SCOPE_TYPES ), allow_null=True, required=False, diff --git a/netbox/virtualization/constants.py b/netbox/virtualization/constants.py deleted file mode 100644 index 6154b825e..000000000 --- a/netbox/virtualization/constants.py +++ /dev/null @@ -1,4 +0,0 @@ -# models values for ContentTypes which may be Cluster scope types -CLUSTER_SCOPE_TYPES = ( - 'region', 'sitegroup', 'site', 'location', -) diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py index 19b614ac6..aaeb259b9 100644 --- a/netbox/virtualization/forms/bulk_edit.py +++ b/netbox/virtualization/forms/bulk_edit.py @@ -1,20 +1,19 @@ from django import forms -from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext_lazy as _ from dcim.choices import InterfaceModeChoices from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN +from dcim.forms.mixins import ScopedBulkEditForm from dcim.models import Device, DeviceRole, Platform, Site from extras.models import ConfigTemplate from ipam.models import VLAN, VLANGroup, VRF from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant -from utilities.forms import BulkRenameForm, add_blank_choice, get_field_value -from utilities.forms.fields import CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms import BulkRenameForm, add_blank_choice +from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField from utilities.forms.rendering import FieldSet -from utilities.forms.widgets import BulkEditNullBooleanSelect, HTMXSelect +from utilities.forms.widgets import BulkEditNullBooleanSelect from virtualization.choices import * -from virtualization.constants import CLUSTER_SCOPE_TYPES from virtualization.models import * __all__ = ( @@ -57,7 +56,7 @@ class ClusterGroupBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('description',) -class ClusterBulkEditForm(NetBoxModelBulkEditForm): +class ClusterBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm): type = DynamicModelChoiceField( label=_('Type'), queryset=ClusterType.objects.all(), @@ -79,19 +78,6 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm): queryset=Tenant.objects.all(), required=False ) - scope_type = ContentTypeChoiceField( - queryset=ContentType.objects.filter(model__in=CLUSTER_SCOPE_TYPES), - widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}), - required=False, - label=_('Scope type') - ) - scope = DynamicModelChoiceField( - label=_('Scope'), - queryset=Site.objects.none(), # Initial queryset - required=False, - disabled=True, - selector=True - ) description = forms.CharField( label=_('Description'), max_length=200, @@ -109,21 +95,6 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm): ) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - if scope_type_id := get_field_value(self, 'scope_type'): - try: - scope_type = ContentType.objects.get(pk=scope_type_id) - model = scope_type.model_class() - self.fields['scope'].queryset = model.objects.all() - self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower - self.fields['scope'].disabled = False - self.fields['scope'].label = _(bettertitle(model._meta.verbose_name)) - except ObjectDoesNotExist: - pass - - class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm): status = forms.ChoiceField( label=_('Status'), diff --git a/netbox/virtualization/forms/bulk_import.py b/netbox/virtualization/forms/bulk_import.py index f52916808..9ccdd68f7 100644 --- a/netbox/virtualization/forms/bulk_import.py +++ b/netbox/virtualization/forms/bulk_import.py @@ -1,15 +1,14 @@ -from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext_lazy as _ from dcim.choices import InterfaceModeChoices +from dcim.forms.mixins import ScopedImportForm from dcim.models import Device, DeviceRole, Platform, Site from extras.models import ConfigTemplate from ipam.models import VRF from netbox.forms import NetBoxModelImportForm from tenancy.models import Tenant -from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField +from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField from virtualization.choices import * -from virtualization.constants import CLUSTER_SCOPE_TYPES from virtualization.models import * __all__ = ( @@ -38,7 +37,7 @@ class ClusterGroupImportForm(NetBoxModelImportForm): fields = ('name', 'slug', 'description', 'tags') -class ClusterImportForm(NetBoxModelImportForm): +class ClusterImportForm(ScopedImportForm, NetBoxModelImportForm): type = CSVModelChoiceField( label=_('Type'), queryset=ClusterType.objects.all(), @@ -57,11 +56,6 @@ class ClusterImportForm(NetBoxModelImportForm): choices=ClusterStatusChoices, help_text=_('Operational status') ) - scope_type = CSVContentTypeField( - queryset=ContentType.objects.filter(model__in=CLUSTER_SCOPE_TYPES), - required=False, - label=_('Scope type (app & model)') - ) site = CSVModelChoiceField( label=_('Site'), queryset=Site.objects.all(), diff --git a/netbox/virtualization/forms/model_forms.py b/netbox/virtualization/forms/model_forms.py index 937520787..28563a821 100644 --- a/netbox/virtualization/forms/model_forms.py +++ b/netbox/virtualization/forms/model_forms.py @@ -14,10 +14,8 @@ from utilities.forms import ConfirmationForm from utilities.forms.fields import ( CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, ) -from utilities.forms.fields import ContentTypeChoiceField from utilities.forms.rendering import FieldSet from utilities.forms.widgets import HTMXSelect -from virtualization.constants import CLUSTER_SCOPE_TYPES from virtualization.models import * __all__ = ( @@ -70,19 +68,6 @@ class ClusterForm(TenancyForm, ScopedForm, NetBoxModelForm): queryset=ClusterGroup.objects.all(), required=False ) - scope_type = ContentTypeChoiceField( - queryset=ContentType.objects.filter(model__in=CLUSTER_SCOPE_TYPES), - widget=HTMXSelect(), - required=False, - label=_('Scope type') - ) - scope = DynamicModelChoiceField( - label=_('Scope'), - queryset=Site.objects.none(), # Initial queryset - required=False, - disabled=True, - selector=True - ) comments = CommentField() fieldsets = ( diff --git a/netbox/virtualization/models/clusters.py b/netbox/virtualization/models/clusters.py index ad2aaedc7..601ee7f23 100644 --- a/netbox/virtualization/models/clusters.py +++ b/netbox/virtualization/models/clusters.py @@ -1,5 +1,5 @@ from django.apps import apps -from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation +from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import gettext_lazy as _ @@ -9,7 +9,6 @@ from dcim.models.mixins import CachedScopeMixin from netbox.models import OrganizationalModel, PrimaryModel from netbox.models.features import ContactsMixin from virtualization.choices import * -from virtualization.constants import CLUSTER_SCOPE_TYPES __all__ = ( 'Cluster', @@ -79,22 +78,6 @@ class Cluster(ContactsMixin, CachedScopeMixin, PrimaryModel): blank=True, null=True ) - scope_type = models.ForeignKey( - to='contenttypes.ContentType', - on_delete=models.PROTECT, - limit_choices_to=models.Q(model__in=CLUSTER_SCOPE_TYPES), - related_name='+', - blank=True, - null=True - ) - scope_id = models.PositiveBigIntegerField( - blank=True, - null=True - ) - scope = GenericForeignKey( - ct_field='scope_type', - fk_field='scope_id' - ) # Generic relations vlan_groups = GenericRelation(