Browse Source

#527: Initial work to allow nullifying fields during bulk edit

Jeremy Stretch 8 years ago
parent
commit
36066068d4

+ 7 - 2
netbox/circuits/forms.py

@@ -3,7 +3,6 @@ from django.db.models import Count
 
 from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
 from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
-from tenancy.forms import bulkedit_tenant_choices
 from tenancy.models import Tenant
 from utilities.forms import (
     APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, FilterChoiceField, Livesearch, SmallTextarea,
@@ -57,6 +56,9 @@ class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
     admin_contact = forms.CharField(required=False, widget=SmallTextarea, label='Admin contact')
     comments = CommentField()
 
+    class Meta:
+        nullable_fields = ['asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
+
 
 class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Provider
@@ -178,11 +180,14 @@ class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
     pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
     type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
     provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
-    tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
+    tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
     port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
     commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
     comments = CommentField()
 
+    class Meta:
+        nullable_fields = ['tenant', 'port_speed', 'commit_rate', 'comments']
+
 
 class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Circuit

+ 19 - 44
netbox/dcim/forms.py

@@ -5,11 +5,11 @@ from django.db.models import Count, Q
 
 from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
 from ipam.models import IPAddress
-from tenancy.forms import bulkedit_tenant_choices
 from tenancy.models import Tenant
 from utilities.forms import (
-    APISelect, add_blank_choice, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField,
-    FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
+    APISelect, add_blank_choice, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField, CSVDataField,
+    ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea,
+    SlugField,
 )
 
 from .models import (
@@ -42,39 +42,6 @@ def get_device_by_name_or_pk(name):
     return device
 
 
-def bulkedit_platform_choices():
-    choices = [
-        (None, '---------'),
-        (0, 'None'),
-    ]
-    choices += [(p.pk, p.name) for p in Platform.objects.all()]
-    return choices
-
-
-def bulkedit_rackgroup_choices():
-    """
-    Include an option to remove the currently assigned group from a rack.
-    """
-    choices = [
-        (None, '---------'),
-        (0, 'None'),
-    ]
-    choices += [(r.pk, r) for r in RackGroup.objects.all()]
-    return choices
-
-
-def bulkedit_rackrole_choices():
-    """
-    Include an option to remove the currently assigned role from a rack.
-    """
-    choices = [
-        (None, '---------'),
-        (0, 'None'),
-    ]
-    choices += [(r.pk, r.name) for r in RackRole.objects.all()]
-    return choices
-
-
 #
 # Sites
 #
@@ -114,7 +81,10 @@ class SiteImportForm(BulkImportForm, 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')
+    tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
+
+    class Meta:
+        nullable_fields = ['tenant']
 
 
 class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
@@ -234,14 +204,17 @@ class RackImportForm(BulkImportForm, BootstrapMixin):
 class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
     pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
     site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site')
-    group = forms.TypedChoiceField(choices=bulkedit_rackgroup_choices, coerce=int, required=False, label='Group')
-    tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
-    role = forms.TypedChoiceField(choices=bulkedit_rackrole_choices, coerce=int, required=False, label='Role')
+    group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group')
+    tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
+    role = forms.ModelChoiceField(queryset=RackRole.objects.all(), required=False)
     type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type')
     width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width')
     u_height = forms.IntegerField(required=False, label='Height (U)')
     comments = CommentField()
 
+    class Meta:
+        nullable_fields = ['group', 'tenant', 'role', 'comments']
+
 
 class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Rack
@@ -279,7 +252,7 @@ class DeviceTypeForm(forms.ModelForm, BootstrapMixin):
                   'is_pdu', 'is_network_device', 'subdevice_role']
 
 
-class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
+class DeviceTypeBulkEditForm(BulkEditForm, BootstrapMixin):
     pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
     manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
     u_height = forms.IntegerField(min_value=1, required=False)
@@ -583,12 +556,14 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
     pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
     device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
     device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
-    tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
-    platform = forms.TypedChoiceField(choices=bulkedit_platform_choices, coerce=int, required=False,
-                                      label='Platform')
+    tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
+    platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False)
     status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status')
     serial = forms.CharField(max_length=50, required=False, label='Serial Number')
 
+    class Meta:
+        nullable_fields = ['tenant', 'platform']
+
 
 class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Device

+ 2 - 4
netbox/extras/forms.py

@@ -3,7 +3,7 @@ from collections import OrderedDict
 from django import forms
 from django.contrib.contenttypes.models import ContentType
 
-from utilities.forms import LaxURLField
+from utilities.forms import BulkEditForm, LaxURLField
 from .models import (
     CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue
 )
@@ -49,8 +49,6 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
         # 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_edit or filterable_only:
                 choices = [(None, '---------')] + choices
             field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required)
@@ -126,7 +124,7 @@ class CustomFieldForm(forms.ModelForm):
         return obj
 
 
-class CustomFieldBulkEditForm(forms.Form):
+class CustomFieldBulkEditForm(BulkEditForm):
     custom_fields = []
 
     def __init__(self, model, *args, **kwargs):

+ 21 - 19
netbox/ipam/forms.py

@@ -3,7 +3,6 @@ from django.db.models import Count
 
 from dcim.models import Site, Device, Interface
 from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
-from tenancy.forms import bulkedit_tenant_choices
 from tenancy.models import Tenant
 from utilities.forms import (
     APISelect, BootstrapMixin, CSVDataField, BulkImportForm, FilterChoiceField, Livesearch, SlugField,
@@ -23,18 +22,6 @@ IP_FAMILY_CHOICES = [
 ]
 
 
-def bulkedit_vrf_choices():
-    """
-    Include an option to assign the object to the global table.
-    """
-    choices = [
-        (None, '---------'),
-        (0, 'Global'),
-    ]
-    choices += [(v.pk, v.name) for v in VRF.objects.all()]
-    return choices
-
-
 #
 # VRFs
 #
@@ -67,9 +54,12 @@ class VRFImportForm(BulkImportForm, BootstrapMixin):
 
 class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
     pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
-    tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
+    tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
     description = forms.CharField(max_length=100, required=False)
 
+    class Meta:
+        nullable_fields = ['tenant', 'description']
+
 
 class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = VRF
@@ -124,6 +114,9 @@ class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
     date_added = forms.DateField(required=False)
     description = forms.CharField(max_length=100, required=False)
 
+    class Meta:
+        nullable_fields = ['date_added', 'description']
+
 
 class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Aggregate
@@ -253,12 +246,15 @@ class PrefixImportForm(BulkImportForm, BootstrapMixin):
 class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
     pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
     site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
-    vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
-    tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
+    vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
+    tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
     status = forms.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False)
     role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
     description = forms.CharField(max_length=100, required=False)
 
+    class Meta:
+        nullable_fields = ['site', 'vrf', 'tenant', 'role', 'description']
+
 
 def prefix_status_choices():
     status_counts = {}
@@ -407,10 +403,13 @@ class IPAddressImportForm(BulkImportForm, BootstrapMixin):
 
 class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
     pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
-    vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
-    tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
+    vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
+    tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
     description = forms.CharField(max_length=100, required=False)
 
+    class Meta:
+        nullable_fields = ['vrf', 'tenant', 'description']
+
 
 class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = IPAddress
@@ -509,11 +508,14 @@ class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
     pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
     site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
     group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
-    tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
+    tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
     status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False)
     role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
     description = forms.CharField(max_length=100, required=False)
 
