7699 refactor mixins

This commit is contained in:
Arthur Hanson 2024-10-30 11:53:51 -07:00
parent 0c7710ddb6
commit 5c022f0ae2
9 changed files with 94 additions and 83 deletions

View File

@ -123,3 +123,8 @@ COMPATIBLE_TERMINATION_TYPES = {
'powerport': ['poweroutlet', 'powerfeed'], 'powerport': ['poweroutlet', 'powerfeed'],
'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'], 'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
} }
# models values for ContentTypes which may be Cluster scope types
LOCATION_SCOPE_TYPES = (
'region', 'sitegroup', 'site', 'location',
)

View File

@ -2,15 +2,36 @@ from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _ 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 import get_field_value
from utilities.forms.fields import (
ContentTypeChoiceField, CSVContentTypeField, DynamicModelChoiceField,
)
from utilities.templatetags.builtins.filters import bettertitle from utilities.templatetags.builtins.filters import bettertitle
from utilities.forms.widgets import HTMXSelect
__all__ = ( __all__ = (
'ScopedBulkEditForm',
'ScopedForm', 'ScopedForm',
) )
class ScopedForm(forms.Form): 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): def __init__(self, *args, **kwargs):
instance = kwargs.get('instance') instance = kwargs.get('instance')
@ -43,3 +64,40 @@ class ScopedForm(forms.Form):
if self.instance and scope_type_id != self.instance.scope_type_id: if self.instance and scope_type_id != self.instance.scope_type_id:
self.initial['scope'] = None 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)')
)

View File

