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:
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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',
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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)
|
|
||||||
|
Reference in New Issue
Block a user