+    class Meta:
+        nullable_fields = ['group', 'tenant', 'role', 'description']
+
 
 def vlan_status_choices():
     status_counts = {}

+ 5 - 0
netbox/project-static/js/forms.js

@@ -37,6 +37,11 @@ $(document).ready(function() {
         })
     }
 
+    // Bulk edit nullification
+    $('input:checkbox[name=_nullify]').click(function (event) {
+        $('#id_' + this.value).toggle('disabled');
+    });
+
     // API select widget
     $('select[filter-for]').change(function () {
 

+ 5 - 2
netbox/secrets/forms.py

@@ -5,7 +5,7 @@ from django import forms
 from django.db.models import Count
 
 from dcim.models import Device
-from utilities.forms import BootstrapMixin, BulkImportForm, CSVDataField, FilterChoiceField, SlugField
+from utilities.forms import BootstrapMixin, BulkEditForm, BulkImportForm, CSVDataField, FilterChoiceField, SlugField
 
 from .models import Secret, SecretRole, UserKey
 
@@ -89,11 +89,14 @@ class SecretImportForm(BulkImportForm, BootstrapMixin):
     csv = CSVDataField(csv_form=SecretFromCSVForm, widget=forms.Textarea(attrs={'class': 'requires-private-key'}))
 
 
-class SecretBulkEditForm(forms.Form, BootstrapMixin):
+class SecretBulkEditForm(BulkEditForm, BootstrapMixin):
     pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
     role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), required=False)
     name = forms.CharField(max_length=100, required=False)
 
