1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Introduce ContentTypeChoiceField

This commit is contained in:
Jeremy Stretch
2021-04-02 10:17:21 -04:00
parent a86178f19b
commit 73e9842877
5 changed files with 120 additions and 84 deletions

View File

@ -59,6 +59,11 @@ IPADDRESS_ROLES_NONUNIQUE = (
VLAN_VID_MIN = 1 VLAN_VID_MIN = 1
VLAN_VID_MAX = 4094 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 # Services

View File

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

View File

@ -23,7 +23,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='vlangroup', model_name='vlangroup',
name='scope_type', 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( migrations.AlterModelOptions(
name='vlangroup', name='vlangroup',

View File

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

View File

@ -20,6 +20,7 @@ from .utils import expand_alphanumeric_pattern, expand_ipaddress_pattern
__all__ = ( __all__ = (
'CommentField', 'CommentField',
'ContentTypeChoiceField',
'CSVChoiceField', 'CSVChoiceField',
'CSVContentTypeField', 'CSVContentTypeField',
'CSVDataField', '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): class CSVDataField(forms.CharField):
""" """
A CharField (rendered as a Textarea) which accepts CSV-formatted data. It returns data as a two-tuple: The first 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') raise forms.ValidationError(f'Invalid object type')
#
# Expansion fields
#
class ExpandableNameField(forms.CharField): class ExpandableNameField(forms.CharField):
""" """
A field which allows for numeric range expansion A field which allows for numeric range expansion
@ -212,56 +310,9 @@ class ExpandableIPAddressField(forms.CharField):
return [value] return [value]
class CommentField(forms.CharField): #
""" # Dynamic fields
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 DynamicModelChoiceMixin: class DynamicModelChoiceMixin:
""" """
@ -373,29 +424,3 @@ class DynamicModelMultipleChoiceField(DynamicModelChoiceMixin, forms.ModelMultip
""" """
filter = django_filters.ModelMultipleChoiceFilter filter = django_filters.ModelMultipleChoiceFilter
widget = widgets.APISelectMultiple 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)