From 4e8fc03c2b6e5ed3b2d0689a480539a940bf13df Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 30 Jan 2018 11:18:37 -0500 Subject: [PATCH 01/10] Fixes #1845: Correct display of VMs in list with no role assigned --- netbox/virtualization/tables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index 0498edd46..3938581fc 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -24,7 +24,7 @@ VIRTUALMACHINE_STATUS = """ """ VIRTUALMACHINE_ROLE = """ - +{% if record.role %}{% else %}—{% endif %} """ VIRTUALMACHINE_PRIMARY_IP = """ From 3edf90714af47a8ff2dcf6fcc8824f6c638f3eb4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 30 Jan 2018 11:57:21 -0500 Subject: [PATCH 02/10] Closes #1406: Display tenant description as title text in object tables --- netbox/circuits/tables.py | 3 ++- netbox/dcim/tables.py | 11 ++++++----- netbox/ipam/tables.py | 13 +++++++------ netbox/tenancy/tables.py | 8 ++++++++ netbox/virtualization/tables.py | 3 ++- 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index 10f776ea3..9a5225d56 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -4,6 +4,7 @@ import django_tables2 as tables from django.utils.safestring import mark_safe from django_tables2.utils import Accessor +from tenancy.tables import COL_TENANT from utilities.tables import BaseTable, ToggleColumn from .models import Circuit, CircuitType, Provider @@ -75,7 +76,7 @@ class CircuitTable(BaseTable): pk = ToggleColumn() cid = tables.LinkColumn(verbose_name='ID') provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')]) - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + tenant = tables.TemplateColumn(template_code=COL_TENANT) termination_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side') termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side') diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index cb3b1ff3b..01ad71ed0 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import django_tables2 as tables from django_tables2.utils import Accessor +from tenancy.tables import COL_TENANT from utilities.tables import BaseTable, ToggleColumn from .models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, @@ -140,7 +141,7 @@ class SiteTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() region = tables.TemplateColumn(template_code=SITE_REGION_LINK) - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(BaseTable.Meta): model = Site @@ -207,7 +208,7 @@ class RackTable(BaseTable): name = tables.LinkColumn() site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + tenant = tables.TemplateColumn(template_code=COL_TENANT) role = tables.TemplateColumn(RACK_ROLE) u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') @@ -231,7 +232,7 @@ class RackImportTable(BaseTable): site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') facility_id = tables.Column(verbose_name='Facility ID') - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') + tenant = tables.TemplateColumn(template_code=COL_TENANT) u_height = tables.Column(verbose_name='Height (U)') class Meta(BaseTable.Meta): @@ -402,7 +403,7 @@ class DeviceTable(BaseTable): pk = ToggleColumn() name = tables.TemplateColumn(template_code=DEVICE_LINK) status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status') - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + tenant = tables.TemplateColumn(template_code=COL_TENANT) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role') @@ -429,7 +430,7 @@ class DeviceDetailTable(DeviceTable): class DeviceImportTable(BaseTable): name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name') status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status') - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') + tenant = tables.TemplateColumn(template_code=COL_TENANT) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack') position = tables.Column(verbose_name='Position') diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 8d7d29b96..32f04c223 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import django_tables2 as tables from django_tables2.utils import Accessor +from tenancy.tables import COL_TENANT from utilities.tables import BaseTable, ToggleColumn from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF @@ -131,9 +132,9 @@ VLANGROUP_ACTIONS = """ TENANT_LINK = """ {% if record.tenant %} - {{ record.tenant }} + {{ record.tenant }} {% elif record.vrf.tenant %} - {{ record.vrf.tenant }}* + {{ record.vrf.tenant }}* {% else %} — {% endif %} @@ -148,7 +149,7 @@ class VRFTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() rd = tables.Column(verbose_name='RD') - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(BaseTable.Meta): model = VRF @@ -239,7 +240,7 @@ class PrefixTable(BaseTable): prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}}) status = tables.TemplateColumn(STATUS_LABEL) vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') - tenant = tables.TemplateColumn(TENANT_LINK) + tenant = tables.TemplateColumn(template_code=TENANT_LINK) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN') role = tables.TemplateColumn(PREFIX_ROLE_LINK) @@ -268,7 +269,7 @@ class IPAddressTable(BaseTable): address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address') vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') status = tables.TemplateColumn(STATUS_LABEL) - tenant = tables.TemplateColumn(TENANT_LINK) + tenant = tables.TemplateColumn(template_code=TENANT_LINK) parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False) interface = tables.Column(orderable=False) @@ -330,7 +331,7 @@ class VLANTable(BaseTable): vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + tenant = tables.TemplateColumn(template_code=COL_TENANT) status = tables.TemplateColumn(STATUS_LABEL) role = tables.TemplateColumn(VLAN_ROLE_LINK) diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 2b2989941..b3c67e9e2 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -11,6 +11,14 @@ TENANTGROUP_ACTIONS = """ {% endif %} """ +COL_TENANT = """ +{% if record.tenant %} + {{ record.tenant }} +{% else %} + — +{% endif %} +""" + # # Tenant groups diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index 3938581fc..2ace86d77 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -4,6 +4,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from dcim.models import Interface +from tenancy.tables import COL_TENANT from utilities.tables import BaseTable, ToggleColumn from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -97,7 +98,7 @@ class VirtualMachineTable(BaseTable): status = tables.TemplateColumn(template_code=VIRTUALMACHINE_STATUS) cluster = tables.LinkColumn('virtualization:cluster', args=[Accessor('cluster.pk')]) role = tables.TemplateColumn(VIRTUALMACHINE_ROLE) - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(BaseTable.Meta): model = VirtualMachine From 16f222b0ab114475649a448a0dad61e6375aaf57 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 30 Jan 2018 12:11:20 -0500 Subject: [PATCH 03/10] Closes #1366: Enable searching for regions by name/slug --- netbox/dcim/filters.py | 13 +++++++++++++ netbox/dcim/forms.py | 5 +++++ netbox/dcim/views.py | 2 ++ netbox/templates/dcim/region_list.html | 5 ++++- 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index e56a12ac0..c3ef82704 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -22,6 +22,10 @@ from .models import ( class RegionFilter(django_filters.FilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) parent_id = django_filters.ModelMultipleChoiceFilter( queryset=Region.objects.all(), label='Parent region (ID)', @@ -37,6 +41,15 @@ class RegionFilter(django_filters.FilterSet): model = Region fields = ['name', 'slug'] + def search(self, queryset, name, value): + if not value.strip(): + return queryset + qs_filter = ( + Q(name__icontains=value) | + Q(slug__icontains=value) + ) + return queryset.filter(qs_filter) + class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): id__in = NumericInFilter(name='id', lookup_expr='in') diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index e051e33e5..ebe10942a 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -81,6 +81,11 @@ class RegionCSVForm(forms.ModelForm): } +class RegionFilterForm(BootstrapMixin, forms.Form): + model = Site + q = forms.CharField(required=False, label='Search') + + # # Sites # diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 0dc393cfb..b0b5beae3 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -80,6 +80,8 @@ class BulkDisconnectView(View): class RegionListView(ObjectListView): queryset = Region.objects.annotate(site_count=Count('sites')) + filter = filters.RegionFilter + filter_form = forms.RegionFilterForm table = tables.RegionTable template_name = 'dcim/region_list.html' diff --git a/netbox/templates/dcim/region_list.html b/netbox/templates/dcim/region_list.html index 73fddbb0a..4d61b4acb 100644 --- a/netbox/templates/dcim/region_list.html +++ b/netbox/templates/dcim/region_list.html @@ -17,8 +17,11 @@

