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 %}