diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index d84e23525..28c4d6844 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -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' diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 2e9c67e2e..390fd31fc 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -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') diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 17c0e1886..f3b2f4bf1 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -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' diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index f3e83ecb1..5c0e937af 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -4,78 +4,90 @@ from django.contrib.contenttypes.models import ContentType from .models import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CustomField, CustomFieldValue +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) + + for cf in custom_fields: + field_name = 'cf_{}'.format(str(cf.name)) + + # Integer + if cf.type == CF_TYPE_INTEGER: + field = forms.IntegerField(required=cf.required, initial=cf.default) + + # Boolean + elif cf.type == CF_TYPE_BOOLEAN: + choices = ( + (None, '---------'), + (True, 'True'), + (False, 'False'), + ) + field = forms.NullBooleanField(required=cf.required, widget=forms.Select(choices=choices)) + + # Date + elif cf.type == CF_TYPE_DATE: + field = forms.DateField(required=cf.required, initial=cf.default) + + # 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 + else: + field = forms.CharField(max_length=100, required=cf.required, initial=cf.default) + + field.model = cf + field.label = cf.label if cf.label else cf.name.capitalize() + field.help_text = cf.description + + 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) - 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 - if cf.type == CF_TYPE_INTEGER: - field = forms.IntegerField(required=cf.required, initial=cf.default) - - # 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)) - - # Date - elif cf.type == CF_TYPE_DATE: - field = forms.DateField(required=cf.required, initial=cf.default) - - # Select - elif cf.type == CF_TYPE_SELECT: - field = forms.ModelChoiceField(queryset=cf.choices.all(), required=cf.required) - - # Text - else: - field = forms.CharField(max_length=100, required=cf.required, initial=cf.default) - - 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) + # 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, - obj_id=self.instance.pk) - except CustomFieldValue.DoesNotExist: - cfv = CustomFieldValue( - field=self.fields[field_name].model, - obj_type=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() + for field_name in self.custom_fields: + try: + 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=self.obj_type, + obj_id=self.instance.pk + ) + cfv.value = self.cleaned_data[field_name] + cfv.save() def save(self, commit=True): obj = super(CustomFieldForm, self).save(commit) @@ -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) diff --git a/netbox/extras/models.py b/netbox/extras/models.py index ccfa3f626..1896421fc 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -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}, diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index f177fe433..c7c5a46c6 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -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' diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 351871675..14ac4fa78 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -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' diff --git a/netbox/templates/dcim/site_bulk_edit.html b/netbox/templates/dcim/site_bulk_edit.html index c5b0e4aa0..f8b6ddc9c 100644 --- a/netbox/templates/dcim/site_bulk_edit.html +++ b/netbox/templates/dcim/site_bulk_edit.html @@ -6,7 +6,7 @@ {% block select_objects_table %} {% for site in selected_objects %} - {{ site.slug }} + {{ site }} {{ site.tenant }} {% endfor %} diff --git a/netbox/templates/inc/custom_fields_panel.html b/netbox/templates/inc/custom_fields_panel.html index 64c9b11f0..a019a985d 100644 --- a/netbox/templates/inc/custom_fields_panel.html +++ b/netbox/templates/inc/custom_fields_panel.html @@ -8,7 +8,7 @@ {{ field }} - {% if value %} + {% if value != None %} {{ value }} {% elif field.required %} Not defined diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index a11915458..1055fb9b3 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -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' diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 2fc36948a..31485fc74 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -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,9 +282,22 @@ class BulkEditView(View): pk_list = request.POST.getlist('pk') if '_apply' in request.POST: - form = self.form(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) @@ -292,7 +305,10 @@ class BulkEditView(View): return redirect(redirect_url) else: - form = self.form(initial={'pk': pk_list}) + if hasattr(self.form, 'custom_fields'): + form = self.form(self.cls, initial={'pk': pk_list}) + else: + form = self.form(initial={'pk': pk_list}) selected_objects = self.cls.objects.filter(pk__in=pk_list) if not selected_objects: @@ -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):