@ -1,5 +1,7 @@
from django.apps import apps from django.apps import apps
from django.contrib.contenttypes.fields import GenericForeignKey
from django.db import models from django.db import models
from dcim.constants import LOCATION_SCOPE_TYPES
__all__ = ( __all__ = (
'CachedScopeMixin', 'CachedScopeMixin',
@ -33,8 +35,25 @@ class RenderConfigMixin(models.Model):
class CachedScopeMixin(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( _location = models.ForeignKey(
to='dcim.Location', to='dcim.Location',
on_delete=models.CASCADE, on_delete=models.CASCADE,

View File

@ -1,3 +1,4 @@
from dcim.constants import LOCATION_SCOPE_TYPES
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from drf_spectacular.utils import extend_schema_field from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers from rest_framework import serializers
@ -5,7 +6,6 @@ from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountF
from netbox.api.serializers import NetBoxModelSerializer from netbox.api.serializers import NetBoxModelSerializer
from tenancy.api.serializers_.tenants import TenantSerializer from tenancy.api.serializers_.tenants import TenantSerializer
from virtualization.choices import * from virtualization.choices import *
from virtualization.constants import CLUSTER_SCOPE_TYPES
from virtualization.models import Cluster, ClusterGroup, ClusterType from virtualization.models import Cluster, ClusterGroup, ClusterType
from utilities.api import get_serializer_for_model from utilities.api import get_serializer_for_model
@ -51,7 +51,7 @@ class ClusterSerializer(NetBoxModelSerializer):
tenant = TenantSerializer(nested=True, required=False, allow_null=True) tenant = TenantSerializer(nested=True, required=False, allow_null=True)
scope_type = ContentTypeField( scope_type = ContentTypeField(
queryset=ContentType.objects.filter( queryset=ContentType.objects.filter(
model__in=CLUSTER_SCOPE_TYPES model__in=LOCATION_SCOPE_TYPES
), ),
allow_null=True, allow_null=True,
required=False, required=False,

View File

@ -1,4 +0,0 @@
# models values for ContentTypes which may be Cluster scope types
CLUSTER_SCOPE_TYPES = (
'region', 'sitegroup', 'site', 'location',
)

View File

@ -1,20 +1,19 @@
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from dcim.choices import InterfaceModeChoices from dcim.choices import InterfaceModeChoices
from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN 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 dcim.models import Device, DeviceRole, Platform, Site
from extras.models import ConfigTemplate from extras.models import ConfigTemplate
from ipam.models import VLAN, VLANGroup, VRF from ipam.models import VLAN, VLANGroup, VRF
from netbox.forms import NetBoxModelBulkEditForm from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import BulkRenameForm, add_blank_choice, get_field_value from utilities.forms import BulkRenameForm, add_blank_choice
from utilities.forms.fields import CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.rendering import FieldSet 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.choices import *
from virtualization.constants import CLUSTER_SCOPE_TYPES
from virtualization.models import * from virtualization.models import *
__all__ = ( __all__ = (
@ -57,7 +56,7 @@ class ClusterGroupBulkEditForm(NetBoxModelBulkEditForm):
nullable_fields = ('description',) nullable_fields = ('description',)
class ClusterBulkEditForm(NetBoxModelBulkEditForm): class ClusterBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm):
type = DynamicModelChoiceField( type = DynamicModelChoiceField(
label=_('Type'), label=_('Type'),
queryset=ClusterType.objects.all(), queryset=ClusterType.objects.all(),
@ -79,19 +78,6 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm):
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False 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( description = forms.CharField(
label=_('Description'), label=_('Description'),
max_length=200, 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): class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
status = forms.ChoiceField( status = forms.ChoiceField(
label=_('Status'), label=_('Status'),

View File

@ -1,15 +1,14 @@
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from dcim.choices import InterfaceModeChoices from dcim.choices import InterfaceModeChoices
from dcim.forms.mixins import ScopedImportForm
from dcim.models import Device, DeviceRole, Platform, Site from dcim.models import Device, DeviceRole, Platform, Site
from extras.models import ConfigTemplate from extras.models import ConfigTemplate
from ipam.models import VRF from ipam.models import VRF
from netbox.forms import NetBoxModelImportForm from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant 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.choices import *
from virtualization.constants import CLUSTER_SCOPE_TYPES
from virtualization.models import * from virtualization.models import *
__all__ = ( __all__ = (
@ -38,7 +37,7 @@ class ClusterGroupImportForm(NetBoxModelImportForm):
fields = ('name', 'slug', 'description', 'tags') fields = ('name', 'slug', 'description', 'tags')
class ClusterImportForm(NetBoxModelImportForm): class ClusterImportForm(ScopedImportForm, NetBoxModelImportForm):
type = CSVModelChoiceField( type = CSVModelChoiceField(
label=_('Type'), label=_('Type'),
queryset=ClusterType.objects.all(), queryset=ClusterType.objects.all(),
@ -57,11 +56,6 @@ class ClusterImportForm(NetBoxModelImportForm):
choices=ClusterStatusChoices, choices=ClusterStatusChoices,
help_text=_('Operational status') 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( site = CSVModelChoiceField(
label=_('Site'), label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),

View File

@ -14,10 +14,8 @@ from utilities.forms import ConfirmationForm
from utilities.forms.fields import ( from utilities.forms.fields import (
CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField,
) )
from utilities.forms.fields import ContentTypeChoiceField
from utilities.forms.rendering import FieldSet from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import HTMXSelect from utilities.forms.widgets import HTMXSelect
from virtualization.constants import CLUSTER_SCOPE_TYPES
from virtualization.models import * from virtualization.models import *
__all__ = ( __all__ = (
@ -70,19 +68,6 @@ class ClusterForm(TenancyForm, ScopedForm, NetBoxModelForm):
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),
required=False 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() comments = CommentField()
fieldsets = ( fieldsets = (

View File

@ -1,5 +1,5 @@
from django.apps import apps 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.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ 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 import OrganizationalModel, PrimaryModel
from netbox.models.features import ContactsMixin from netbox.models.features import ContactsMixin
from virtualization.choices import * from virtualization.choices import *
from virtualization.constants import CLUSTER_SCOPE_TYPES
__all__ = ( __all__ = (
'Cluster', 'Cluster',
@ -79,22 +78,6 @@ class Cluster(ContactsMixin, CachedScopeMixin, PrimaryModel):
blank=True, blank=True,
null=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 # Generic relations
vlan_groups = GenericRelation( vlan_groups = GenericRelation(