فهرست منبع

Introduce ContentTypeChoiceField

Jeremy Stretch 4 سال پیش
والد
کامیت
73e9842877
5فایلهای تغییر یافته به همراه120 افزوده شده و 84 حذف شده
  1. 5 0
      netbox/ipam/constants.py
  2. 12 4
      netbox/ipam/forms.py
  3. 1 1
      netbox/ipam/migrations/0045_vlangroup_scope.py
  4. 1 3
      netbox/ipam/models/vlans.py
  5. 101 76
      netbox/utilities/forms/fields.py

+ 5 - 0
netbox/ipam/constants.py

@@ -59,6 +59,11 @@ IPADDRESS_ROLES_NONUNIQUE = (
 VLAN_VID_MIN = 1
 VLAN_VID_MAX = 4094
 
+# models values for ContentTypes which may be VLANGroup scope types
+VLANGROUP_SCOPE_TYPES = (
+    'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster',
+)
+
 
 #
 # Services

+ 12 - 4
netbox/ipam/forms.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.contrib.contenttypes.models import ContentType
 from django.utils.translation import gettext as _
 
 from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup
@@ -9,9 +10,10 @@ from extras.models import Tag
 from tenancy.forms import TenancyFilterForm, TenancyForm
 from tenancy.models import Tenant
 from utilities.forms import (
-    add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, CSVChoiceField, CSVModelChoiceField, DatePicker,
-    DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField, NumericArrayField,
-    ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
+    add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, ContentTypeChoiceField, CSVChoiceField,
+    CSVModelChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField,
+    NumericArrayField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
+    BOOLEAN_WITH_BLANK_CHOICES,
 )
 from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
 from .choices import *
@@ -1137,6 +1139,10 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
 #
 
 class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
+    scope_type = ContentTypeChoiceField(
+        queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
+        required=False
+    )
     region = DynamicModelChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -1223,9 +1229,11 @@ class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
         super().clean()
 
         # Assign scope based on scope_type
-        if self.cleaned_data['scope_type']:
+        if self.cleaned_data.get('scope_type'):
             scope_field = self.cleaned_data['scope_type'].model
             self.instance.scope = self.cleaned_data.get(scope_field)
+        else:
+            self.instance.scope_id = None
 
 
 class VLANGroupCSVForm(CustomFieldModelCSVForm):

+ 1 - 1
netbox/ipam/migrations/0045_vlangroup_scope.py

@@ -23,7 +23,7 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='vlangroup',
             name='scope_type',
-            field=models.ForeignKey(blank=True, limit_choices_to=models.Q(model__in=['region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster']), null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'),
+            field=models.ForeignKey(blank=True, limit_choices_to=models.Q(model__in=('region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')), null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'),
         ),
         migrations.AlterModelOptions(
             name='vlangroup',

+ 1 - 3
netbox/ipam/models/vlans.py

@@ -35,9 +35,7 @@ class VLANGroup(OrganizationalModel):
     scope_type = models.ForeignKey(
         to=ContentType,
         on_delete=models.CASCADE,
-        limit_choices_to=Q(
-            model__in=['region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster']
-        ),
+        limit_choices_to=Q(model__in=VLANGROUP_SCOPE_TYPES),
         blank=True,
         null=True
     )

+ 101 - 76
netbox/utilities/forms/fields.py

@@ -20,6 +20,7 @@ from .utils import expand_alphanumeric_pattern, expand_ipaddress_pattern
 
 __all__ = (
     'CommentField',
+    'ContentTypeChoiceField',
     'CSVChoiceField',
     'CSVContentTypeField',
     'CSVDataField',
@@ -36,6 +37,99 @@ __all__ = (
 )
 
 
+class CommentField(forms.CharField):
+    """
+    A textarea with support for Markdown rendering. Exists mostly just to add a standard help_text.
+    """
+    widget = forms.Textarea
+    default_label = ''
+    # TODO: Port Markdown cheat sheet to internal documentation
+    default_helptext = '<i class="mdi mdi-information-outline"></i> '\
+                       '<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank" tabindex="-1">'\
+                       'Markdown</a> syntax is supported'
+
+    def __init__(self, *args, **kwargs):
+        required = kwargs.pop('required', False)
+        label = kwargs.pop('label', self.default_label)
+        help_text = kwargs.pop('help_text', self.default_helptext)
+        super().__init__(required=required, label=label, help_text=help_text, *args, **kwargs)
+
+
+class SlugField(forms.SlugField):
+    """
+    Extend the built-in SlugField to automatically populate from a field called `name` unless otherwise specified.
+    """
+    def __init__(self, slug_source='name', *args, **kwargs):
+        label = kwargs.pop('label', "Slug")
+        help_text = kwargs.pop('help_text', "URL-friendly unique shorthand")
+        widget = kwargs.pop('widget', widgets.SlugWidget)
+        super().__init__(label=label, help_text=help_text, widget=widget, *args, **kwargs)
+        self.widget.attrs['slug-source'] = slug_source
+
+
+class TagFilterField(forms.MultipleChoiceField):
+    """
+    A filter field for the tags of a model. Only the tags used by a model are displayed.
+
+    :param model: The model of the filter
+    """
+    widget = widgets.StaticSelect2Multiple
+
+    def __init__(self, model, *args, **kwargs):
+        def get_choices():
+            tags = model.tags.annotate(
+                count=Count('extras_taggeditem_items')
+            ).order_by('name')
+            return [
+                (str(tag.slug), '{} ({})'.format(tag.name, tag.count)) for tag in tags
+            ]
+
+        # Choices are fetched each time the form is initialized
+        super().__init__(label='Tags', choices=get_choices, required=False, *args, **kwargs)
+
+
+class LaxURLField(forms.URLField):
+    """
+    Modifies Django's built-in URLField to remove the requirement for fully-qualified domain names
+    (e.g. http://myserver/ is valid)
+    """
+    default_validators = [EnhancedURLValidator()]
+
+
+class JSONField(_JSONField):
+    """
+    Custom wrapper around Django's built-in JSONField to avoid presenting "null" as the default text.
+    """
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        if not self.help_text:
+            self.help_text = 'Enter context data in <a href="https://json.org/">JSON</a> format.'
+            self.widget.attrs['placeholder'] = ''
+
+    def prepare_value(self, value):
+        if isinstance(value, InvalidJSONInput):
+            return value
+        if value is None:
+            return ''
+        return json.dumps(value, sort_keys=True, indent=4)
+
+
+class ContentTypeChoiceField(forms.ModelChoiceField):
+
+    def __init__(self, queryset, *args, **kwargs):
+        # Order ContentTypes by app_label
+        queryset = queryset.order_by('app_label', 'model')
+        super().__init__(queryset, *args, **kwargs)
+
+    def label_from_instance(self, obj):
+        meta = obj.model_class()._meta
+        return f'{meta.app_config.verbose_name} > {meta.verbose_name}'
+
+
+#
+# CSV fields
+#
+
 class CSVDataField(forms.CharField):
     """
     A CharField (rendered as a Textarea) which accepts CSV-formatted data. It returns data as a two-tuple: The first
@@ -167,6 +261,10 @@ class CSVContentTypeField(CSVModelChoiceField):
             raise forms.ValidationError(f'Invalid object type')
 
 
+#
+# Expansion fields
+#
+
 class ExpandableNameField(forms.CharField):
     """
     A field which allows for numeric range expansion
@@ -212,56 +310,9 @@ class ExpandableIPAddressField(forms.CharField):
         return [value]
 
 
-class CommentField(forms.CharField):
-    """
-    A textarea with support for Markdown rendering. Exists mostly just to add a standard help_text.
-    """
-    widget = forms.Textarea
-    default_label = ''
-    # TODO: Port Markdown cheat sheet to internal documentation
-    default_helptext = '<i class="mdi mdi-information-outline"></i> '\
-                       '<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank" tabindex="-1">'\
-                       'Markdown</a> syntax is supported'
-
-    def __init__(self, *args, **kwargs):
-        required = kwargs.pop('required', False)
-        label = kwargs.pop('label', self.default_label)
-        help_text = kwargs.pop('help_text', self.default_helptext)
-        super().__init__(required=required, label=label, help_text=help_text, *args, **kwargs)
-
-
-class SlugField(forms.SlugField):
-    """
-    Extend the built-in SlugField to automatically populate from a field called `name` unless otherwise specified.
-    """
-    def __init__(self, slug_source='name', *args, **kwargs):
-        label = kwargs.pop('label', "Slug")
-        help_text = kwargs.pop('help_text', "URL-friendly unique shorthand")
-        widget = kwargs.pop('widget', widgets.SlugWidget)
-        super().__init__(label=label, help_text=help_text, widget=widget, *args, **kwargs)
-        self.widget.attrs['slug-source'] = slug_source
-
-
-class TagFilterField(forms.MultipleChoiceField):
-    """
-    A filter field for the tags of a model. Only the tags used by a model are displayed.
-
-    :param model: The model of the filter
-    """
-    widget = widgets.StaticSelect2Multiple
-
-    def __init__(self, model, *args, **kwargs):
-        def get_choices():
-            tags = model.tags.annotate(
-                count=Count('extras_taggeditem_items')
-            ).order_by('name')
-            return [
-                (str(tag.slug), '{} ({})'.format(tag.name, tag.count)) for tag in tags
-            ]
-
-        # Choices are fetched each time the form is initialized
-        super().__init__(label='Tags', choices=get_choices, required=False, *args, **kwargs)
-
+#
+# Dynamic fields
+#
 
 class DynamicModelChoiceMixin:
     """
@@ -373,29 +424,3 @@ class DynamicModelMultipleChoiceField(DynamicModelChoiceMixin, forms.ModelMultip
     """
     filter = django_filters.ModelMultipleChoiceFilter
     widget = widgets.APISelectMultiple
-
-
-class LaxURLField(forms.URLField):
-    """
-    Modifies Django's built-in URLField to remove the requirement for fully-qualified domain names
-    (e.g. http://myserver/ is valid)
-    """
-    default_validators = [EnhancedURLValidator()]
-
-
-class JSONField(_JSONField):
-    """
-    Custom wrapper around Django's built-in JSONField to avoid presenting "null" as the default text.
-    """
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        if not self.help_text:
-            self.help_text = 'Enter context data in <a href="https://json.org/">JSON</a> format.'
-            self.widget.attrs['placeholder'] = ''
-
-    def prepare_value(self, value):
-        if isinstance(value, InvalidJSONInput):
-            return value
-        if value is None:
-            return ''
-        return json.dumps(value, sort_keys=True, indent=4)