Merge branch 'develop' into develop-2.3

This commit is contained in:
Jeremy Stretch 2018-01-31 11:13:17 -05:00
commit 6b101d2c49
25 changed files with 275 additions and 50 deletions

View File

@ -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')

View File

@ -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')
@ -623,6 +636,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)',
@ -641,7 +658,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 VirtualChassisFilter(django_filters.FilterSet):

View File

@ -92,6 +92,11 @@ class RegionCSVForm(forms.ModelForm):
}
class RegionFilterForm(BootstrapMixin, forms.Form):
model = Site
q = forms.CharField(required=False, label='Search')
#
# Sites
#
@ -2212,6 +2217,50 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm):
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
#

View File

@ -1558,6 +1558,10 @@ 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']
@ -1568,6 +1572,17 @@ class InventoryItem(models.Model):
def get_absolute_url(self):
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

View File

@ -3,11 +3,13 @@ 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,
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 = """
@ -147,7 +149,7 @@ class SiteTable(BaseTable):
name = tables.LinkColumn()
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
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
@ -199,7 +201,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')
@ -223,7 +225,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):
@ -396,7 +398,7 @@ class DeviceTable(BaseTable):
pk = ToggleColumn()
name = tables.TemplateColumn(template_code=DEVICE_LINK)
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')])
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
@ -423,7 +425,7 @@ class DeviceDetailTable(DeviceTable):
class DeviceImportTable(BaseTable):
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
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')
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
position = tables.Column(verbose_name='Position')
@ -523,6 +525,20 @@ class InterfaceConnectionTable(BaseTable):
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')
#
# Virtual chassis
#

View File

@ -199,9 +199,13 @@ urlpatterns = [
url(r'^device-bays/rename/$', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
# 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+)/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
url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),

View File

@ -125,6 +125,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'
@ -2010,6 +2012,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, ObjectEditView):
permission_required = 'dcim.change_inventoryitem'
model = InventoryItem
@ -2020,6 +2030,9 @@ class InventoryItemEditView(PermissionRequiredMixin, ObjectEditView):
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, ObjectDeleteView):
permission_required = 'dcim.delete_inventoryitem'
@ -2169,3 +2182,33 @@ class VCMembershipEditView(PermissionRequiredMixin, ObjectEditView):
class VCMembershipDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_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'

View File

@ -283,24 +283,23 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
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):
"""
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):
"""

View File

@ -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 %}
<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 %}
<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 %}
&mdash;
{% 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)

View File

@ -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)

View File

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

View File

@ -64,13 +64,14 @@
{% endfor %}
</tbody>
</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>
{% 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>
{% endblock %}

View File

@ -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>
{% endif %}
{% 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 %}
</td>
</tr>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -17,8 +17,11 @@
</div>
<h1>{% block title %}Regions{% endblock %}</h1>
<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' %}
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

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

View File

@ -22,7 +22,7 @@
</form>
</div>
{% 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="col-md-12">
{% if report.description %}

View File

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

View File

@ -150,6 +150,21 @@
</div>
{% endif %}
</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-heading">
<strong>Recent Activity</strong>

View File

@ -104,7 +104,7 @@
</li>
</ul>
</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>
<ul class="dropdown-menu">
<li class="dropdown-header">Devices</li>
@ -156,6 +156,16 @@
<a href="{% url 'dcim:manufacturer_list' %}">Manufacturers</a>
</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>
{% if perms.dcim.change_consoleport %}

View File

@ -9,7 +9,8 @@
<div class="panel panel-danger">
<div class="panel-heading"><strong>Confirm Bulk Deletion</strong></div>
<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>

View File

@ -11,6 +11,14 @@ TENANTGROUP_ACTIONS = """
{% 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

View File

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

View File

@ -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
@ -24,7 +25,7 @@ VIRTUALMACHINE_STATUS = """
"""
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 = """
@ -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