extras
forms.py
from collections import OrderedDict
from django import forms
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from taggit.forms import TagField
from dcim.models import DeviceRole, Platform, Region, Site
from tenancy.models import Tenant, TenantGroup
from utilities.forms import (
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
CommentField, ContentTypeSelect, DatePicker, DateTimePicker, FilterChoiceField, LaxURLField, JSONField,
SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES,
)
from .constants import *
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag
#
# Custom fields
#
def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False):
"""
Retrieve all CustomFields applicable to the given ContentType
"""
field_dict = OrderedDict()
custom_fields = CustomField.objects.filter(obj_type=content_type)
if filterable_only:
custom_fields = custom_fields.exclude(filter_logic=CF_FILTER_DISABLED)
for cf in custom_fields:
field_name = 'cf_{}'.format(str(cf.name))
initial = cf.default if not bulk_edit else None
# Integer
if cf.type == CF_TYPE_INTEGER:
field = forms.IntegerField(required=cf.required, initial=initial)
# Boolean
elif cf.type == CF_TYPE_BOOLEAN:
choices = (
(None, '---------'),
(1, 'True'),
(0, 'False'),
)
if initial is not None and initial.lower() in ['true', 'yes', '1']:
initial = 1
elif initial is not None and initial.lower() in ['false', 'no', '0']:
initial = 0
else:
initial = None
field = forms.NullBooleanField(
required=cf.required, initial=initial, widget=StaticSelect2(choices=choices)
)
# Date
elif cf.type == CF_TYPE_DATE:
field = forms.DateField(required=cf.required, initial=initial, widget=DatePicker())
# Select
elif cf.type == CF_TYPE_SELECT:
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
if not cf.required or bulk_edit or filterable_only:
choices = [(None, '---------')] + choices
# Check for a default choice
default_choice = None
if initial:
try:
default_choice = cf.choices.get(value=initial).pk
except ObjectDoesNotExist:
past
field = forms.TypedChoiceField(
choices=choices, coerce=int, required=cf.required, initial=default_choice, widget=StaticSelect2()
)
# URL
elif cf.type == CF_TYPE_URL:
field = LaxURLField(required=cf.required, initial=initial)
# Text
else:
field = forms.CharField(max_length=255, required=cf.required, initial=initial)
field.model = cf
field.label = cf.label if cf.label else cf.name.replace('_', ' ').capitalize()
if cf.description:
field.help_text = cf.description
field_dict[field_name] = field
return field_dict
clast CustomFieldForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.custom_fields = []
self.obj_type = ContentType.objects.get_for_model(self._meta.model)
super().__init__(*args, **kwargs)
# Add all applicable CustomFields to the form
custom_fields = []
for name, field in get_custom_fields_for_model(self.obj_type).items():
self.fields[name] = field
custom_fields.append(name)
self.custom_fields = custom_fields
# If editing an existing object, initialize values for all custom fields
if self.instance.pk:
existing_values = CustomFieldValue.objects.filter(
obj_type=self.obj_type,
obj_id=self.instance.pk
).prefetch_related('field')
for cfv in existing_values:
self.initial['cf_{}'.format(str(cfv.field.name))] = cfv.serialized_value
def _save_custom_fields(self):
for field_name in self.custom_fields:
try:
cfv = CustomFieldValue.objects.prefetch_related('field').get(
field=self.fields[field_name].model,
obj_type=self.obj_type,
obj_id=self.instance.pk
)
except CustomFieldValue.DoesNotExist:
# Skip this field if none exists already and its value is empty
if self.cleaned_data[field_name] in [None, '']:
continue
cfv = CustomFieldValue(
field=self.fields[field_name].model,
obj_type=self.obj_type,
obj_id=self.instance.pk
)
cfv.value = self.cleaned_data[field_name]
cfv.save()
def save(self, commit=True):
obj = super().save(commit)
# Handle custom fields the same way we do M2M fields
if commit:
self._save_custom_fields()
else:
self.save_custom_fields = self._save_custom_fields
return obj
clast CustomFieldBulkEditForm(BulkEditForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.custom_fields = []
self.obj_type = ContentType.objects.get_for_model(self.model)
# Add all applicable CustomFields to the form
custom_fields = get_custom_fields_for_model(self.obj_type, bulk_edit=True).items()
for name, field in custom_fields:
# Annotate non-required custom fields as nullable
if not field.required:
self.nullable_fields.append(name)
field.required = False
self.fields[name] = field
# Annotate this as a custom field
self.custom_fields.append(name)
clast CustomFieldFilterForm(forms.Form):
def __init__(self, *args, **kwargs):
self.obj_type = ContentType.objects.get_for_model(self.model)
super().__init__(*args, **kwargs)
# Add all applicable CustomFields to the form
custom_fields = get_custom_fields_for_model(self.obj_type, filterable_only=True).items()
for name, field in custom_fields:
field.required = False
self.fields[name] = field
#
# Tags
#
clast TagForm(BootstrapMixin, forms.ModelForm):
slug = SlugField()
comments = CommentField()
clast Meta:
model = Tag
fields = [
'name', 'slug', 'color', 'comments'
]
clast AddRemoveTagsForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add add/remove tags fields
self.fields['add_tags'] = TagField(required=False)
self.fields['remove_tags'] = TagField(required=False)
clast TagFilterForm(BootstrapMixin, forms.Form):
model = Tag
q = forms.CharField(
required=False,
label='Search'
)
clast TagBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Tag.objects.all(),
widget=forms.MultipleHiddenInput
)
color = forms.CharField(
max_length=6,
required=False,
widget=ColorSelect()
)
clast Meta:
nullable_fields = []
#
# Config contexts
#
clast ConfigContextForm(BootstrapMixin, forms.ModelForm):
data = JSONField(
label=''
)
clast Meta:
model = ConfigContext
fields = [
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenant_groups',
'tenants', 'data',
]
widgets = {
'regions': APISelectMultiple(
api_url="/api/dcim/regions/"
),
'sites': APISelectMultiple(
api_url="/api/dcim/sites/"
),
'roles': APISelectMultiple(
api_url="/api/dcim/device-roles/"
),
'platforms': APISelectMultiple(
api_url="/api/dcim/platforms/"
),
'tenant_groups': APISelectMultiple(
api_url="/api/tenancy/tenant-groups/"
),
'tenants': APISelectMultiple(
api_url="/api/tenancy/tenants/"
)
}
clast ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=ConfigContext.objects.all(),
widget=forms.MultipleHiddenInput
)
weight = forms.IntegerField(
required=False,
min_value=0
)
is_active = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect()
)
description = forms.CharField(
required=False,
max_length=100
)
clast Meta:
nullable_fields = [
'description',
]
clast ConfigContextFilterForm(BootstrapMixin, forms.Form):
q = forms.CharField(
required=False,
label='Search'
)
region = FilterChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug",
)
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
)
)
role = FilterChoiceField(
queryset=DeviceRole.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/device-roles/",
value_field="slug",
)
)
platform = FilterChoiceField(
queryset=Platform.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/platforms/",
value_field="slug",
)
)
tenant_group = FilterChoiceField(
queryset=TenantGroup.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/tenancy/tenant-groups/",
value_field="slug",
)
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
)
)
#
# Filter form for local config context data
#
clast LocalConfigContextFilterForm(forms.Form):
local_context_data = forms.NullBooleanField(
required=False,
label='Has local config context data',
widget=StaticSelect2(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
#
# Image attachments
#
clast ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
clast Meta:
model = ImageAttachment
fields = [
'name', 'image',
]
#
# Change logging
#
clast ObjectChangeFilterForm(BootstrapMixin, forms.Form):
model = ObjectChange
q = forms.CharField(
required=False,
label='Search'
)
time_after = forms.DateTimeField(
label='After',
required=False,
widget=DateTimePicker()
)
time_before = forms.DateTimeField(
label='Before',
required=False,
widget=DateTimePicker()
)
action = forms.ChoiceField(
choices=add_blank_choice(OBJECTCHANGE_ACTION_CHOICES),
required=False
)
user = forms.ModelChoiceField(
queryset=User.objects.order_by('username'),
required=False
)
changed_object_type = forms.ModelChoiceField(
queryset=ContentType.objects.order_by('model'),
required=False,
widget=ContentTypeSelect(),
label='Object Type'
)
#
# Scripts
#
clast ScriptForm(BootstrapMixin, forms.Form):
_commit = forms.BooleanField(
required=False,
initial=True,
label="Commit changes",
help_text="Commit changes to the database (uncheck for a dry-run)"
)
def __init__(self, vars, *args, commit_default=True, **kwargs):
super().__init__(*args, **kwargs)
# Dynamically populate fields for variables
for name, var in vars.items():
self.fields[name] = var.as_field()
# Toggle default commit behavior based on Meta option
if not commit_default:
self.fields['_commit'].initial = False
# Move _commit to the end of the form
self.fields.move_to_end('_commit', True)
@property
def requires_input(self):
"""
A boolean indicating whether the form requires user input (ignore the _commit field).
"""
return bool(len(self.fields) > 1)