{% block title %}Regions{% endblock %}

-
+
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %}
+
+ {% include 'inc/search_panel.html' %} +
{% endblock %} From ffc2c564b8ac3d1b9471eed9d7b199b4160f7cc9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 30 Jan 2018 13:06:43 -0500 Subject: [PATCH 04/10] Cleaned up InventoryItem add/edit/delete links and return URL --- netbox/dcim/views.py | 8 +++++++- netbox/templates/dcim/device_inventory.html | 13 +++++++------ netbox/templates/dcim/inc/inventoryitem.html | 2 +- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index b0b5beae3..ff402b6c3 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, ObjectDeleteView, ObjectEditView, ObjectListView, + ComponentEditView, GetReturnURLMixin, ObjectDeleteView, ObjectEditView, ObjectListView, ) from virtualization.models import VirtualMachine from . import filters, forms, tables @@ -1826,8 +1826,14 @@ class InventoryItemEditView(PermissionRequiredMixin, ComponentEditView): obj.device = get_object_or_404(Device, pk=url_kwargs['device']) return obj + def get_return_url(self, request, obj): + return reverse('dcim:device_inventory', kwargs={'pk': obj.device.pk}) + class InventoryItemDeleteView(PermissionRequiredMixin, ComponentDeleteView): permission_required = 'dcim.delete_inventoryitem' model = InventoryItem parent_field = 'device' + + def get_return_url(self, request, obj): + return reverse('dcim:device_inventory', kwargs={'pk': obj.device.pk}) diff --git a/netbox/templates/dcim/device_inventory.html b/netbox/templates/dcim/device_inventory.html index 32b15670c..1db2dcefa 100644 --- a/netbox/templates/dcim/device_inventory.html +++ b/netbox/templates/dcim/device_inventory.html @@ -64,13 +64,14 @@ {% endfor %} + {% if perms.dcim.add_inventoryitem %} + + {% endif %}
- {% if perms.dcim.add_inventoryitem %} - - - Add Inventory Item - - {% endif %} {% endblock %} diff --git a/netbox/templates/dcim/inc/inventoryitem.html b/netbox/templates/dcim/inc/inventoryitem.html index 21de1014e..b50765271 100644 --- a/netbox/templates/dcim/inc/inventoryitem.html +++ b/netbox/templates/dcim/inc/inventoryitem.html @@ -11,7 +11,7 @@ {% endif %} {% if perms.dcim.delete_inventoryitem %} - + {% endif %} From a5d2055c117f359b15d155cd5848dcd5beb5cdc9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 30 Jan 2018 13:39:33 -0500 Subject: [PATCH 05/10] Closes #1073: Include prefixes/IPs from all VRFs when viewing the children of a container prefix in the global table --- netbox/ipam/models.py | 16 ++++++++++++---- netbox/ipam/views.py | 6 +++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index bdaec4fd8..9b30586f2 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -283,15 +283,23 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): def get_child_prefixes(self): """ - Return all Prefixes within this Prefix and VRF. + Return all Prefixes within this Prefix and VRF. If this Prefix is a container in the global table, return child + Prefixes belonging to any VRF. """ - return Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf) + if self.vrf is None and self.status == PREFIX_STATUS_CONTAINER: + return Prefix.objects.filter(prefix__net_contained=str(self.prefix)) + else: + return Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf) def get_child_ips(self): """ - Return all IPAddresses within this Prefix and VRF. + Return all IPAddresses within this Prefix and VRF. If this Prefix is a container in the global table, return + child IPAddresses belonging to any VRF. """ - return IPAddress.objects.filter(address__net_host_contained=str(self.prefix), vrf=self.vrf) + if self.vrf is None and self.status == PREFIX_STATUS_CONTAINER: + return IPAddress.objects.filter(address__net_host_contained=str(self.prefix)) + else: + return IPAddress.objects.filter(address__net_host_contained=str(self.prefix), vrf=self.vrf) def get_available_prefixes(self): """ diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 25475eec8..18e7ff7e5 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -491,11 +491,11 @@ class PrefixPrefixesView(View): prefix = get_object_or_404(Prefix.objects.all(), pk=pk) # Child prefixes table - child_prefixes = Prefix.objects.filter( - vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix) - ).select_related( + child_prefixes = prefix.get_child_prefixes().select_related( 'site', 'vlan', 'role', ).annotate_depth(limit=0) + + # Annotate available prefixes if child_prefixes: child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes) From 2bb0e65aea595b92f03d957246b76a372edddf7f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 30 Jan 2018 17:46:00 -0500 Subject: [PATCH 06/10] 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 @@ -