Browse Source

Merge branch 'develop' into develop-2.3

Jeremy Stretch 7 years ago
parent
commit
6b101d2c49

+ 2 - 1
netbox/circuits/tables.py

@@ -4,6 +4,7 @@ import django_tables2 as tables
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 from django_tables2.utils import Accessor
 from django_tables2.utils import Accessor
 
 
+from tenancy.tables import COL_TENANT
 from utilities.tables import BaseTable, ToggleColumn
 from utilities.tables import BaseTable, ToggleColumn
 from .models import Circuit, CircuitType, Provider
 from .models import Circuit, CircuitType, Provider
 
 
@@ -75,7 +76,7 @@ class CircuitTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     cid = tables.LinkColumn(verbose_name='ID')
     cid = tables.LinkColumn(verbose_name='ID')
     provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
     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_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side')
     termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side')
     termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side')
 
 

+ 30 - 1
netbox/dcim/filters.py

@@ -22,6 +22,10 @@ from .models import (
 
 
 
 
 class RegionFilter(django_filters.FilterSet):
 class RegionFilter(django_filters.FilterSet):
+    q = django_filters.CharFilter(
+        method='search',
+        label='Search',
+    )
     parent_id = django_filters.ModelMultipleChoiceFilter(
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         label='Parent region (ID)',
         label='Parent region (ID)',
@@ -37,6 +41,15 @@ class RegionFilter(django_filters.FilterSet):
         model = Region
         model = Region
         fields = ['name', 'slug']
         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):
 class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
     id__in = NumericInFilter(name='id', lookup_expr='in')
     id__in = NumericInFilter(name='id', lookup_expr='in')
@@ -623,6 +636,10 @@ class DeviceBayFilter(DeviceComponentFilterSet):
 
 
 
 
 class InventoryItemFilter(DeviceComponentFilterSet):
 class InventoryItemFilter(DeviceComponentFilterSet):
+    q = django_filters.CharFilter(
+        method='search',
+        label='Search',
+    )
     parent_id = django_filters.ModelMultipleChoiceFilter(
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=InventoryItem.objects.all(),
         queryset=InventoryItem.objects.all(),
         label='Parent inventory item (ID)',
         label='Parent inventory item (ID)',
@@ -641,7 +658,19 @@ class InventoryItemFilter(DeviceComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = InventoryItem
         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 VirtualChassisFilter(django_filters.FilterSet):
 class VirtualChassisFilter(django_filters.FilterSet):

+ 49 - 0
netbox/dcim/forms.py

@@ -92,6 +92,11 @@ class RegionCSVForm(forms.ModelForm):
         }
         }
 
 
 
 
+class RegionFilterForm(BootstrapMixin, forms.Form):
+    model = Site
+    q = forms.CharField(required=False, label='Search')
+
+
 #
 #
 # Sites
 # Sites
 #
 #
@@ -2212,6 +2217,50 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm):
         fields = ['name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']
         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 --'
+    )
+
+
 #
 #
 # Virtual chassis
 # Virtual chassis
 #
 #

+ 15 - 0
netbox/dcim/models.py

@@ -1558,6 +1558,10 @@ class InventoryItem(models.Model):
     discovered = models.BooleanField(default=False, verbose_name='Discovered')
     discovered = models.BooleanField(default=False, verbose_name='Discovered')
     description = models.CharField(max_length=100, blank=True)
     description = models.CharField(max_length=100, blank=True)
 
 
+    csv_headers = [
+        'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
+    ]
+
     class Meta:
     class Meta:
         ordering = ['device__id', 'parent__id', 'name']
         ordering = ['device__id', 'parent__id', 'name']
         unique_together = ['device', 'parent', 'name']
         unique_together = ['device', 'parent', 'name']
@@ -1568,6 +1572,17 @@ class InventoryItem(models.Model):
     def get_absolute_url(self):
     def get_absolute_url(self):
         return self.device.get_absolute_url()
         return self.device.get_absolute_url()
 
 
+    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
+        ])
+
 
 
 #
 #
 # Virtual chassis
 # Virtual chassis

+ 23 - 7
netbox/dcim/tables.py

@@ -3,11 +3,13 @@ from __future__ import unicode_literals
 import django_tables2 as tables
 import django_tables2 as tables
 from django_tables2.utils import Accessor
 from django_tables2.utils import Accessor
 
 
