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

Added bulk editing capability for custom fields

This commit is contained in:
Jeremy Stretch
2016-08-16 14:57:04 -04:00
parent a9a55350df
commit 7d879bb0dc
11 changed files with 143 additions and 227 deletions

View File

@ -66,15 +66,6 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
template_name = 'circuits/provider_bulk_edit.html'
default_redirect_url = 'circuits:provider_list'
def update_objects(self, pk_list, form):
fields_to_update = {}
for field in ['asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_provider'
@ -159,19 +150,6 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
template_name = 'circuits/circuit_bulk_edit.html'
default_redirect_url = 'circuits:circuit_list'
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['tenant'] == 0:
fields_to_update['tenant'] = None
elif form.cleaned_data['tenant']:
fields_to_update['tenant'] = form.cleaned_data['tenant']
for field in ['type', 'provider', 'port_speed', 'commit_rate', 'comments']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuit'

View File

@ -3,7 +3,7 @@ import re
from django import forms
from django.db.models import Count, Q
from extras.forms import CustomFieldForm
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm
from ipam.models import IPAddress
from tenancy.forms import bulkedit_tenant_choices
from tenancy.models import Tenant
@ -112,7 +112,7 @@ class SiteImportForm(BulkImportForm, BootstrapMixin):
csv = CSVDataField(csv_form=SiteFromCSVForm)
class SiteBulkEditForm(forms.Form, BootstrapMixin):
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')

View File

@ -122,16 +122,6 @@ class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
template_name = 'dcim/site_bulk_edit.html'
default_redirect_url = 'dcim:site_list'
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['tenant'] == 0:
fields_to_update['tenant'] = None
elif form.cleaned_data['tenant']:
fields_to_update['tenant'] = form.cleaned_data['tenant']
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
#
# Rack groups
@ -248,20 +238,6 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
template_name = 'dcim/rack_bulk_edit.html'
default_redirect_url = 'dcim:rack_list'
def update_objects(self, pk_list, form):
fields_to_update = {}
for field in ['group', 'tenant', 'role']:
if form.cleaned_data[field] == 0:
fields_to_update[field] = None
elif form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
for field in ['site', 'type', 'width', 'u_height', 'comments']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rack'
@ -372,15 +348,6 @@ class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
template_name = 'dcim/devicetype_bulk_edit.html'
default_redirect_url = 'dcim:devicetype_list'
def update_objects(self, pk_list, form):
fields_to_update = {}
for field in ['manufacturer', 'u_height']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicetype'
@ -682,23 +649,6 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
template_name = 'dcim/device_bulk_edit.html'
default_redirect_url = 'dcim:device_list'
def update_objects(self, pk_list, form):
fields_to_update = {}
for field in ['tenant', 'platform']:
if form.cleaned_data[field] == 0:
fields_to_update[field] = None
elif form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
if form.cleaned_data['status']:
status = form.cleaned_data['status']
fields_to_update['status'] = True if status == 'True' else False
for field in ['tenant', 'device_type', 'device_role', 'serial']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_device'

View File

@ -4,19 +4,12 @@ from django.contrib.contenttypes.models import ContentType
from .models import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CustomField, CustomFieldValue
class CustomFieldForm(forms.ModelForm):
custom_fields = []
def get_custom_fields_for_model(content_type, bulk_editing=False):
"""Retrieve all CustomFields applicable to the given ContentType"""
field_dict = {}
custom_fields = CustomField.objects.filter(obj_type=content_type)
def __init__(self, *args, **kwargs):
super(CustomFieldForm, self).__init__(*args, **kwargs)
obj_type = ContentType.objects.get_for_model(self._meta.model)
# Find all CustomFields for this model
custom_fields = CustomField.objects.filter(obj_type=obj_type)
for cf in custom_fields:
field_name = 'cf_{}'.format(str(cf.name))
# Integer
@ -25,10 +18,12 @@ class CustomFieldForm(forms.ModelForm):
# Boolean
elif cf.type == CF_TYPE_BOOLEAN:
if cf.required:
field = forms.BooleanField(required=False, initial=bool(cf.default))
else:
field = forms.NullBooleanField(required=False, initial=bool(cf.default))
choices = (
(None, '---------'),
(True, 'True'),
(False, 'False'),
)
field = forms.NullBooleanField(required=cf.required, widget=forms.Select(choices=choices))
# Date
elif cf.type == CF_TYPE_DATE:
@ -36,6 +31,13 @@ class CustomFieldForm(forms.ModelForm):
# Select
elif cf.type == CF_TYPE_SELECT:
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
if not cf.required:
choices = [(0, 'None')] + choices
if bulk_editing:
choices = [(None, '---------')] + choices
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required)
else:
field = forms.ModelChoiceField(queryset=cf.choices.all(), required=cf.required)
# Text
@ -45,35 +47,45 @@ class CustomFieldForm(forms.ModelForm):
field.model = cf
field.label = cf.label if cf.label else cf.name.capitalize()
field.help_text = cf.description
self.fields[field_name] = field
self.custom_fields.append(field_name)
field_dict[field_name] = field
return field_dict
class CustomFieldForm(forms.ModelForm):
custom_fields = []
def __init__(self, *args, **kwargs):
self.obj_type = ContentType.objects.get_for_model(self._meta.model)
super(CustomFieldForm, self).__init__(*args, **kwargs)
# Add all applicable CustomFields to the form
for name, field in get_custom_fields_for_model(self.obj_type).items():
self.fields[name] = field
self.custom_fields.append(name)
# If editing an existing object, initialize values for all custom fields
if self.instance.pk:
existing_values = CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=self.instance.pk)\
existing_values = CustomFieldValue.objects.filter(obj_type=self.obj_type, obj_id=self.instance.pk)\
.select_related('field')
for cfv in existing_values:
self.initial['cf_{}'.format(str(cfv.field.name))] = cfv.value
def _save_custom_fields(self):
if self.instance.pk:
obj_type = ContentType.objects.get_for_model(self.instance)
for field_name in self.custom_fields:
try:
cfv = CustomFieldValue.objects.get(field=self.fields[field_name].model, obj_type=obj_type,
cfv = CustomFieldValue.objects.get(field=self.fields[field_name].model, obj_type=self.obj_type,
obj_id=self.instance.pk)
except CustomFieldValue.DoesNotExist:
cfv = CustomFieldValue(
field=self.fields[field_name].model,
obj_type=obj_type,
obj_type=self.obj_type,
obj_id=self.instance.pk
)
if cfv.pk and self.cleaned_data[field_name] is None:
cfv.delete()
elif self.cleaned_data[field_name] is not None:
cfv.value = self.cleaned_data[field_name]
cfv.save()
@ -87,3 +99,19 @@ class CustomFieldForm(forms.ModelForm):
self.save_custom_fields = self._save_custom_fields
return obj
class CustomFieldBulkEditForm(forms.Form):
custom_fields = []
def __init__(self, model, *args, **kwargs):
self.obj_type = ContentType.objects.get_for_model(model)
super(CustomFieldBulkEditForm, self).__init__(*args, **kwargs)
# Add all applicable CustomFields to the form
for name, field in get_custom_fields_for_model(self.obj_type, bulk_editing=True).items():
field.required = False
self.fields[name] = field
self.custom_fields.append(name)

View File

@ -131,14 +131,22 @@ class CustomFieldValue(models.Model):
if self.field.type == CF_TYPE_INTEGER:
self.val_int = value
elif self.field.type == CF_TYPE_BOOLEAN:
self.val_int = bool(value) if value else None
self.val_int = int(bool(value)) if value is not None else None
elif self.field.type == CF_TYPE_DATE:
self.val_date = value
elif self.field.type == CF_TYPE_SELECT:
self.val_int = value.id
# Could be ModelChoiceField or TypedChoiceField
self.val_int = value.id if hasattr(value, 'id') else value
else:
self.val_char = value
def save(self, *args, **kwargs):
if (self.field.type == CF_TYPE_TEXT and self.value == '') or self.value is None:
if self.pk:
self.delete()
else:
super(CustomFieldValue, self).save(*args, **kwargs)
class CustomFieldChoice(models.Model):
field = models.ForeignKey('CustomField', related_name='choices', limit_choices_to={'type': CF_TYPE_SELECT},

View File

@ -136,19 +136,6 @@ class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
template_name = 'ipam/vrf_bulk_edit.html'
default_redirect_url = 'ipam:vrf_list'
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['tenant'] == 0:
fields_to_update['tenant'] = None
elif form.cleaned_data['tenant']:
fields_to_update['tenant'] = form.cleaned_data['tenant']
for field in ['description']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vrf'
@ -261,15 +248,6 @@ class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
template_name = 'ipam/aggregate_bulk_edit.html'
default_redirect_url = 'ipam:aggregate_list'
def update_objects(self, pk_list, form):
fields_to_update = {}
for field in ['rir', 'date_added', 'description']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_aggregate'
@ -401,20 +379,6 @@ class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
template_name = 'ipam/prefix_bulk_edit.html'
default_redirect_url = 'ipam:prefix_list'
def update_objects(self, pk_list, form):
fields_to_update = {}
for field in ['vrf', 'tenant']:
if form.cleaned_data[field] == 0:
fields_to_update[field] = None
elif form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
for field in ['site', 'status', 'role', 'description']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_prefix'
@ -527,20 +491,6 @@ class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
template_name = 'ipam/ipaddress_bulk_edit.html'
default_redirect_url = 'ipam:ipaddress_list'
def update_objects(self, pk_list, form):
fields_to_update = {}
for field in ['vrf', 'tenant']:
if form.cleaned_data[field] == 0:
fields_to_update[field] = None
elif form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
for field in ['description']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_ipaddress'
@ -629,19 +579,6 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
template_name = 'ipam/vlan_bulk_edit.html'
default_redirect_url = 'ipam:vlan_list'
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['tenant'] == 0:
fields_to_update['tenant'] = None
elif form.cleaned_data['tenant']:
fields_to_update['tenant'] = form.cleaned_data['tenant']
for field in ['site', 'group', 'status', 'role', 'description']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlan'

View File

@ -205,15 +205,6 @@ class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
template_name = 'secrets/secret_bulk_edit.html'
default_redirect_url = 'secrets:secret_list'
def update_objects(self, pk_list, form):
fields_to_update = {}
for field in ['role', 'name']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'secrets.delete_secret'

View File

@ -6,7 +6,7 @@
{% block select_objects_table %}
{% for site in selected_objects %}
<tr>
<td><a href="{% url 'dcim:site' slug=site.slug %}">{{ site.slug }}</a></td>
<td><a href="{% url 'dcim:site' slug=site.slug %}">{{ site }}</a></td>
<td>{{ site.tenant }}</td>
</tr>
{% endfor %}

View File

@ -8,7 +8,7 @@
<tr>
<td>{{ field }}</td>
<td>
{% if value %}
{% if value != None %}
{{ value }}
{% elif field.required %}
<span class="text-warning">Not defined</span>

View File

@ -107,16 +107,6 @@ class TenantBulkEditView(PermissionRequiredMixin, BulkEditView):
template_name = 'tenancy/tenant_bulk_edit.html'
default_redirect_url = 'tenancy:tenant_list'
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['group'] == 0:
fields_to_update['group'] = None
elif form.cleaned_data['group']:
fields_to_update['group'] = form.cleaned_data['group']
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
class TenantBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'tenancy.delete_tenant'

View File

@ -7,7 +7,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse
from django.db import transaction, IntegrityError
from django.db.models import ProtectedError
from django.forms import ModelMultipleChoiceField, MultipleHiddenInput
from django.forms import ModelMultipleChoiceField, MultipleHiddenInput, TypedChoiceField
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.template import TemplateSyntaxError
@ -15,8 +15,8 @@ from django.utils.decorators import method_decorator
from django.utils.http import is_safe_url
from django.views.generic import View
from extras.forms import CustomFieldForm
from extras.models import ExportTemplate, UserAction
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm
from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction
from .error_handlers import handle_protectederror
from .forms import ConfirmationForm
@ -282,15 +282,31 @@ class BulkEditView(View):
pk_list = request.POST.getlist('pk')
if '_apply' in request.POST:
if hasattr(self.form, 'custom_fields'):
form = self.form(self.cls, request.POST)
else:
form = self.form(request.POST)
if form.is_valid():
updated_count = self.update_objects(pk_list, form)
custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else []
standard_fields = [field for field in form.fields if field not in custom_fields and field != 'pk']
# Update objects
updated_count = self.update_objects(pk_list, form, standard_fields)
# Update custom fields for objects
if custom_fields:
self.update_custom_fields(pk_list, form, custom_fields)
if updated_count:
msg = u'Updated {} {}'.format(updated_count, self.cls._meta.verbose_name_plural)
messages.success(self.request, msg)
UserAction.objects.log_bulk_edit(request.user, ContentType.objects.get_for_model(self.cls), msg)
return redirect(redirect_url)
else:
if hasattr(self.form, 'custom_fields'):
form = self.form(self.cls, initial={'pk': pk_list})
else:
form = self.form(initial={'pk': pk_list})
@ -305,11 +321,29 @@ class BulkEditView(View):
'cancel_url': redirect_url,
})
def update_objects(self, obj_list, form):
"""
This method provides the update logic (must be overridden by subclasses).
"""
raise NotImplementedError()
def update_objects(self, pk_list, form, fields):
fields_to_update = {}
for name in fields:
if isinstance(form.fields[name], TypedChoiceField) and form.cleaned_data[name] == 0:
fields_to_update[name] = None
elif form.cleaned_data[name]:
fields_to_update[name] = form.cleaned_data[name]
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
def update_custom_fields(self, pk_list, form, fields):
obj_type = ContentType.objects.get_for_model(self.cls)
for name in fields:
if form.cleaned_data[name] not in [None, u'']:
for pk in pk_list:
try:
cfv = CustomFieldValue.objects.get(field=form.fields[name].model, obj_type=obj_type, obj_id=pk)
except CustomFieldValue.DoesNotExist:
cfv = CustomFieldValue(field=form.fields[name].model, obj_type=obj_type, obj_id=pk)
cfv.value = form.cleaned_data[name]
cfv.save()
class BulkDeleteView(View):