+    class Meta:
+        nullable_fields = ['name']
+
 
 class SecretFilterForm(forms.Form, BootstrapMixin):
     role = FilterChoiceField(queryset=SecretRole.objects.annotate(filter_count=Count('secrets')), to_field_name='slug')

+ 10 - 1
netbox/templates/utilities/bulk_edit_form.html

@@ -8,6 +8,9 @@
     {% if request.POST.redirect_url %}
         <input type="hidden" name="redirect_url" value="{{ request.POST.redirect_url }}" />
     {% endif %}
+    {% for field in form.hidden_fields %}
+        {{ field }}
+    {% endfor %}
     <div class="row">
         <div class="col-md-7">
             <div class="panel panel-default">
@@ -29,7 +32,13 @@
             <div class="panel panel-default">
                 <div class="panel-heading"><strong>{% block form_title %}Attributes{% endblock %}</strong></div>
                 <div class="panel-body">
-                    {% render_form form %}
+                    {% for field in form.visible_fields %}
+                        {% if field.name in form.nullable_fields %}
+                            {% render_field field bulk_nullable=True %}
+                        {% else %}
+                            {% render_field field %}
+                        {% endif %}
+                    {% endfor %}
                 </div>
             </div>
 		    <div class="form-group text-right">

+ 15 - 10
netbox/templates/utilities/render_field.html

@@ -5,26 +5,26 @@
         <div class="col-md-9 col-md-offset-3">
             <div class="checkbox{% if field.errors %} has-error{% endif %}">
                 <label for="{{ field.id_for_label }}">
-                    {{ field }}
-                    {{ field.label }}
+                    {{ field }} {{ field.label }}
                 </label>
                 {% if field.help_text %}
                     <span class="help-block">{{ field.help_text|safe }}</span>
                 {% endif %}
             </div>
-        </div>
-    {% elif field|widget_type == 'radioselect' %}
-        <div class="col-md-9 col-md-offset-3">
-            <div class="radio{% if field.errors %} has-error{% endif %}">
-                <label for="{{ field.id_for_label }}">
-                    {{ field }}
-                    {{ field.label }}
+            {% if bulk_nullable %}
+                <label class="checkbox-inline">
+                    <input type="checkbox" name="_nullify" value="{{ field.name }}" /> Set null
                 </label>
-            </div>
+            {% endif %}
         </div>
     {% elif field|widget_type == 'textarea' %}
         <div class="col-md-12">
             {{ field }}
+            {% if bulk_nullable %}
+                <label class="checkbox-inline">
+                    <input type="checkbox" name="_nullify" value="{{ field.name }}" /> Set null
+                </label>
+            {% endif %}
             {% if field.help_text %}
                 <span class="help-block">{{ field.help_text|safe }}</span>
             {% endif %}
