From a4b6777e4e107b67dc6cc48fd10b6a949ef73354 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 12 Mar 2021 16:00:00 -0500 Subject: [PATCH] Enable bulk editing of nested group models --- netbox/dcim/forms.py | 61 ++++++++++++++++++++++++++++++ netbox/dcim/tests/test_views.py | 42 ++++++++++++++++++++ netbox/dcim/urls.py | 3 ++ netbox/dcim/views.py | 39 +++++++++++++++++++ netbox/tenancy/forms.py | 18 +++++++++ netbox/tenancy/tests/test_views.py | 4 ++ netbox/tenancy/urls.py | 1 + netbox/tenancy/views.py | 13 +++++++ 8 files changed, 181 insertions(+) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index aaa909c9f..dcdde0d60 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -201,6 +201,24 @@ class RegionCSVForm(CustomFieldModelCSVForm): fields = Region.csv_headers +class RegionBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Region.objects.all(), + widget=forms.MultipleHiddenInput + ) + parent = DynamicModelChoiceField( + queryset=Region.objects.all(), + required=False + ) + description = forms.CharField( + max_length=200, + required=False + ) + + class Meta: + nullable_fields = ['parent', 'description'] + + class RegionFilterForm(BootstrapMixin, forms.Form): model = Site q = forms.CharField( @@ -240,6 +258,24 @@ class SiteGroupCSVForm(CustomFieldModelCSVForm): fields = SiteGroup.csv_headers +class SiteGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + widget=forms.MultipleHiddenInput + ) + parent = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + required=False + ) + description = forms.CharField( + max_length=200, + required=False + ) + + class Meta: + nullable_fields = ['parent', 'description'] + + class SiteGroupFilterForm(BootstrapMixin, forms.Form): model = Site q = forms.CharField( @@ -480,6 +516,31 @@ class LocationCSVForm(CustomFieldModelCSVForm): fields = Location.csv_headers +class LocationBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Location.objects.all(), + widget=forms.MultipleHiddenInput + ) + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False + ) + parent = DynamicModelChoiceField( + queryset=Location.objects.all(), + required=False, + query_params={ + 'site_id': '$site' + } + ) + description = forms.CharField( + max_length=200, + required=False + ) + + class Meta: + nullable_fields = ['parent', 'description'] + + class LocationFilterForm(BootstrapMixin, forms.Form): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 845136914..8fde267d9 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -57,6 +57,44 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase): "Region 6,region-6,Sixth region", ) + cls.bulk_edit_data = { + 'description': 'New description', + } + + +class SiteGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): + model = SiteGroup + + @classmethod + def setUpTestData(cls): + + # Create three SiteGroups + sitegroups = ( + SiteGroup(name='Site Group 1', slug='site-group-1'), + SiteGroup(name='Site Group 2', slug='site-group-2'), + SiteGroup(name='Site Group 3', slug='site-group-3'), + ) + for sitegroup in sitegroups: + sitegroup.save() + + cls.form_data = { + 'name': 'Site Group X', + 'slug': 'site-group-x', + 'parent': sitegroups[2].pk, + 'description': 'A new site group', + } + + cls.csv_data = ( + "name,slug,description", + "Site Group 4,site-group-4,Fourth site group", + "Site Group 5,site-group-5,Fifth site group", + "Site Group 6,site-group-6,Sixth site group", + ) + + cls.bulk_edit_data = { + 'description': 'New description', + } + class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase): model = Site @@ -157,6 +195,10 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase): "Site 1,Location 6,location-6,Sixth location", ) + cls.bulk_edit_data = { + 'description': 'New description', + } + class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase): model = RackRole diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index fd4a3c52c..290049010 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -12,6 +12,7 @@ urlpatterns = [ path('regions/', views.RegionListView.as_view(), name='region_list'), path('regions/add/', views.RegionEditView.as_view(), name='region_add'), path('regions/import/', views.RegionBulkImportView.as_view(), name='region_import'), + path('regions/edit/', views.RegionBulkEditView.as_view(), name='region_bulk_edit'), path('regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'), path('regions//edit/', views.RegionEditView.as_view(), name='region_edit'), path('regions//delete/', views.RegionDeleteView.as_view(), name='region_delete'), @@ -21,6 +22,7 @@ urlpatterns = [ path('site-groups/', views.SiteGroupListView.as_view(), name='sitegroup_list'), path('site-groups/add/', views.SiteGroupEditView.as_view(), name='sitegroup_add'), path('site-groups/import/', views.SiteGroupBulkImportView.as_view(), name='sitegroup_import'), + path('site-groups/edit/', views.SiteGroupBulkEditView.as_view(), name='sitegroup_bulk_edit'), path('site-groups/delete/', views.SiteGroupBulkDeleteView.as_view(), name='sitegroup_bulk_delete'), path('site-groups//edit/', views.SiteGroupEditView.as_view(), name='sitegroup_edit'), path('site-groups//delete/', views.SiteGroupDeleteView.as_view(), name='sitegroup_delete'), @@ -42,6 +44,7 @@ urlpatterns = [ path('locations/', views.LocationListView.as_view(), name='location_list'), path('locations/add/', views.LocationEditView.as_view(), name='location_add'), path('locations/import/', views.LocationBulkImportView.as_view(), name='location_import'), + path('locations/edit/', views.LocationBulkEditView.as_view(), name='location_bulk_edit'), path('locations/delete/', views.LocationBulkDeleteView.as_view(), name='location_bulk_delete'), path('locations//edit/', views.LocationEditView.as_view(), name='location_edit'), path('locations//delete/', views.LocationDeleteView.as_view(), name='location_delete'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 04f3fa658..da733843c 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -126,6 +126,19 @@ class RegionBulkImportView(generic.BulkImportView): table = tables.RegionTable +class RegionBulkEditView(generic.BulkEditView): + queryset = Region.objects.add_related_count( + Region.objects.all(), + Site, + 'region', + 'site_count', + cumulative=True + ) + filterset = filters.RegionFilterSet + table = tables.RegionTable + form = forms.RegionBulkEditForm + + class RegionBulkDeleteView(generic.BulkDeleteView): queryset = Region.objects.add_related_count( Region.objects.all(), @@ -170,6 +183,19 @@ class SiteGroupBulkImportView(generic.BulkImportView): table = tables.SiteGroupTable +class SiteGroupBulkEditView(generic.BulkEditView): + queryset = SiteGroup.objects.add_related_count( + SiteGroup.objects.all(), + Site, + 'group', + 'site_count', + cumulative=True + ) + filterset = filters.SiteGroupFilterSet + table = tables.SiteGroupTable + form = forms.SiteGroupBulkEditForm + + class SiteGroupBulkDeleteView(generic.BulkDeleteView): queryset = SiteGroup.objects.add_related_count( SiteGroup.objects.all(), @@ -279,6 +305,19 @@ class LocationBulkImportView(generic.BulkImportView): table = tables.LocationTable +class LocationBulkEditView(generic.BulkEditView): + queryset = Location.objects.add_related_count( + Location.objects.all(), + Rack, + 'location', + 'rack_count', + cumulative=True + ).prefetch_related('site') + filterset = filters.LocationFilterSet + table = tables.LocationTable + form = forms.LocationBulkEditForm + + class LocationBulkDeleteView(generic.BulkDeleteView): queryset = Location.objects.add_related_count( Location.objects.all(), diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 43401baab..d53748055 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -44,6 +44,24 @@ class TenantGroupCSVForm(CustomFieldModelCSVForm): fields = TenantGroup.csv_headers +class TenantGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=TenantGroup.objects.all(), + widget=forms.MultipleHiddenInput + ) + parent = DynamicModelChoiceField( + queryset=TenantGroup.objects.all(), + required=False + ) + description = forms.CharField( + max_length=200, + required=False + ) + + class Meta: + nullable_fields = ['parent', 'description'] + + # # Tenants # diff --git a/netbox/tenancy/tests/test_views.py b/netbox/tenancy/tests/test_views.py index 5b88b84cf..8ef7efb12 100644 --- a/netbox/tenancy/tests/test_views.py +++ b/netbox/tenancy/tests/test_views.py @@ -29,6 +29,10 @@ class TenantGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): "Tenant Group 6,tenant-group-6,Sixth tenant group", ) + cls.bulk_edit_data = { + 'description': 'New description', + } + class TenantTestCase(ViewTestCases.PrimaryObjectViewTestCase): model = Tenant diff --git a/netbox/tenancy/urls.py b/netbox/tenancy/urls.py index c07df97a6..c54c7fee9 100644 --- a/netbox/tenancy/urls.py +++ b/netbox/tenancy/urls.py @@ -11,6 +11,7 @@ urlpatterns = [ path('tenant-groups/', views.TenantGroupListView.as_view(), name='tenantgroup_list'), path('tenant-groups/add/', views.TenantGroupEditView.as_view(), name='tenantgroup_add'), path('tenant-groups/import/', views.TenantGroupBulkImportView.as_view(), name='tenantgroup_import'), + path('tenant-groups/edit/', views.TenantGroupBulkEditView.as_view(), name='tenantgroup_bulk_edit'), path('tenant-groups/delete/', views.TenantGroupBulkDeleteView.as_view(), name='tenantgroup_bulk_delete'), path('tenant-groups//edit/', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'), path('tenant-groups//delete/', views.TenantGroupDeleteView.as_view(), name='tenantgroup_delete'), diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 4c72cea42..11f5ead00 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -39,6 +39,19 @@ class TenantGroupBulkImportView(generic.BulkImportView): table = tables.TenantGroupTable +class TenantGroupBulkEditView(generic.BulkEditView): + queryset = TenantGroup.objects.add_related_count( + TenantGroup.objects.all(), + Tenant, + 'group', + 'tenant_count', + cumulative=True + ) + filterset = filters.TenantGroupFilterSet + table = tables.TenantGroupTable + form = forms.TenantGroupBulkEditForm + + class TenantGroupBulkDeleteView(generic.BulkDeleteView): queryset = TenantGroup.objects.add_related_count( TenantGroup.objects.all(),