From 2bb0e65aea595b92f03d957246b76a372edddf7f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 30 Jan 2018 17:46:00 -0500 Subject: [PATCH] Closes #144: Implemented list and bulk edit/delete views for InventoryItems --- netbox/dcim/filters.py | 18 +++++++- netbox/dcim/forms.py | 44 +++++++++++++++++++ netbox/dcim/models.py | 15 +++++++ netbox/dcim/tables.py | 18 +++++++- netbox/dcim/urls.py | 6 ++- netbox/dcim/views.py | 35 ++++++++++++++- netbox/templates/dcim/inventoryitem_list.html | 23 ++++++++++ netbox/templates/inc/nav_menu.html | 12 ++++- 8 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 netbox/templates/dcim/inventoryitem_list.html diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index c3ef82704..f3d70edd4 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -613,6 +613,10 @@ class DeviceBayFilter(DeviceComponentFilterSet): class InventoryItemFilter(DeviceComponentFilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) parent_id = django_filters.ModelMultipleChoiceFilter( queryset=InventoryItem.objects.all(), label='Parent inventory item (ID)', @@ -631,7 +635,19 @@ class InventoryItemFilter(DeviceComponentFilterSet): class Meta: model = InventoryItem - fields = ['name', 'part_id', 'serial', 'discovered'] + fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered'] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + qs_filter = ( + Q(name__icontains=value) | + Q(part_id__icontains=value) | + Q(serial__iexact=value) | + Q(asset_tag__iexact=value) | + Q(description__icontains=value) + ) + return queryset.filter(qs_filter) class ConsoleConnectionFilter(django_filters.FilterSet): diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index ebe10942a..98c075cd3 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1928,3 +1928,47 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm): class Meta: model = InventoryItem fields = ['name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description'] + + +class InventoryItemCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Device name or ID', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + manufacturer = forms.ModelChoiceField( + queryset=Manufacturer.objects.all(), + to_field_name='name', + required=False, + help_text='Manufacturer name', + error_messages={ + 'invalid_choice': 'Invalid manufacturer.', + } + ) + + class Meta: + model = InventoryItem + fields = ['device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description'] + + +class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField(queryset=InventoryItem.objects.all(), widget=forms.MultipleHiddenInput) + manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False) + part_id = forms.CharField(max_length=50, required=False, label='Part ID') + description = forms.CharField(max_length=100, required=False) + + class Meta: + nullable_fields = ['manufacturer', 'part_id', 'description'] + + +class InventoryItemFilterForm(BootstrapMixin, forms.Form): + model = InventoryItem + q = forms.CharField(required=False, label='Search') + manufacturer = FilterChoiceField( + queryset=Manufacturer.objects.annotate(filter_count=Count('inventory_items')), + to_field_name='slug', + null_label='-- None --' + ) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index d50b35487..2b1f403e7 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1452,9 +1452,24 @@ class InventoryItem(models.Model): discovered = models.BooleanField(default=False, verbose_name='Discovered') description = models.CharField(max_length=100, blank=True) + csv_headers = [ + 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', + ] + class Meta: ordering = ['device__id', 'parent__id', 'name'] unique_together = ['device', 'parent', 'name'] def __str__(self): return self.name + + def to_csv(self): + return csv_format([ + self.device.name or '{' + self.device.pk + '}', + self.name, + self.manufacturer.name if self.manufacturer else None, + self.part_id, + self.serial, + self.asset_tag, + self.description + ]) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 01ad71ed0..0349396fa 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -7,8 +7,8 @@ from tenancy.tables import COL_TENANT from utilities.tables import BaseTable, ToggleColumn from .models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, - DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutlet, - PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site, + DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, InventoryItem, Manufacturer, Platform, + PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site, ) REGION_LINK = """ @@ -528,3 +528,17 @@ class InterfaceConnectionTable(BaseTable): class Meta(BaseTable.Meta): model = Interface fields = ('device_a', 'interface_a', 'device_b', 'interface_b') + + +# +# InventoryItems +# + +class InventoryItemTable(BaseTable): + pk = ToggleColumn() + device = tables.LinkColumn('dcim:device_inventory', args=[Accessor('device.pk')]) + manufacturer = tables.Column(accessor=Accessor('manufacturer.name'), verbose_name='Manufacturer') + + class Meta(BaseTable.Meta): + model = InventoryItem + fields = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description') diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index a15774569..bd10ad216 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -195,9 +195,13 @@ urlpatterns = [ url(r'^device-bays/(?P\d+)/depopulate/$', views.devicebay_depopulate, name='devicebay_depopulate'), # Inventory items - url(r'^devices/(?P\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'), + url(r'^inventory-items/$', views.InventoryItemListView.as_view(), name='inventoryitem_list'), + url(r'^inventory-items/import/$', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'), + url(r'^inventory-items/edit/$', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'), + url(r'^inventory-items/delete/$', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'), url(r'^inventory-items/(?P\d+)/edit/$', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'), url(r'^inventory-items/(?P\d+)/delete/$', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'), + url(r'^devices/(?P\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'), # Console/power/interface connections url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index ff402b6c3..5bf2c337b 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -23,7 +23,7 @@ from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator from utilities.views import ( BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView, - ComponentEditView, GetReturnURLMixin, ObjectDeleteView, ObjectEditView, ObjectListView, + ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView, ) from virtualization.models import VirtualMachine from . import filters, forms, tables @@ -1815,6 +1815,14 @@ class InterfaceConnectionsListView(ObjectListView): # Inventory items # +class InventoryItemListView(ObjectListView): + queryset = InventoryItem.objects.select_related('device', 'manufacturer') + filter = filters.InventoryItemFilter + filter_form = forms.InventoryItemFilterForm + table = tables.InventoryItemTable + template_name = 'dcim/inventoryitem_list.html' + + class InventoryItemEditView(PermissionRequiredMixin, ComponentEditView): permission_required = 'dcim.change_inventoryitem' model = InventoryItem @@ -1837,3 +1845,28 @@ class InventoryItemDeleteView(PermissionRequiredMixin, ComponentDeleteView): def get_return_url(self, request, obj): return reverse('dcim:device_inventory', kwargs={'pk': obj.device.pk}) + + +class InventoryItemBulkImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'dcim.add_inventoryitem' + model_form = forms.InventoryItemCSVForm + table = tables.InventoryItemTable + default_return_url = 'dcim:inventoryitem_list' + + +class InventoryItemBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_inventoryitem' + cls = InventoryItem + queryset = InventoryItem.objects.select_related('device', 'manufacturer') + filter = filters.InventoryItemFilter + table = tables.InventoryItemTable + form = forms.InventoryItemBulkEditForm + default_return_url = 'dcim:inventoryitem_list' + + +class InventoryItemBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): + permission_required = 'dcim.delete_inventoryitem' + cls = InventoryItem + queryset = InventoryItem.objects.select_related('device', 'manufacturer') + table = tables.InventoryItemTable + default_return_url = 'dcim:inventoryitem_list' diff --git a/netbox/templates/dcim/inventoryitem_list.html b/netbox/templates/dcim/inventoryitem_list.html new file mode 100644 index 000000000..612534d98 --- /dev/null +++ b/netbox/templates/dcim/inventoryitem_list.html @@ -0,0 +1,23 @@ +{% extends '_base.html' %} +{% load helpers %} + +{% block content %} +
+ {% if perms.dcim.add_devicetype %} + + + Import inventory items + + {% endif %} + {% include 'inc/export_button.html' with obj_type='inventory items' %} +
+

{% block title %}Inventory Items{% endblock %}

+
+
+ {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:inventoryitem_bulk_edit' bulk_delete_url='dcim:inventoryitem_bulk_delete' %} +
+
+ {% include 'inc/search_panel.html' %} +
+
+{% endblock %} diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html index dd811fb54..1857afcc2 100644 --- a/netbox/templates/inc/nav_menu.html +++ b/netbox/templates/inc/nav_menu.html @@ -104,7 +104,7 @@ -