@@ -40,6 +40,11 @@
         <label class="col-md-3 control-label{% if field.field.required %} required{% endif %}" for="{{ field.id_for_label }}">{{ field.label }}</label>
         <div class="col-md-9">
             {{ field }}
+            {% if bulk_nullable %}
+                <label class="checkbox-inline">
+                    <input type="checkbox" name="_nullify" value="{{ field.name }}" /> Set null
+                </label>
+            {% endif %}
             {% if field.help_text %}
                 <span class="help-block">{{ field.help_text|safe }}</span>
             {% endif %}

+ 4 - 25
netbox/tenancy/forms.py

@@ -7,30 +7,6 @@ from utilities.forms import BootstrapMixin, BulkImportForm, CommentField, CSVDat
 from .models import Tenant, TenantGroup
 
 
-def bulkedit_tenantgroup_choices():
-    """
-    Include an option to remove the currently assigned TenantGroup from a Tenant.
-    """
-    choices = [
-        (None, '---------'),
-        (0, 'None'),
-    ]
-    choices += [(g.pk, g.name) for g in TenantGroup.objects.all()]
-    return choices
-
-
-def bulkedit_tenant_choices():
-    """
-    Include an option to remove the currently assigned Tenant from an object.
-    """
-    choices = [
-        (None, '---------'),
-        (0, 'None'),
-    ]
-    choices += [(t.pk, t.name) for t in Tenant.objects.all()]
-    return choices
-
-
 #
 # Tenant groups
 #
@@ -71,7 +47,10 @@ class TenantImportForm(BulkImportForm, BootstrapMixin):
 
 class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
     pk = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), widget=forms.MultipleHiddenInput)
-    group = forms.TypedChoiceField(choices=bulkedit_tenantgroup_choices, coerce=int, required=False, label='Group')
+    group = forms.ModelChoiceField(queryset=TenantGroup.objects.all(), required=False)
+
+    class Meta:
+        nullable_fields = ['group']
 
 
 class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):

+ 7 - 0
netbox/utilities/forms.py

@@ -294,6 +294,13 @@ class ConfirmationForm(forms.Form, BootstrapMixin):
     confirm = forms.BooleanField(required=True)
 
 
+class BulkEditForm(forms.Form):
+
+    def __init__(self, *args, **kwargs):
+        super(BulkEditForm, self).__init__(*args, **kwargs)
+        self.nullable_fields = getattr(self.Meta, 'nullable_fields')
+
+
 class BulkImportForm(forms.Form):
 
     def clean(self):

+ 2 - 1
netbox/utilities/templatetags/form_helpers.py

@@ -5,12 +5,13 @@ register = template.Library()
 
 
 @register.inclusion_tag('utilities/render_field.html')
-def render_field(field):
+def render_field(field, bulk_nullable=False):
     """
     Render a single form field from template
     """
     return {
         'field': field,
+        'bulk_nullable': bulk_nullable,
     }
 
 

+ 9 - 14
netbox/utilities/views.py

@@ -310,8 +310,15 @@ class BulkEditView(View):
                 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 standard fields. If a field is listed in _nullify, delete its value.
+                nullified_fields = request.POST.getlist('_nullify')
+                fields_to_update = {}
+                for field in standard_fields:
+                    if field in form.nullable_fields and field in nullified_fields:
+                        fields_to_update[field] = ''
+                    elif form.cleaned_data[field]:
+                        fields_to_update[field] = form.cleaned_data[field]
+                updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
 
                 # Update custom fields for objects
                 if custom_fields:
@@ -342,18 +349,6 @@ class BulkEditView(View):
             'cancel_url': redirect_url,
         })
 
-    def update_objects(self, pk_list, form, fields):
-        fields_to_update = {}
-
-        for name in fields:
-            # Check for zero value (bulk editing)
-            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)
         objs_updated = False