+from tenancy.tables import COL_TENANT
 from utilities.tables import BaseTable, ToggleColumn
 from utilities.tables import BaseTable, ToggleColumn
 from .models import (
 from .models import (
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
-    DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
-    PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site, VirtualChassis
+    DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, InventoryItem, Manufacturer, Platform,
+    PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
+    VirtualChassis,
 )
 )
 
 
 REGION_LINK = """
 REGION_LINK = """
@@ -147,7 +149,7 @@ class SiteTable(BaseTable):
     name = tables.LinkColumn()
     name = tables.LinkColumn()
     status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
     status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
     region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
     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):
     class Meta(BaseTable.Meta):
         model = Site
         model = Site
@@ -199,7 +201,7 @@ class RackTable(BaseTable):
     name = tables.LinkColumn()
     name = tables.LinkColumn()
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
     group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
     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)
     role = tables.TemplateColumn(RACK_ROLE)
     u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
     u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
 
 
@@ -223,7 +225,7 @@ class RackImportTable(BaseTable):
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
     group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
     group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
     facility_id = tables.Column(verbose_name='Facility ID')
     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)')
     u_height = tables.Column(verbose_name='Height (U)')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
@@ -396,7 +398,7 @@ class DeviceTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     name = tables.TemplateColumn(template_code=DEVICE_LINK)
     name = tables.TemplateColumn(template_code=DEVICE_LINK)
     status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
     status = tables.TemplateColumn(template_code=STATUS_LABEL, 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')])
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
     rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
     rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
     device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
     device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
@@ -423,7 +425,7 @@ class DeviceDetailTable(DeviceTable):
 class DeviceImportTable(BaseTable):
 class DeviceImportTable(BaseTable):
     name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
     name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
     status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
     status = tables.TemplateColumn(template_code=STATUS_LABEL, 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')
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
     rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
     rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
     position = tables.Column(verbose_name='Position')
     position = tables.Column(verbose_name='Position')
@@ -524,6 +526,20 @@ class InterfaceConnectionTable(BaseTable):
 
 
 
 
 #
 #
+# 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')
+
+
+#
 # Virtual chassis
 # Virtual chassis
 #
 #
 
 

+ 5 - 1
netbox/dcim/urls.py

@@ -199,9 +199,13 @@ urlpatterns = [
     url(r'^device-bays/rename/$', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
     url(r'^device-bays/rename/$', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
 
 
     # Inventory items
     # Inventory items
-    url(r'^devices/(?P<device>\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<pk>\d+)/edit/$', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
     url(r'^inventory-items/(?P<pk>\d+)/edit/$', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
     url(r'^inventory-items/(?P<pk>\d+)/delete/$', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
     url(r'^inventory-items/(?P<pk>\d+)/delete/$', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
+    url(r'^devices/(?P<device>\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),
 
 
     # Console/power/interface connections
     # Console/power/interface connections
     url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
     url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),

+ 43 - 0
netbox/dcim/views.py

@@ -125,6 +125,8 @@ class BulkDisconnectView(View):
 
 
 class RegionListView(ObjectListView):
 class RegionListView(ObjectListView):
     queryset = Region.objects.annotate(site_count=Count('sites'))
     queryset = Region.objects.annotate(site_count=Count('sites'))
+    filter = filters.RegionFilter
+    filter_form = forms.RegionFilterForm
     table = tables.RegionTable
     table = tables.RegionTable
     template_name = 'dcim/region_list.html'
     template_name = 'dcim/region_list.html'
 
 
@@ -2010,6 +2012,14 @@ class InterfaceConnectionsListView(ObjectListView):
 # Inventory items
 # 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, ObjectEditView):
 class InventoryItemEditView(PermissionRequiredMixin, ObjectEditView):
     permission_required = 'dcim.change_inventoryitem'
     permission_required = 'dcim.change_inventoryitem'
     model = InventoryItem
     model = InventoryItem
@@ -2020,6 +2030,9 @@ class InventoryItemEditView(PermissionRequiredMixin, ObjectEditView):
             obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
             obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
         return obj
         return obj
 
 
+    def get_return_url(self, request, obj):
+        return reverse('dcim:device_inventory', kwargs={'pk': obj.device.pk})
+
 
 
 class InventoryItemDeleteView(PermissionRequiredMixin, ObjectDeleteView):
 class InventoryItemDeleteView(PermissionRequiredMixin, ObjectDeleteView):
     permission_required = 'dcim.delete_inventoryitem'
     permission_required = 'dcim.delete_inventoryitem'
@@ -2169,3 +2182,33 @@ class VCMembershipEditView(PermissionRequiredMixin, ObjectEditView):
 class VCMembershipDeleteView(PermissionRequiredMixin, ObjectDeleteView):
 class VCMembershipDeleteView(PermissionRequiredMixin, ObjectDeleteView):
     permission_required = 'dcim.delete_vcmembership'
     permission_required = 'dcim.delete_vcmembership'
     model = VCMembership
     model = VCMembership
+    parent_field = 'device'
+
+    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
+    template_name = 'dcim/inventoryitem_bulk_delete.html'
+    default_return_url = 'dcim:inventoryitem_list'

+ 12 - 13
netbox/ipam/models.py

@@ -283,24 +283,23 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
 
 
     def get_child_prefixes(self):
     def get_child_prefixes(self):
         """
         """
-        Return all child Prefixes within this Prefix.
+        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)
-
-    def get_available_prefixes(self):
-        """
-        Return all available prefixes within this Prefix.
-        """
-        prefix = netaddr.IPSet(self.prefix)
-        child_prefixes = netaddr.IPSet([p.prefix for p in self.get_child_prefixes()])
-        available_prefixes = prefix - child_prefixes
-        return available_prefixes
+        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):
     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):
     def get_available_prefixes(self):
         """
         """

+ 7 - 6
netbox/ipam/tables.py

@@ -3,6 +3,7 @@ from __future__ import unicode_literals
 import django_tables2 as tables
 import django_tables2 as tables
 from django_tables2.utils import Accessor
 from django_tables2.utils import Accessor
 
 
+from tenancy.tables import COL_TENANT
 from utilities.tables import BaseTable, ToggleColumn
 from utilities.tables import BaseTable, ToggleColumn
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
 
 
@@ -131,9 +132,9 @@ VLANGROUP_ACTIONS = """
 
 
 TENANT_LINK = """
 TENANT_LINK = """
 {% if record.tenant %}
 {% if record.tenant %}
-    <a href="{% url 'tenancy:tenant' slug=record.tenant.slug %}">{{ record.tenant }}</a>
+    <a href="{% url 'tenancy:tenant' slug=record.tenant.slug %}" title="{{ record.tenant.description }}">{{ record.tenant }}</a>
 {% elif record.vrf.tenant %}
 {% elif record.vrf.tenant %}
-    <a href="{% url 'tenancy:tenant' slug=record.vrf.tenant.slug %}">{{ record.vrf.tenant }}</a>*
+    <a href="{% url 'tenancy:tenant' slug=record.vrf.tenant.slug %}" title="{{ record.vrf.tenant.description }}">{{ record.vrf.tenant }}</a>*
 {% else %}
 {% else %}
     &mdash;
     &mdash;
 {% endif %}
 {% endif %}
@@ -148,7 +149,7 @@ class VRFTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     name = tables.LinkColumn()
     name = tables.LinkColumn()
     rd = tables.Column(verbose_name='RD')
     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):
     class Meta(BaseTable.Meta):
         model = VRF
         model = VRF
@@ -239,7 +240,7 @@ class PrefixTable(BaseTable):
     prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}})
     prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}})
     status = tables.TemplateColumn(STATUS_LABEL)
     status = tables.TemplateColumn(STATUS_LABEL)
     vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
     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')])
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
     vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
     vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
     role = tables.TemplateColumn(PREFIX_ROLE_LINK)
     role = tables.TemplateColumn(PREFIX_ROLE_LINK)
@@ -268,7 +269,7 @@ class IPAddressTable(BaseTable):
     address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
     address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
     vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
     vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
     status = tables.TemplateColumn(STATUS_LABEL)
     status = tables.TemplateColumn(STATUS_LABEL)
-    tenant = tables.TemplateColumn(TENANT_LINK)
+    tenant = tables.TemplateColumn(template_code=TENANT_LINK)
     parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False)
     parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False)
     interface = tables.Column(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')
     vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
     group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
     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)
     status = tables.TemplateColumn(STATUS_LABEL)
     role = tables.TemplateColumn(VLAN_ROLE_LINK)
     role = tables.TemplateColumn(VLAN_ROLE_LINK)
 
 

+ 3 - 3
netbox/ipam/views.py

@@ -491,11 +491,11 @@ class PrefixPrefixesView(View):
         prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
         prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
 
 
         # Child prefixes table
         # 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',
             'site', 'vlan', 'role',
         ).annotate_depth(limit=0)
         ).annotate_depth(limit=0)
+
+        # Annotate available prefixes
         if child_prefixes:
         if child_prefixes:
             child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
             child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
 
 

+ 2 - 1
netbox/netbox/views.py

@@ -15,7 +15,7 @@ from circuits.tables import CircuitTable, ProviderTable
 from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter
 from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter
 from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site
 from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site
 from dcim.tables import DeviceDetailTable, DeviceTypeTable, RackTable, SiteTable
 from dcim.tables import DeviceDetailTable, DeviceTypeTable, RackTable, SiteTable
-from extras.models import TopologyMap, UserAction
+from extras.models import ReportResult, TopologyMap, UserAction
 from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
 from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
 from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
 from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
 from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
 from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
@@ -177,6 +177,7 @@ class HomeView(View):
             'search_form': SearchForm(),
             'search_form': SearchForm(),
             'stats': stats,
             'stats': stats,
             'topology_maps': TopologyMap.objects.filter(site__isnull=True),
             'topology_maps': TopologyMap.objects.filter(site__isnull=True),
+            'report_results': ReportResult.objects.order_by('-created')[:10],
             'recent_activity': UserAction.objects.select_related('user')[:50]
             'recent_activity': UserAction.objects.select_related('user')[:50]
         })
         })
 
 

+ 7 - 6
netbox/templates/dcim/device_inventory.html

@@ -64,13 +64,14 @@
                     {% endfor %}
                     {% endfor %}
                 </tbody>
                 </tbody>
             </table>
             </table>
+            {% if perms.dcim.add_inventoryitem %}
+                <div class="panel-footer text-right">
+                    <a href="{% url 'dcim:inventoryitem_add' device=device.pk %}" class="btn btn-primary btn-xs">
+                        <span class="fa fa-plus" aria-hidden="true"></span> Add Inventory Item
+                    </a>
+                </div>
+            {% endif %}
         </div>
         </div>
-        {% if perms.dcim.add_inventoryitem %}
-            <a href="{% url 'dcim:inventoryitem_add' device=device.pk %}" class="btn btn-success">
-                <span class="fa fa-plus" aria-hidden="true"></span>
-                Add Inventory Item
-            </a>
-        {% endif %}
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 1 - 1
netbox/templates/dcim/inc/inventoryitem.html

@@ -11,7 +11,7 @@
             <a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
             <a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
         {% endif %}
         {% endif %}
         {% if perms.dcim.delete_inventoryitem %}
         {% if perms.dcim.delete_inventoryitem %}
-            <a href="{% url 'dcim:inventoryitem_delete' pk=item.pk %}?return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
+            <a href="{% url 'dcim:inventoryitem_delete' pk=item.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
         {% endif %}
         {% endif %}
     </td>
     </td>
 </tr>
 </tr>

+ 5 - 0
netbox/templates/dcim/inventoryitem_bulk_delete.html

@@ -0,0 +1,5 @@
+{% extends 'utilities/obj_bulk_delete.html' %}
+
+{% block message_extra %}
+    <p class="text-center text-danger"><i class="fa fa-warning"></i> This will also delete all child inventory items of those listed.</p>
+{% endblock %}

+ 23 - 0
netbox/templates/dcim/inventoryitem_list.html

@@ -0,0 +1,23 @@
+{% extends '_base.html' %}
+{% load helpers %}
+
+{% block content %}
+<div class="pull-right">
+    {% if perms.dcim.add_devicetype %}
+        <a href="{% url 'dcim:inventoryitem_import' %}" class="btn btn-info">
+            <span class="fa fa-download" aria-hidden="true"></span>
+            Import inventory items
+        </a>
+    {% endif %}
+    {% include 'inc/export_button.html' with obj_type='inventory items' %}
+</div>
+<h1>{% block title %}Inventory Items{% endblock %}</h1>
+<div class="row">
+	<div class="col-md-9">
+        {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:inventoryitem_bulk_edit' bulk_delete_url='dcim:inventoryitem_bulk_delete' %}
+    </div>
+    <div class="col-md-3">
+		{% include 'inc/search_panel.html' %}
+    </div>
+</div>
+{% endblock %}

+ 4 - 1
netbox/templates/dcim/region_list.html

@@ -17,8 +17,11 @@
 </div>
 </div>
 <h1>{% block title %}Regions{% endblock %}</h1>
 <h1>{% block title %}Regions{% endblock %}</h1>
 <div class="row">
 <div class="row">
-	<div class="col-md-12">
+	<div class="col-md-9">
         {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %}
         {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %}
     </div>
     </div>
+	<div class="col-md-3">
+		{% include 'inc/search_panel.html' %}
+	</div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 2 - 2
netbox/templates/extras/inc/report_label.html

@@ -1,6 +1,6 @@
-{% if report.result.failed %}
+{% if result.failed %}
     <label class="label label-danger">Failed</label>
     <label class="label label-danger">Failed</label>
-{% elif report.result %}
+{% elif result %}
     <label class="label label-success">Passed</label>
     <label class="label label-success">Passed</label>
 {% else %}
 {% else %}
     <label class="label label-default">N/A</label>
     <label class="label label-default">N/A</label>

+ 1 - 1
netbox/templates/extras/report.html

@@ -22,7 +22,7 @@
             </form>
             </form>
         </div>
         </div>
     {% endif %}
     {% endif %}
-    <h1>{{ report.name }}{% include 'extras/inc/report_label.html' %}</h1>
+    <h1>{{ report.name }}{% include 'extras/inc/report_label.html' with result=report.result %}</h1>
     <div class="row">
     <div class="row">
         <div class="col-md-12">
         <div class="col-md-12">
             {% if report.description %}
             {% if report.description %}

+ 1 - 1
netbox/templates/extras/report_list.html

@@ -24,7 +24,7 @@
                                         <a href="{% url 'extras:report' name=report.full_name %}" name="report.{{ report.name }}"><strong>{{ report.name }}</strong></a>
                                         <a href="{% url 'extras:report' name=report.full_name %}" name="report.{{ report.name }}"><strong>{{ report.name }}</strong></a>
                                     </td>
                                     </td>
                                     <td>
                                     <td>
-                                        {% include 'extras/inc/report_label.html' %}
+                                        {% include 'extras/inc/report_label.html' with result=report.result %}
                                     </td>
                                     </td>
                                     <td>{{ report.description|default:"" }}</td>
                                     <td>{{ report.description|default:"" }}</td>
                                     {% if report.result %}
                                     {% if report.result %}

+ 15 - 0
netbox/templates/home.html

@@ -150,6 +150,21 @@
                 </div>
                 </div>
             {% endif %}
             {% endif %}
         </div>
         </div>
+        {% if report_results %}
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Reports</strong>
+                </div>
+                <table class="table table-hover panel-body">
+                    {% for result in report_results %}
+                        <span>
+                            <td><a href="{% url 'extras:report' name=result.report %}">{{ result.report }}</a></td>
+                            <td class="text-right"><span title="{{ result.created }}">{% include 'extras/inc/report_label.html' %}</span></td>
+                        </tr>
+                    {% endfor %}
+                </table>
+            </div>
+        {% endif %}
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading">
             <div class="panel-heading">
                 <strong>Recent Activity</strong>
                 <strong>Recent Activity</strong>

+ 11 - 1
netbox/templates/inc/nav_menu.html

@@ -104,7 +104,7 @@
                         </li>
                         </li>
                     </ul>
                     </ul>
                 </li>
                 </li>
-                <li class="dropdown{% if request.path|contains:'/dcim/device,/dcim/manufacturers/,/dcim/platforms/,-connections/' %} active{% endif %}">
+                <li class="dropdown{% if request.path|contains:'/dcim/device,/dcim/manufacturers/,/dcim/platforms/,-connections/,/dcim/inventory-items/' %} active{% endif %}">
                     <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
                     <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
                     <ul class="dropdown-menu">
                     <ul class="dropdown-menu">
                         <li class="dropdown-header">Devices</li>
                         <li class="dropdown-header">Devices</li>
@@ -156,6 +156,16 @@
                             <a href="{% url 'dcim:manufacturer_list' %}">Manufacturers</a>
                             <a href="{% url 'dcim:manufacturer_list' %}">Manufacturers</a>
                         </li>
                         </li>
                         <li class="divider"></li>
                         <li class="divider"></li>
+                        <li class="dropdown-header">Inventory</li>
+                        <li>
+                            {% if perms.dcim.add_inventoryitem %}
+                                <div class="buttons pull-right">
+                                    <a href="{% url 'dcim:inventoryitem_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
+                                </div>
+                            {% endif %}
+                            <a href="{% url 'dcim:inventoryitem_list' %}">Inventory Items</a>
+                        </li>
+                        <li class="divider"></li>
                         <li class="dropdown-header">Connections</li>
                         <li class="dropdown-header">Connections</li>
                         <li>
                         <li>
                             {% if perms.dcim.change_consoleport %}
                             {% if perms.dcim.change_consoleport %}

+ 2 - 1
netbox/templates/utilities/obj_bulk_delete.html

@@ -9,7 +9,8 @@
             <div class="panel panel-danger">
             <div class="panel panel-danger">
                 <div class="panel-heading"><strong>Confirm Bulk Deletion</strong></div>
                 <div class="panel-heading"><strong>Confirm Bulk Deletion</strong></div>
                 <div class="panel-body">
                 <div class="panel-body">
-                    <strong>Warning:</strong> The following operation will delete {{ table.rows|length }} {{ obj_type_plural }}. Please carefully review the {{ obj_type_plural }} to be deleted and confirm below.
+                    <p><strong>Warning:</strong> The following operation will delete {{ table.rows|length }} {{ obj_type_plural }}. Please carefully review the {{ obj_type_plural }} to be deleted and confirm below.</p>
+                    {% block message_extra %}{% endblock %}
                 </div>
                 </div>
             </div>
             </div>
         </div>
         </div>

+ 8 - 0
netbox/tenancy/tables.py

@@ -11,6 +11,14 @@ TENANTGROUP_ACTIONS = """
 {% endif %}
 {% endif %}
 """
 """
 
 
+COL_TENANT = """
+{% if record.tenant %}
+    <a href="{% url 'tenancy:tenant' slug=record.tenant.slug %}" title="{{ record.tenant.description }}">{{ record.tenant }}</a>
+{% else %}
+    &mdash;
+{% endif %}
+"""
+
 
 
 #
 #
 # Tenant groups
 # Tenant groups

+ 1 - 1
netbox/utilities/templatetags/helpers.py

@@ -114,7 +114,7 @@ def example_choices(field, arg=3):
         if len(examples) == arg:
         if len(examples) == arg:
             examples.append('etc.')
             examples.append('etc.')
             break
             break
-        if not id:
+        if not id or not label:
             continue
             continue
         examples.append(label)
         examples.append(label)
     return ', '.join(examples) or 'None'
     return ', '.join(examples) or 'None'

+ 3 - 2
netbox/virtualization/tables.py

@@ -4,6 +4,7 @@ import django_tables2 as tables
 from django_tables2.utils import Accessor
 from django_tables2.utils import Accessor
 
 
 from dcim.models import Interface
 from dcim.models import Interface
+from tenancy.tables import COL_TENANT
 from utilities.tables import BaseTable, ToggleColumn
 from utilities.tables import BaseTable, ToggleColumn
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 
 
@@ -24,7 +25,7 @@ VIRTUALMACHINE_STATUS = """
 """
 """
 
 
 VIRTUALMACHINE_ROLE = """
 VIRTUALMACHINE_ROLE = """
-<label class="label" style="background-color: #{{ record.role.color }}">{{ value }}</label>
+{% if record.role %}<label class="label" style="background-color: #{{ record.role.color }}">{{ value }}</label>{% else %}&mdash;{% endif %}
 """
 """
 
 
 VIRTUALMACHINE_PRIMARY_IP = """
 VIRTUALMACHINE_PRIMARY_IP = """
@@ -97,7 +98,7 @@ class VirtualMachineTable(BaseTable):
     status = tables.TemplateColumn(template_code=VIRTUALMACHINE_STATUS)
     status = tables.TemplateColumn(template_code=VIRTUALMACHINE_STATUS)
     cluster = tables.LinkColumn('virtualization:cluster', args=[Accessor('cluster.pk')])
     cluster = tables.LinkColumn('virtualization:cluster', args=[Accessor('cluster.pk')])
     role = tables.TemplateColumn(VIRTUALMACHINE_ROLE)
     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):
     class Meta(BaseTable.Meta):
         model = VirtualMachine
         model = VirtualMachine