From afdb24610d8743260a2248ba7d9e455521755496 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Mar 2017 12:04:57 -0400 Subject: [PATCH 1/4] Initial work on global search --- netbox/netbox/forms.py | 40 +++++++++ netbox/netbox/urls.py | 5 +- netbox/netbox/views.py | 157 ++++++++++++++++++++++++++++++++++- netbox/secrets/views.py | 2 +- netbox/templates/home.html | 63 ++------------ netbox/templates/search.html | 74 +++++++++++++++++ 6 files changed, 278 insertions(+), 63 deletions(-) create mode 100644 netbox/netbox/forms.py create mode 100644 netbox/templates/search.html diff --git a/netbox/netbox/forms.py b/netbox/netbox/forms.py new file mode 100644 index 000000000..2842668b4 --- /dev/null +++ b/netbox/netbox/forms.py @@ -0,0 +1,40 @@ +from django import forms + +from utilities.forms import BootstrapMixin + + +OBJ_TYPE_CHOICES = ( + ('', 'All Objects'), + ('Circuits', ( + ('provider', 'Providers'), + ('circuit', 'Circuits'), + )), + ('DCIM', ( + ('site', 'Sites'), + ('rack', 'Racks'), + ('devicetype', 'Device types'), + ('device', 'Devices'), + )), + ('IPAM', ( + ('vrf', 'VRFs'), + ('aggregate', 'Aggregates'), + ('prefix', 'Prefixes'), + ('ipaddress', 'IP addresses'), + ('vlan', 'VLANs'), + )), + ('Secrets', ( + ('secret', 'Secrets'), + )), + ('Tenancy', ( + ('tenant', 'Tenants'), + )), +) + + +class SearchForm(BootstrapMixin, forms.Form): + q = forms.CharField( + label='Query' + ) + obj_type = forms.ChoiceField( + choices=OBJ_TYPE_CHOICES, required=False, label='Type' + ) diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index e7a76bed9..724ab3090 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -2,7 +2,7 @@ from django.conf import settings from django.conf.urls import include, url from django.contrib import admin -from netbox.views import APIRootView, home, handle_500, trigger_500 +from netbox.views import APIRootView, home, handle_500, SearchView, trigger_500 from users.views import login, logout @@ -10,8 +10,9 @@ handler500 = handle_500 _patterns = [ - # Default page + # Base views url(r'^$', home, name='home'), + url(r'^search/$', SearchView.as_view(), name='search'), # Login/logout url(r'^login/$', login, name='login'), diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index 17a1e19d2..12df44e6f 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -1,18 +1,122 @@ import sys -from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.reverse import reverse +from django.db.models import Count from django.shortcuts import render +from django.views.generic import View -from circuits.models import Provider, Circuit -from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection +from circuits.filters import CircuitFilter, ProviderFilter +from circuits.models import Circuit, Provider +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 DeviceTable, DeviceTypeTable, RackTable, SiteTable from extras.models import UserAction -from ipam.models import Aggregate, Prefix, IPAddress, VLAN, VRF +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 +from secrets.filters import SecretFilter from secrets.models import Secret +from secrets.tables import SecretTable +from tenancy.filters import TenantFilter from tenancy.models import Tenant +from tenancy.tables import TenantTable +from .forms import SearchForm + + +SEARCH_MAX_RESULTS = 15 +SEARCH_TYPES = { + # Circuits + 'provider': { + 'queryset': Provider.objects.annotate(count_circuits=Count('circuits')), + 'filter': ProviderFilter, + 'table': ProviderTable, + 'url': 'circuits:provider_list', + }, + 'circuit': { + 'queryset': Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related( + 'terminations__site' + ), + 'filter': CircuitFilter, + 'table': CircuitTable, + 'url': 'circuits:circuit_list', + }, + # DCIM + 'site': { + 'queryset': Site.objects.select_related('region', 'tenant'), + 'filter': SiteFilter, + 'table': SiteTable, + 'url': 'dcim:site_list', + }, + 'rack': { + 'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('devices__device_type').annotate(device_count=Count('devices', distinct=True)), + 'filter': RackFilter, + 'table': RackTable, + 'url': 'dcim:rack_list', + }, + 'devicetype': { + 'queryset': DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances')), + 'filter': DeviceTypeFilter, + 'table': DeviceTypeTable, + 'url': 'dcim:devicetype_list', + }, + 'device': { + 'queryset': Device.objects.select_related( + 'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6' + ), + 'filter': DeviceFilter, + 'table': DeviceTable, + 'url': 'dcim:device_list', + }, + # IPAM + 'vrf': { + 'queryset': VRF.objects.select_related('tenant'), + 'filter': VRFFilter, + 'table': VRFTable, + 'url': 'ipam:vrf_list', + }, + 'aggregate': { + 'queryset': Aggregate.objects.select_related('rir'), + 'filter': AggregateFilter, + 'table': AggregateTable, + 'url': 'ipam:aggregate_list', + }, + 'prefix': { + 'queryset': Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'), + 'filter': PrefixFilter, + 'table': PrefixTable, + 'url': 'ipam:prefix_list', + }, + 'ipaddress': { + 'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device'), + 'filter': IPAddressFilter, + 'table': IPAddressTable, + 'url': 'ipam:ipaddress_list', + }, + 'vlan': { + 'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes'), + 'filter': VLANFilter, + 'table': VLANTable, + 'url': 'ipam:vlan_list', + }, + # Secrets + 'secret': { + 'queryset': Secret.objects.select_related('role', 'device'), + 'filter': SecretFilter, + 'table': SecretTable, + 'url': 'secrets:secret_list', + }, + # Tenancy + 'tenant': { + 'queryset': Tenant.objects.select_related('group'), + 'filter': TenantFilter, + 'table': TenantTable, + 'url': 'tenancy:tenant_list', + }, +} def home(request): @@ -47,11 +151,56 @@ def home(request): } return render(request, 'home.html', { + 'search_form': SearchForm(), 'stats': stats, 'recent_activity': UserAction.objects.select_related('user')[:50] }) +class SearchView(View): + + def get(self, request): + + # No query + if 'q' not in request.GET: + return render(request, 'search.html', { + 'form': SearchForm(), + }) + + form = SearchForm(request.GET) + results = [] + + if form.is_valid(): + + # Searching for a single type of object + if form.cleaned_data['obj_type']: + obj_types = [form.cleaned_data['obj_type']] + # Searching all object types + else: + obj_types = SEARCH_TYPES.keys() + + for obj_type in obj_types: + queryset = SEARCH_TYPES[obj_type]['queryset'] + filter = SEARCH_TYPES[obj_type]['filter'] + table = SEARCH_TYPES[obj_type]['table'] + url = SEARCH_TYPES[obj_type]['url'] + filtered_queryset = filter({'q': form.cleaned_data['q']}, queryset=queryset).qs + total_count = filtered_queryset.count() + if total_count: + results.append({ + 'name': queryset.model._meta.verbose_name_plural, + 'table': table(filtered_queryset[:SEARCH_MAX_RESULTS]), + 'total': total_count, + 'url': '{}?q={}'.format(reverse(url), form.cleaned_data['q']) + }) + + return render(request, 'search.html', { + 'form': form, + 'results': results, + }) + + + class APIRootView(APIView): _ignore_model_permissions = True exclude_from_schema = True diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 8a1b35a6b..308e9de3d 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -48,7 +48,7 @@ class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): @method_decorator(login_required, name='dispatch') class SecretListView(ObjectListView): - queryset = Secret.objects.select_related('role').prefetch_related('device') + queryset = Secret.objects.select_related('role', 'device') filter = filters.SecretFilter filter_form = forms.SecretFilterForm table = tables.SecretTable diff --git a/netbox/templates/home.html b/netbox/templates/home.html index 043526332..b489d90e5 100644 --- a/netbox/templates/home.html +++ b/netbox/templates/home.html @@ -3,62 +3,13 @@ {% block content %}
diff --git a/netbox/templates/search.html b/netbox/templates/search.html new file mode 100644 index 000000000..aca275639 --- /dev/null +++ b/netbox/templates/search.html @@ -0,0 +1,74 @@ +{% extends '_base.html' %} +{% load form_helpers %} + +{% block content %} + {% if request.GET.q %} +
+
+ {# Compressed search form #} +
+ {{ form.q }} + {{ form.obj_type }} + +
+
+
+
+
+ {% for obj_type in results %} +

{{ obj_type.name|title }}

+ {% include 'table.html' with table=obj_type.table %} + {% if obj_type.total > obj_type.table.rows|length %} + + + All {{ obj_type.total }} results + + {% endif %} +
+ {% empty %} +

No results found

+ {% endfor %} +
+
+ {% if results %} +
+
+ Search Results +
+
+ {% for obj_type in results %} + + {{ obj_type.name|title }} + {{ obj_type.total }} + + {% endfor %} +
+
+ {% endif %} +
+
+ {% else %} + {# Larger search form #} +
+
+
+
+
+ Search +
+
+ {% render_form form %} +
+ +
+
+
+
+ {% endif %} +{% endblock %} From d04436aa0a0d1c8bf97a561c581a3279f0cfa7ed Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Mar 2017 14:22:27 -0400 Subject: [PATCH 2/4] Search form improvements --- netbox/netbox/forms.py | 2 +- netbox/templates/home.html | 10 +--- netbox/templates/search.html | 79 ++++++++++++++----------------- netbox/templates/search_form.html | 9 ++++ 4 files changed, 46 insertions(+), 54 deletions(-) create mode 100644 netbox/templates/search_form.html diff --git a/netbox/netbox/forms.py b/netbox/netbox/forms.py index 2842668b4..63af2e04b 100644 --- a/netbox/netbox/forms.py +++ b/netbox/netbox/forms.py @@ -33,7 +33,7 @@ OBJ_TYPE_CHOICES = ( class SearchForm(BootstrapMixin, forms.Form): q = forms.CharField( - label='Query' + label='Query', widget=forms.TextInput(attrs={'style': 'width: 350px'}) ) obj_type = forms.ChoiceField( choices=OBJ_TYPE_CHOICES, required=False, label='Type' diff --git a/netbox/templates/home.html b/netbox/templates/home.html index b489d90e5..15965b13f 100644 --- a/netbox/templates/home.html +++ b/netbox/templates/home.html @@ -2,15 +2,7 @@ {% load render_table from django_tables2 %} {% block content %} - +{% include 'search_form.html' %}
diff --git a/netbox/templates/search.html b/netbox/templates/search.html index aca275639..5e72e3396 100644 --- a/netbox/templates/search.html +++ b/netbox/templates/search.html @@ -3,52 +3,43 @@ {% block content %} {% if request.GET.q %} -
-
- {# Compressed search form #} -
- {{ form.q }} - {{ form.obj_type }} - -
-
-
-
-
- {% for obj_type in results %} -

{{ obj_type.name|title }}

- {% include 'table.html' with table=obj_type.table %} - {% if obj_type.total > obj_type.table.rows|length %} - - - All {{ obj_type.total }} results - + {% include 'search_form.html' with search_form=form %} + {% if results %} +
+
+ {% for obj_type in results %} +

{{ obj_type.name|title }}

+ {% include 'table.html' with table=obj_type.table %} + {% if obj_type.total > obj_type.table.rows|length %} + + + All {{ obj_type.total }} results + + {% endif %} +
+ {% endfor %} +
+
+ {% if results %} +
+
+ Search Results +
+
+ {% for obj_type in results %} + + {{ obj_type.name|title }} + {{ obj_type.total }} + + {% endfor %} +
+
{% endif %} -
- {% empty %} -

No results found

- {% endfor %} +
-
- {% if results %} -
-
- Search Results -
-
- {% for obj_type in results %} - - {{ obj_type.name|title }} - {{ obj_type.total }} - - {% endfor %} -
-
- {% endif %} -
-
+ {% else %} +

No results found

+ {% endif %} {% else %} {# Larger search form #}
diff --git a/netbox/templates/search_form.html b/netbox/templates/search_form.html new file mode 100644 index 000000000..b60d879b7 --- /dev/null +++ b/netbox/templates/search_form.html @@ -0,0 +1,9 @@ +
+
+
+ {{ search_form.q }} + {{ search_form.obj_type }} + +
+
+
From a5dc91c17514826d23ac5b278eabe558d0018d0b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Mar 2017 16:05:23 -0400 Subject: [PATCH 3/4] Introduced SearchTable for improved performance --- netbox/circuits/tables.py | 52 ++++++++++----- netbox/dcim/filters.py | 6 +- netbox/dcim/tables.py | 103 ++++++++++++++++++++++------- netbox/ipam/tables.py | 122 ++++++++++++++++++++++++----------- netbox/netbox/views.py | 69 ++++++++++---------- netbox/secrets/tables.py | 14 ++-- netbox/templates/search.html | 36 +++++------ netbox/templates/table.html | 4 +- netbox/tenancy/tables.py | 13 ++-- netbox/utilities/tables.py | 15 ++++- 10 files changed, 290 insertions(+), 144 deletions(-) diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index ab877a8ce..07e2c4477 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor -from utilities.tables import BaseTable, ToggleColumn +from utilities.tables import BaseTable, SearchTable, ToggleColumn from .models import Circuit, CircuitType, Provider @@ -19,9 +19,7 @@ CIRCUITTYPE_ACTIONS = """ class ProviderTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn('circuits:provider', args=[Accessor('slug')], verbose_name='Name') - asn = tables.Column(verbose_name='ASN') - account = tables.Column(verbose_name='Account') + name = tables.LinkColumn() circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits') class Meta(BaseTable.Meta): @@ -29,17 +27,25 @@ class ProviderTable(BaseTable): fields = ('pk', 'name', 'asn', 'account', 'circuit_count') +class ProviderSearchTable(SearchTable): + name = tables.LinkColumn() + + class Meta(SearchTable.Meta): + model = Provider + fields = ('name', 'asn', 'account') + + # # Circuit types # class CircuitTypeTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn(verbose_name='Name') + name = tables.LinkColumn() circuit_count = tables.Column(verbose_name='Circuits') - slug = tables.Column(verbose_name='Slug') - actions = tables.TemplateColumn(template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}}, - verbose_name='') + actions = tables.TemplateColumn( + template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='' + ) class Meta(BaseTable.Meta): model = CircuitType @@ -52,16 +58,28 @@ class CircuitTypeTable(BaseTable): class CircuitTable(BaseTable): pk = ToggleColumn() - cid = tables.LinkColumn('circuits:circuit', args=[Accessor('pk')], verbose_name='ID') - type = tables.Column(verbose_name='Type') - provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider') - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') - a_side = tables.LinkColumn('dcim:site', accessor=Accessor('termination_a.site'), orderable=False, - args=[Accessor('termination_a.site.slug')]) - z_side = tables.LinkColumn('dcim:site', accessor=Accessor('termination_z.site'), orderable=False, - args=[Accessor('termination_z.site.slug')]) - description = tables.Column(verbose_name='Description') + cid = tables.LinkColumn(verbose_name='ID') + provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')]) + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + a_side = tables.LinkColumn( + 'dcim:site', accessor=Accessor('termination_a.site'), orderable=False, + args=[Accessor('termination_a.site.slug')] + ) + z_side = tables.LinkColumn( + 'dcim:site', accessor=Accessor('termination_z.site'), orderable=False, + args=[Accessor('termination_z.site.slug')] + ) class Meta(BaseTable.Meta): model = Circuit fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description') + + +class CircuitSearchTable(SearchTable): + cid = tables.LinkColumn(verbose_name='ID') + provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')]) + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + + class Meta(SearchTable.Meta): + model = Circuit + fields = ('cid', 'type', 'provider', 'tenant', 'description') diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 9d9425f9c..6eab5ae34 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -8,9 +8,9 @@ from tenancy.models import Tenant from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter from .models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, - DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, InterfaceTemplate, - Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, - RackReservation, RackRole, Region, Site, VIRTUAL_IFACE_TYPES, + DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceTemplate, Manufacturer, InventoryItem, + Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, + RackRole, Region, Site, VIRTUAL_IFACE_TYPES, ) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index c4c7c5b73..b6ebb1be2 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor -from utilities.tables import BaseTable, ToggleColumn +from utilities.tables import BaseTable, SearchTable, ToggleColumn from .models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType, @@ -136,11 +136,9 @@ class RegionTable(BaseTable): class SiteTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name') - facility = tables.Column(verbose_name='Facility') - region = tables.TemplateColumn(template_code=SITE_REGION_LINK, verbose_name='Region') - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') - asn = tables.Column(verbose_name='ASN') + name = tables.LinkColumn() + region = tables.TemplateColumn(template_code=SITE_REGION_LINK) + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks') device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices') prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes') @@ -155,6 +153,16 @@ class SiteTable(BaseTable): ) +class SiteSearchTable(SearchTable): + name = tables.LinkColumn() + region = tables.TemplateColumn(template_code=SITE_REGION_LINK) + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + + class Meta(SearchTable.Meta): + model = Site + fields = ('name', 'facility', 'region', 'tenant', 'asn') + + # # Rack groups # @@ -197,20 +205,33 @@ class RackRoleTable(BaseTable): class RackTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn('dcim:rack', args=[Accessor('pk')], verbose_name='Name') - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') + name = tables.LinkColumn() + site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) 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') - role = tables.TemplateColumn(RACK_ROLE, verbose_name='Role') + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + role = tables.TemplateColumn(RACK_ROLE) u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') - devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices') + devices = tables.Column(accessor=Accessor('device_count')) get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization') class Meta(BaseTable.Meta): model = Rack - fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', - 'get_utilization') + fields = ( + 'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'get_utilization' + ) + + +class RackSearchTable(SearchTable): + 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')]) + role = tables.TemplateColumn(RACK_ROLE) + u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') + + class Meta(SearchTable.Meta): + model = Rack + fields = ('name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height') class RackImportTable(BaseTable): @@ -249,9 +270,7 @@ class ManufacturerTable(BaseTable): class DeviceTypeTable(BaseTable): pk = ToggleColumn() - manufacturer = tables.Column(verbose_name='Manufacturer') model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type') - part_number = tables.Column(verbose_name='Part Number') is_full_depth = tables.BooleanColumn(verbose_name='Full Depth') is_console_server = tables.BooleanColumn(verbose_name='CS') is_pdu = tables.BooleanColumn(verbose_name='PDU') @@ -267,6 +286,22 @@ class DeviceTypeTable(BaseTable): ) +class DeviceTypeSearchTable(SearchTable): + model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type') + is_full_depth = tables.BooleanColumn(verbose_name='Full Depth') + is_console_server = tables.BooleanColumn(verbose_name='CS') + is_pdu = tables.BooleanColumn(verbose_name='PDU') + is_network_device = tables.BooleanColumn(verbose_name='Net') + subdevice_role = tables.TemplateColumn(SUBDEVICE_ROLE_TEMPLATE, verbose_name='Subdevice Role') + + class Meta(SearchTable.Meta): + model = DeviceType + fields = ( + 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu', + 'is_network_device', 'subdevice_role', + ) + + # # Device type components # @@ -373,22 +408,42 @@ class PlatformTable(BaseTable): class DeviceTable(BaseTable): pk = ToggleColumn() + name = tables.TemplateColumn(template_code=DEVICE_LINK) status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='') - name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name') - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='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') + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + 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') - device_type = tables.LinkColumn('dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type', - text=lambda record: record.device_type.full_name) - primary_ip = tables.TemplateColumn(orderable=False, verbose_name='IP Address', - template_code=DEVICE_PRIMARY_IP) + device_type = tables.LinkColumn( + 'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type', + text=lambda record: record.device_type.full_name + ) + primary_ip = tables.TemplateColumn( + orderable=False, verbose_name='IP Address', template_code=DEVICE_PRIMARY_IP + ) class Meta(BaseTable.Meta): model = Device fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip') +class DeviceSearchTable(SearchTable): + name = tables.TemplateColumn(template_code=DEVICE_LINK) + status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='') + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + 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') + device_type = tables.LinkColumn( + 'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type', + text=lambda record: record.device_type.full_name + ) + + class Meta(SearchTable.Meta): + model = Device + fields = ('name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type') + + class DeviceImportTable(BaseTable): name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name') tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 6c99f7d9e..49f87d716 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor -from utilities.tables import BaseTable, ToggleColumn +from utilities.tables import BaseTable, SearchTable, ToggleColumn from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF @@ -133,16 +133,25 @@ TENANT_LINK = """ class VRFTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn('ipam:vrf', args=[Accessor('pk')], verbose_name='Name') + name = tables.LinkColumn() rd = tables.Column(verbose_name='RD') - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') - description = tables.Column(verbose_name='Description') + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) class Meta(BaseTable.Meta): model = VRF fields = ('pk', 'name', 'rd', 'tenant', 'description') +class VRFSearchTable(SearchTable): + name = tables.LinkColumn() + rd = tables.Column(verbose_name='RD') + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + + class Meta(SearchTable.Meta): + model = VRF + fields = ('name', 'rd', 'tenant', 'description') + + # # RIRs # @@ -177,18 +186,25 @@ class RIRTable(BaseTable): class AggregateTable(BaseTable): pk = ToggleColumn() - prefix = tables.LinkColumn('ipam:aggregate', args=[Accessor('pk')], verbose_name='Aggregate') - rir = tables.Column(verbose_name='RIR') + prefix = tables.LinkColumn(verbose_name='Aggregate') child_count = tables.Column(verbose_name='Prefixes') get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization') date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added') - description = tables.Column(verbose_name='Description') class Meta(BaseTable.Meta): model = Aggregate fields = ('pk', 'prefix', 'rir', 'child_count', 'get_utilization', 'date_added', 'description') +class AggregateSearchTable(SearchTable): + prefix = tables.LinkColumn(verbose_name='Aggregate') + date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added') + + class Meta(SearchTable.Meta): + model = Aggregate + fields = ('prefix', 'rir', 'date_added', 'description') + + # # Roles # @@ -212,14 +228,13 @@ class RoleTable(BaseTable): class PrefixTable(BaseTable): pk = ToggleColumn() - status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') - prefix = tables.TemplateColumn(PREFIX_LINK, verbose_name='Prefix', attrs={'th': {'style': 'padding-left: 17px'}}) + 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, verbose_name='Tenant') - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') + tenant = tables.TemplateColumn(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, verbose_name='Role') - description = tables.Column(verbose_name='Description') + role = tables.TemplateColumn(PREFIX_ROLE_LINK) class Meta(BaseTable.Meta): model = Prefix @@ -230,12 +245,11 @@ class PrefixTable(BaseTable): class PrefixBriefTable(BaseTable): - prefix = tables.TemplateColumn(PREFIX_LINK_BRIEF, verbose_name='Prefix') - vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF') - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') - status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') - vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN') - role = tables.Column(verbose_name='Role') + prefix = tables.TemplateColumn(PREFIX_LINK_BRIEF) + vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global') + site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) + status = tables.TemplateColumn(STATUS_LABEL) + vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')]) class Meta(BaseTable.Meta): model = Prefix @@ -243,6 +257,20 @@ class PrefixBriefTable(BaseTable): orderable = False +class PrefixSearchTable(SearchTable): + 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) + 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) + + class Meta(SearchTable.Meta): + model = Prefix + fields = ('prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description') + + # # IPAddresses # @@ -250,13 +278,11 @@ class PrefixBriefTable(BaseTable): class IPAddressTable(BaseTable): pk = ToggleColumn() address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address') - status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') + status = tables.TemplateColumn(STATUS_LABEL) vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') - tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant') - device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False, - verbose_name='Device') - interface = tables.Column(orderable=False, verbose_name='Interface') - description = tables.Column(verbose_name='Description') + tenant = tables.TemplateColumn(TENANT_LINK) + device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False) + interface = tables.Column(orderable=False) class Meta(BaseTable.Meta): model = IPAddress @@ -268,17 +294,30 @@ class IPAddressTable(BaseTable): class IPAddressBriefTable(BaseTable): address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address') - device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False, - verbose_name='Device') - interface = tables.Column(orderable=False, verbose_name='Interface') - nat_inside = tables.LinkColumn('ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, - verbose_name='NAT (Inside)') + device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False) + interface = tables.Column(orderable=False) + nat_inside = tables.LinkColumn( + 'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)' + ) class Meta(BaseTable.Meta): model = IPAddress fields = ('address', 'device', 'interface', 'nat_inside') +class IPAddressSearchTable(SearchTable): + address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address') + status = tables.TemplateColumn(STATUS_LABEL) + vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') + tenant = tables.TemplateColumn(TENANT_LINK) + device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False) + interface = tables.Column(orderable=False) + + class Meta(SearchTable.Meta): + model = IPAddress + fields = ('address', 'status', 'vrf', 'tenant', 'device', 'interface', 'description') + + # # VLAN groups # @@ -304,15 +343,26 @@ class VLANGroupTable(BaseTable): class VLANTable(BaseTable): pk = ToggleColumn() vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID') - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') + site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') - name = tables.Column(verbose_name='Name') prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes') - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') - status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') - role = tables.TemplateColumn(VLAN_ROLE_LINK, verbose_name='Role') - description = tables.Column(verbose_name='Description') + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) + status = tables.TemplateColumn(STATUS_LABEL) + role = tables.TemplateColumn(VLAN_ROLE_LINK) class Meta(BaseTable.Meta): model = VLAN fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description') + + +class VLANSearchTable(SearchTable): + 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')]) + status = tables.TemplateColumn(STATUS_LABEL) + role = tables.TemplateColumn(VLAN_ROLE_LINK) + + class Meta(SearchTable.Meta): + model = VLAN + fields = ('vid', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description') diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index 12df44e6f..8b4c91d2f 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -4,26 +4,25 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.reverse import reverse -from django.db.models import Count from django.shortcuts import render from django.views.generic import View from circuits.filters import CircuitFilter, ProviderFilter from circuits.models import Circuit, Provider -from circuits.tables import CircuitTable, ProviderTable +from circuits.tables import CircuitSearchTable, ProviderSearchTable from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site -from dcim.tables import DeviceTable, DeviceTypeTable, RackTable, SiteTable +from dcim.tables import DeviceSearchTable, DeviceTypeSearchTable, RackSearchTable, SiteSearchTable from extras.models import 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 +from ipam.tables import AggregateSearchTable, IPAddressSearchTable, PrefixSearchTable, VLANSearchTable, VRFSearchTable from secrets.filters import SecretFilter from secrets.models import Secret -from secrets.tables import SecretTable +from secrets.tables import SecretSearchTable from tenancy.filters import TenantFilter from tenancy.models import Tenant -from tenancy.tables import TenantTable +from tenancy.tables import TenantSearchTable from .forms import SearchForm @@ -31,89 +30,85 @@ SEARCH_MAX_RESULTS = 15 SEARCH_TYPES = { # Circuits 'provider': { - 'queryset': Provider.objects.annotate(count_circuits=Count('circuits')), + 'queryset': Provider.objects.all(), 'filter': ProviderFilter, - 'table': ProviderTable, + 'table': ProviderSearchTable, 'url': 'circuits:provider_list', }, 'circuit': { - 'queryset': Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related( - 'terminations__site' - ), + 'queryset': Circuit.objects.select_related('type', 'provider', 'tenant'), 'filter': CircuitFilter, - 'table': CircuitTable, + 'table': CircuitSearchTable, 'url': 'circuits:circuit_list', }, # DCIM 'site': { 'queryset': Site.objects.select_related('region', 'tenant'), 'filter': SiteFilter, - 'table': SiteTable, + 'table': SiteSearchTable, 'url': 'dcim:site_list', }, 'rack': { - 'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('devices__device_type').annotate(device_count=Count('devices', distinct=True)), + 'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role'), 'filter': RackFilter, - 'table': RackTable, + 'table': RackSearchTable, 'url': 'dcim:rack_list', }, 'devicetype': { - 'queryset': DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances')), + 'queryset': DeviceType.objects.select_related('manufacturer'), 'filter': DeviceTypeFilter, - 'table': DeviceTypeTable, + 'table': DeviceTypeSearchTable, 'url': 'dcim:devicetype_list', }, 'device': { - 'queryset': Device.objects.select_related( - 'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6' - ), + 'queryset': Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack'), 'filter': DeviceFilter, - 'table': DeviceTable, + 'table': DeviceSearchTable, 'url': 'dcim:device_list', }, # IPAM 'vrf': { 'queryset': VRF.objects.select_related('tenant'), 'filter': VRFFilter, - 'table': VRFTable, + 'table': VRFSearchTable, 'url': 'ipam:vrf_list', }, 'aggregate': { 'queryset': Aggregate.objects.select_related('rir'), 'filter': AggregateFilter, - 'table': AggregateTable, + 'table': AggregateSearchTable, 'url': 'ipam:aggregate_list', }, 'prefix': { 'queryset': Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'), 'filter': PrefixFilter, - 'table': PrefixTable, + 'table': PrefixSearchTable, 'url': 'ipam:prefix_list', }, 'ipaddress': { 'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device'), 'filter': IPAddressFilter, - 'table': IPAddressTable, + 'table': IPAddressSearchTable, 'url': 'ipam:ipaddress_list', }, 'vlan': { - 'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes'), + 'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role'), 'filter': VLANFilter, - 'table': VLANTable, + 'table': VLANSearchTable, 'url': 'ipam:vlan_list', }, # Secrets 'secret': { 'queryset': Secret.objects.select_related('role', 'device'), 'filter': SecretFilter, - 'table': SecretTable, + 'table': SecretSearchTable, 'url': 'secrets:secret_list', }, # Tenancy 'tenant': { 'queryset': Tenant.objects.select_related('group'), 'filter': TenantFilter, - 'table': TenantTable, + 'table': TenantSearchTable, 'url': 'tenancy:tenant_list', }, } @@ -180,17 +175,21 @@ class SearchView(View): obj_types = SEARCH_TYPES.keys() for obj_type in obj_types: + queryset = SEARCH_TYPES[obj_type]['queryset'] - filter = SEARCH_TYPES[obj_type]['filter'] + filter_cls = SEARCH_TYPES[obj_type]['filter'] table = SEARCH_TYPES[obj_type]['table'] url = SEARCH_TYPES[obj_type]['url'] - filtered_queryset = filter({'q': form.cleaned_data['q']}, queryset=queryset).qs - total_count = filtered_queryset.count() - if total_count: + + # Construct the results table for this object type + filtered_queryset = filter_cls({'q': form.cleaned_data['q']}, queryset=queryset).qs + table = table(filtered_queryset) + table.paginate(per_page=SEARCH_MAX_RESULTS) + + if table.page: results.append({ 'name': queryset.model._meta.verbose_name_plural, - 'table': table(filtered_queryset[:SEARCH_MAX_RESULTS]), - 'total': total_count, + 'table': table, 'url': '{}?q={}'.format(reverse(url), form.cleaned_data['q']) }) diff --git a/netbox/secrets/tables.py b/netbox/secrets/tables.py index 2fb6d9bbe..15e003d8f 100644 --- a/netbox/secrets/tables.py +++ b/netbox/secrets/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor -from utilities.tables import BaseTable, ToggleColumn +from utilities.tables import BaseTable, SearchTable, ToggleColumn from .models import SecretRole, Secret @@ -36,11 +36,15 @@ class SecretRoleTable(BaseTable): class SecretTable(BaseTable): pk = ToggleColumn() - device = tables.LinkColumn('secrets:secret', args=[Accessor('pk')], verbose_name='Device') - role = tables.Column(verbose_name='Role') - name = tables.Column(verbose_name='Name') - last_updated = tables.DateTimeColumn(verbose_name='Last updated') + device = tables.LinkColumn() class Meta(BaseTable.Meta): model = Secret fields = ('pk', 'device', 'role', 'name', 'last_updated') + + +class SecretSearchTable(SearchTable): + + class Meta(SearchTable.Meta): + model = Secret + fields = ('device', 'role', 'name', 'last_updated') diff --git a/netbox/templates/search.html b/netbox/templates/search.html index 5e72e3396..e0c60003a 100644 --- a/netbox/templates/search.html +++ b/netbox/templates/search.html @@ -1,6 +1,8 @@ {% extends '_base.html' %} {% load form_helpers %} +{% block title %}Search{% endblock %} + {% block content %} {% if request.GET.q %} {% include 'search_form.html' with search_form=form %} @@ -8,33 +10,31 @@
{% for obj_type in results %} -

{{ obj_type.name|title }}

- {% include 'table.html' with table=obj_type.table %} - {% if obj_type.total > obj_type.table.rows|length %} +

{{ obj_type.name }}

+ {% include 'table.html' with table=obj_type.table hide_paginator=True %} + {% if obj_type.table.page.has_next %} - All {{ obj_type.total }} results + All {{ obj_type.table.page.paginator.count }} results {% endif %}
{% endfor %}
- {% if results %} -
-
- Search Results -
-
- {% for obj_type in results %} - - {{ obj_type.name|title }} - {{ obj_type.total }} - - {% endfor %} -
+
+
+ Search Results
- {% endif %} +
+ {% for obj_type in results %} + + {{ obj_type.name }} + {{ obj_type.table.page.paginator.count }} + + {% endfor %} +
+
{% else %} diff --git a/netbox/templates/table.html b/netbox/templates/table.html index 8782f0796..4792f8e68 100644 --- a/netbox/templates/table.html +++ b/netbox/templates/table.html @@ -4,5 +4,7 @@ {# Extends the stock django_tables2 template to provide custom formatting of the pagination controls #} {% block pagination %} - {% include 'paginator.html' %} + {% if not hide_paginator %} + {% include 'paginator.html' %} + {% endif %} {% endblock pagination %} diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 0ce0d577c..bacb4c12f 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor -from utilities.tables import BaseTable, ToggleColumn +from utilities.tables import BaseTable, SearchTable, ToggleColumn from .models import Tenant, TenantGroup @@ -36,10 +36,15 @@ class TenantGroupTable(BaseTable): class TenantTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn('tenancy:tenant', args=[Accessor('slug')], verbose_name='Name') - group = tables.Column(verbose_name='Group') - description = tables.Column(verbose_name='Description') + name = tables.LinkColumn() class Meta(BaseTable.Meta): model = Tenant fields = ('pk', 'name', 'group', 'description') + + +class TenantSearchTable(SearchTable): + + class Meta(SearchTable.Meta): + model = Tenant + fields = ('name', 'group', 'description') diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index a53227937..279a7310b 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -4,7 +4,9 @@ from django.utils.safestring import mark_safe class BaseTable(tables.Table): - + """ + Default table for object lists + """ def __init__(self, *args, **kwargs): super(BaseTable, self).__init__(*args, **kwargs) @@ -18,6 +20,17 @@ class BaseTable(tables.Table): } +class SearchTable(tables.Table): + """ + Default table for search results + """ + class Meta: + attrs = { + 'class': 'table table-hover', + } + orderable = False + + class ToggleColumn(tables.CheckBoxColumn): def __init__(self, *args, **kwargs): From 66615f1a96826c2cf7500e8e9ef4ff0ee6993890 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Mar 2017 16:45:25 -0400 Subject: [PATCH 4/4] Prettied things up a bit --- netbox/netbox/views.py | 1 - netbox/project-static/css/base.css | 3 +++ netbox/templates/panel_table.html | 5 +++-- netbox/templates/search.html | 14 +++++++------- netbox/utilities/tables.py | 2 +- netbox/utilities/templatetags/helpers.py | 7 +++++++ 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index 8b4c91d2f..0e0b9e50c 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -199,7 +199,6 @@ class SearchView(View): }) - class APIRootView(APIView): _ignore_model_permissions = True exclude_from_schema = True diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index 11ea04b72..db37c3535 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -92,6 +92,9 @@ tfoot td { table.attr-table td:nth-child(1) { width: 25%; } +.table-headings th { + background-color: #f5f5f5; +} /* Paginator */ div.paginator { diff --git a/netbox/templates/panel_table.html b/netbox/templates/panel_table.html index 0d44d7ea9..cb807eeb6 100644 --- a/netbox/templates/panel_table.html +++ b/netbox/templates/panel_table.html @@ -20,6 +20,7 @@ {% endblock %} {% block pagination %} - {% include 'paginator.html' %} + {% if not hide_paginator %} + {% include 'paginator.html' %} + {% endif %} {% endblock pagination %} - diff --git a/netbox/templates/search.html b/netbox/templates/search.html index e0c60003a..afd4293ca 100644 --- a/netbox/templates/search.html +++ b/netbox/templates/search.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load helpers %} {% load form_helpers %} {% block title %}Search{% endblock %} @@ -10,12 +11,12 @@
{% for obj_type in results %} -

{{ obj_type.name }}

- {% include 'table.html' with table=obj_type.table hide_paginator=True %} +

{{ obj_type.name|bettertitle }}

+ {% include 'panel_table.html' with table=obj_type.table hide_paginator=True %} {% if obj_type.table.page.has_next %} - - All {{ obj_type.table.page.paginator.count }} results + + See all {{ obj_type.table.page.paginator.count }} results {% endif %}
@@ -28,8 +29,8 @@
{% for obj_type in results %} - - {{ obj_type.name }} + + {{ obj_type.name|bettertitle }} {{ obj_type.table.page.paginator.count }} {% endfor %} @@ -41,7 +42,6 @@

No results found

{% endif %} {% else %} - {# Larger search form #}
diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index 279a7310b..1c5eab2a6 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -26,7 +26,7 @@ class SearchTable(tables.Table): """ class Meta: attrs = { - 'class': 'table table-hover', + 'class': 'table table-hover table-headings', } orderable = False diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 164aa24b2..3c5770cb3 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -51,6 +51,13 @@ def startswith(value, arg): """ return str(value).startswith(arg) +@register.filter() +def bettertitle(value): + """ + Alternative to the builtin title(); uppercases words without replacing letters that are already uppercase. + """ + return ' '.join([w[0].upper() + w[1:] for w in value.split()]) + # # Tags