Browse Source

Added bulk editing capability for custom fields

Jeremy Stretch 8 years ago
parent
commit
7d879bb0dc

+ 0 - 22
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'

+ 2 - 2
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')
 

+ 0 - 50
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'

+ 81 - 53
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
 
 
-class CustomFieldForm(forms.ModelForm):
-    custom_fields = []
-
-    def __init__(self, *args, **kwargs):
+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)
 
-        super(CustomFieldForm, self).__init__(*args, **kwargs)
+        # Text
+        else:
+            field = forms.CharField(max_length=100, required=cf.required, initial=cf.default)
 
-        obj_type = ContentType.objects.get_for_model(self._meta.model)
+        field.model = cf
+        field.label = cf.label if cf.label else cf.name.capitalize()
+        field.help_text = cf.description
 
-        # Find all CustomFields for this model
-        custom_fields = CustomField.objects.filter(obj_type=obj_type)
-        for cf in custom_fields:
+        field_dict[field_name] = field
 
-            field_name = 'cf_{}'.format(str(cf.name))
+    return field_dict
 
-            # 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))
+class CustomFieldForm(forms.ModelForm):
+    custom_fields = []
 
-            # Date
-            elif cf.type == CF_TYPE_DATE:
-                field = forms.DateField(required=cf.required, initial=cf.default)
+    def __init__(self, *args, **kwargs):
 
-            # Select
-            elif cf.type == CF_TYPE_SELECT:
-                field = forms.ModelChoiceField(queryset=cf.choices.all(), required=cf.required)
+        self.obj_type = ContentType.objects.get_for_model(self._meta.model)
 
-            # Text
-            else:
-                field = forms.CharField(max_length=100, required=cf.required, initial=cf.default)
+        super(CustomFieldForm, self).__init__(*args, **kwargs)
 
-            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)

+ 10 - 2
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},

+ 0 - 63
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'

+ 0 - 9
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'

+ 1 - 1
netbox/templates/dcim/site_bulk_edit.html

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

+ 1 - 1
netbox/templates/inc/custom_fields_panel.html

@@ -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>

+ 0 - 10
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'

+ 45 - 11
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):