Initial work on new search backend

This commit is contained in:
jeremystretch 2022-10-10 15:54:41 -04:00
parent bd79a27e4d
commit 80bf6e7b2c
18 changed files with 502 additions and 255 deletions

View File

@ -1,34 +1,66 @@
import circuits.filtersets
import circuits.tables
from circuits.models import Circuit, Provider, ProviderNetwork
from circuits import filtersets, models
from netbox.search import SearchIndex, register_search
from utilities.utils import count_related
@register_search()
class ProviderIndex(SearchIndex):
model = Provider
queryset = Provider.objects.annotate(count_circuits=count_related(Circuit, 'provider'))
filterset = circuits.filtersets.ProviderFilterSet
table = circuits.tables.ProviderTable
url = 'circuits:provider_list'
class CircuitIndex(SearchIndex):
model = models.Circuit
fields = (
('cid', 100),
('description', 500),
('comments', 1000),
)
queryset = models.Circuit.objects.prefetch_related(
'type', 'provider', 'tenant', 'tenant__group', 'terminations__site'
)
filterset = filtersets.CircuitFilterSet
@register_search()
class CircuitIndex(SearchIndex):
model = Circuit
queryset = Circuit.objects.prefetch_related(
'type', 'provider', 'tenant', 'tenant__group', 'terminations__site'
class CircuitTerminationIndex(SearchIndex):
model = models.CircuitTermination
fields = (
('xconnect_id', 300),
('pp_info', 300),
('description', 500),
('port_speed', 2000),
('upstream_speed', 2000),
)
filterset = circuits.filtersets.CircuitFilterSet
table = circuits.tables.CircuitTable
url = 'circuits:circuit_list'
@register_search()
class CircuitTypeIndex(SearchIndex):
model = models.CircuitType
fields = (
('name', 100),
('slug', 100),
('description', 500),
)
@register_search()
class ProviderIndex(SearchIndex):
model = models.Provider
fields = (
('name', 100),
('account', 200),
('comments', 1000),
)
queryset = models.Provider.objects.annotate(
count_circuits=count_related(models.Circuit, 'provider')
)
filterset = filtersets.ProviderFilterSet
@register_search()
class ProviderNetworkIndex(SearchIndex):
model = ProviderNetwork
queryset = ProviderNetwork.objects.prefetch_related('provider')
filterset = circuits.filtersets.ProviderNetworkFilterSet
table = circuits.tables.ProviderNetworkTable
url = 'circuits:providernetwork_list'
model = models.ProviderNetwork
fields = (
('name', 100),
('service_id', 200),
('description', 500),
('comments', 1000),
)
queryset = models.ProviderNetwork.objects.prefetch_related('provider')
filterset = filtersets.ProviderNetworkFilterSet

View File

@ -1,81 +1,28 @@
import dcim.filtersets
import dcim.tables
from dcim.models import (
Cable,
Device,
DeviceType,
Location,
Module,
ModuleType,
PowerFeed,
Rack,
RackReservation,
Site,
VirtualChassis,
)
from dcim import filtersets, models
from netbox.search import SearchIndex, register_search
from utilities.utils import count_related
@register_search()
class SiteIndex(SearchIndex):
model = Site
queryset = Site.objects.prefetch_related('region', 'tenant', 'tenant__group')
filterset = dcim.filtersets.SiteFilterSet
table = dcim.tables.SiteTable
url = 'dcim:site_list'
@register_search()
class RackIndex(SearchIndex):
model = Rack
queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate(
device_count=count_related(Device, 'rack')
class CableIndex(SearchIndex):
model = models.Cable
fields = (
('label', 100),
)
filterset = dcim.filtersets.RackFilterSet
table = dcim.tables.RackTable
url = 'dcim:rack_list'
@register_search()
class RackReservationIndex(SearchIndex):
model = RackReservation
queryset = RackReservation.objects.prefetch_related('rack', 'user')
filterset = dcim.filtersets.RackReservationFilterSet
table = dcim.tables.RackReservationTable
url = 'dcim:rackreservation_list'
@register_search()
class LocationIndex(SearchIndex):
model = Location
queryset = Location.objects.add_related_count(
Location.objects.add_related_count(Location.objects.all(), Device, 'location', 'device_count', cumulative=True),
Rack,
'location',
'rack_count',
cumulative=True,
).prefetch_related('site')
filterset = dcim.filtersets.LocationFilterSet
table = dcim.tables.LocationTable
url = 'dcim:location_list'
@register_search()
class DeviceTypeIndex(SearchIndex):
model = DeviceType
queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
instance_count=count_related(Device, 'device_type')
)
filterset = dcim.filtersets.DeviceTypeFilterSet
table = dcim.tables.DeviceTypeTable
url = 'dcim:devicetype_list'
queryset = models.Cable.objects.all()
filterset = filtersets.CableFilterSet
@register_search()
class DeviceIndex(SearchIndex):
model = Device
queryset = Device.objects.prefetch_related(
model = models.Device
fields = (
('asset_tag', 50),
('serial', 60),
('name', 100),
('comments', 1000),
)
queryset = models.Device.objects.prefetch_related(
'device_type__manufacturer',
'device_role',
'tenant',
@ -85,59 +32,162 @@ class DeviceIndex(SearchIndex):
'primary_ip4',
'primary_ip6',
)
filterset = dcim.filtersets.DeviceFilterSet
table = dcim.tables.DeviceTable
url = 'dcim:device_list'
filterset = filtersets.DeviceFilterSet
@register_search()
class ModuleTypeIndex(SearchIndex):
model = ModuleType
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
instance_count=count_related(Module, 'module_type')
class DeviceRoleIndex(SearchIndex):
model = models.DeviceRole
fields = (
('name', 100),
('slug', 100),
('description', 500),
)
filterset = dcim.filtersets.ModuleTypeFilterSet
table = dcim.tables.ModuleTypeTable
url = 'dcim:moduletype_list'
@register_search()
class DeviceTypeIndex(SearchIndex):
model = models.DeviceType
fields = (
('model', 100),
('part_number', 200),
('comments', 1000),
)
queryset = models.DeviceType.objects.prefetch_related('manufacturer').annotate(
instance_count=count_related(models.Device, 'device_type')
)
filterset = filtersets.DeviceTypeFilterSet
@register_search()
class LocationIndex(SearchIndex):
model = models.Location
fields = (
('name', 100),
('slug', 100),
('description', 500),
)
queryset = models.Location.objects.add_related_count(
models.Location.objects.add_related_count(
models.Location.objects.all(), models.Device, 'location', 'device_count', cumulative=True
),
models.Rack,
'location',
'rack_count',
cumulative=True,
).prefetch_related('site')
filterset = filtersets.LocationFilterSet
@register_search()
class ModuleIndex(SearchIndex):
model = Module
queryset = Module.objects.prefetch_related(
model = models.Module
fields = (
('asset_tag', 50),
('serial', 60),
('comments', 1000),
)
queryset = models.Module.objects.prefetch_related(
'module_type__manufacturer',
'device',
'module_bay',
)
filterset = dcim.filtersets.ModuleFilterSet
table = dcim.tables.ModuleTable
url = 'dcim:module_list'
filterset = filtersets.ModuleFilterSet
@register_search()
class VirtualChassisIndex(SearchIndex):
model = VirtualChassis
queryset = VirtualChassis.objects.prefetch_related('master').annotate(
member_count=count_related(Device, 'virtual_chassis')
class ModuleTypeIndex(SearchIndex):
model = models.ModuleType
fields = (
('model', 100),
('part_number', 200),
('comments', 1000),
)
filterset = dcim.filtersets.VirtualChassisFilterSet
table = dcim.tables.VirtualChassisTable
url = 'dcim:virtualchassis_list'
@register_search()
class CableIndex(SearchIndex):
model = Cable
queryset = Cable.objects.all()
filterset = dcim.filtersets.CableFilterSet
table = dcim.tables.CableTable
url = 'dcim:cable_list'
queryset = models.ModuleType.objects.prefetch_related('manufacturer').annotate(
instance_count=count_related(models.Module, 'module_type')
)
filterset = filtersets.ModuleTypeFilterSet
@register_search()
class PowerFeedIndex(SearchIndex):
model = PowerFeed
queryset = PowerFeed.objects.all()
filterset = dcim.filtersets.PowerFeedFilterSet
table = dcim.tables.PowerFeedTable
url = 'dcim:powerfeed_list'
model = models.PowerFeed
fields = (
('name', 100),
('comments', 1000),
)
queryset = models.PowerFeed.objects.all()
filterset = filtersets.PowerFeedFilterSet
@register_search()
class RackIndex(SearchIndex):
model = models.Rack
fields = (
('asset_tag', 50),
('serial', 60),
('name', 100),
('facility_id', 100),
('comments', 1000),
)
queryset = models.Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate(
device_count=count_related(models.Device, 'rack')
)
filterset = filtersets.RackFilterSet
@register_search()
class RackReservationIndex(SearchIndex):
model = models.RackReservation
fields = (
('description', 500),
)
queryset = models.RackReservation.objects.prefetch_related('rack', 'user')
filterset = filtersets.RackReservationFilterSet
@register_search()
class RegionIndex(SearchIndex):
model = models.Region
fields = (
('name', 100),
('slug', 100),
('description', 500)
)
@register_search()
class SiteIndex(SearchIndex):
model = models.Site
fields = (
('name', 100),
('facility', 100),
('description', 500),
('physical_address', 1000),
('shipping_address', 1000),
)
queryset = models.Site.objects.prefetch_related('region', 'tenant', 'tenant__group')
filterset = filtersets.SiteFilterSet
@register_search()
class SiteGroupIndex(SearchIndex):
model = models.SiteGroup
fields = (
('name', 100),
('slug', 100),
('description', 500)
)
@register_search()
class VirtualChassisIndex(SearchIndex):
model = models.VirtualChassis
fields = (
('name', 100),
('domain', 300)
)
queryset = models.VirtualChassis.objects.prefetch_related('master').annotate(
member_count=count_related(models.Device, 'virtual_chassis')
)
filterset = filtersets.VirtualChassisFilterSet

View File

@ -0,0 +1,25 @@
from django.core.management.base import BaseCommand
from extras.models import CachedValue
from extras.registry import registry
from netbox.search.backends import search_backend
class Command(BaseCommand):
"""Reindex cached search values"""
help = 'Reindex cached search values.'
def handle(self, *args, **kwargs):
self.stdout.write('Clearing cached values...', ending="\n")
CachedValue.objects.all().delete()
for app_label, models in registry['search'].items():
for name, idx in models.items():
self.stdout.write(f'Reindexing {app_label}.{name}...', ending="\n")
model = idx.model
for instance in model.objects.all():
search_backend.cache(model, instance)
cache_size = CachedValue.objects.count()
self.stdout.write(f'Done. Generated {cache_size} cached values', ending="\n")

View File

@ -0,0 +1,31 @@
# Generated by Django 4.1.1 on 2022-10-10 18:22
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('extras', '0078_unique_constraints'),
]
operations = [
migrations.CreateModel(
name='CachedValue',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('timestamp', models.DateTimeField(auto_now_add=True)),
('object_id', models.PositiveBigIntegerField()),
('field', models.CharField(max_length=200)),
('type', models.CharField(max_length=30)),
('value', models.TextField()),
('weight', models.PositiveSmallIntegerField(default=1000)),
('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')),
],
options={
'ordering': ('weight', 'pk'),
},
),
]

View File

@ -2,9 +2,11 @@ from .change_logging import ObjectChange
from .configcontexts import ConfigContext, ConfigContextModel
from .customfields import CustomField
from .models import *
from .search import *
from .tags import Tag, TaggedItem
__all__ = (
'CachedValue',
'ConfigContext',
'ConfigContextModel',
'ConfigRevision',

View File

@ -0,0 +1,40 @@
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
__all__ = (
'CachedValue',
)
class CachedValue(models.Model):
timestamp = models.DateTimeField(
auto_now_add=True,
editable=False
)
object_type = models.ForeignKey(
to=ContentType,
on_delete=models.CASCADE,
related_name='+'
)
object_id = models.PositiveBigIntegerField()
object = GenericForeignKey(
ct_field='object_type',
fk_field='object_id'
)
field = models.CharField(
max_length=200
)
type = models.CharField(
max_length=30
)
value = models.TextField()
weight = models.PositiveSmallIntegerField(
default=1000
)
class Meta:
ordering = ('weight', 'pk')
def __str__(self):
return f'{self.object_type} {self.object_id}: {self.field}={self.value}'

View File

@ -7,8 +7,9 @@ from netbox.search import SearchIndex, register_search
@register_search()
class JournalEntryIndex(SearchIndex):
model = JournalEntry
fields = (
('comments', 1000),
)
queryset = JournalEntry.objects.prefetch_related('assigned_object', 'created_by')
filterset = extras.filtersets.JournalEntryFilterSet
table = extras.tables.JournalEntryTable
url = 'extras:journalentry_list'
category = 'Journal'

View File

@ -5,7 +5,6 @@ from .models import DummyModel
class DummyModelIndex(SearchIndex):
model = DummyModel
queryset = DummyModel.objects.all()
url = 'plugins:dummy_plugin:dummy_models'
indexes = (

View File

@ -4,66 +4,84 @@ from ipam.models import ASN, VLAN, VRF, Aggregate, IPAddress, Prefix, Service
from netbox.search import SearchIndex, register_search
@register_search()
class VRFIndex(SearchIndex):
model = VRF
queryset = VRF.objects.prefetch_related('tenant', 'tenant__group')
filterset = ipam.filtersets.VRFFilterSet
table = ipam.tables.VRFTable
url = 'ipam:vrf_list'
@register_search()
class AggregateIndex(SearchIndex):
model = Aggregate
fields = (
('prefix', 100),
('description', 500),
('date_added', 2000),
)
queryset = Aggregate.objects.prefetch_related('rir')
filterset = ipam.filtersets.AggregateFilterSet
table = ipam.tables.AggregateTable
url = 'ipam:aggregate_list'
@register_search()
class PrefixIndex(SearchIndex):
model = Prefix
queryset = Prefix.objects.prefetch_related(
'site', 'vrf__tenant', 'tenant', 'tenant__group', 'vlan', 'role'
)
filterset = ipam.filtersets.PrefixFilterSet
table = ipam.tables.PrefixTable
url = 'ipam:prefix_list'
@register_search()
class IPAddressIndex(SearchIndex):
model = IPAddress
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group')
filterset = ipam.filtersets.IPAddressFilterSet
table = ipam.tables.IPAddressTable
url = 'ipam:ipaddress_list'
@register_search()
class VLANIndex(SearchIndex):
model = VLAN
queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'tenant__group', 'role')
filterset = ipam.filtersets.VLANFilterSet
table = ipam.tables.VLANTable
url = 'ipam:vlan_list'
@register_search()
class ASNIndex(SearchIndex):
model = ASN
fields = (
('asn', 100),
('description', 500),
)
queryset = ASN.objects.prefetch_related('rir', 'tenant', 'tenant__group')
filterset = ipam.filtersets.ASNFilterSet
table = ipam.tables.ASNTable
url = 'ipam:asn_list'
@register_search()
class IPAddressIndex(SearchIndex):
model = IPAddress
fields = (
('address', 100),
('dns_name', 300),
('description', 500),
)
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group')
filterset = ipam.filtersets.IPAddressFilterSet
@register_search()
class PrefixIndex(SearchIndex):
model = Prefix
fields = (
('prefix', 100),
('description', 500),
)
queryset = Prefix.objects.prefetch_related(
'site', 'vrf__tenant', 'tenant', 'tenant__group', 'vlan', 'role'
)
filterset = ipam.filtersets.PrefixFilterSet
@register_search()
class ServiceIndex(SearchIndex):
model = Service
fields = (
('name', 100),
('description', 500),
)
queryset = Service.objects.prefetch_related('device', 'virtual_machine')
filterset = ipam.filtersets.ServiceFilterSet
table = ipam.tables.ServiceTable
url = 'ipam:service_list'
@register_search()
class VLANIndex(SearchIndex):
model = VLAN
fields = (
('name', 100),
('vid', 100),
('description', 500),
)
queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'tenant__group', 'role')
filterset = ipam.filtersets.VLANFilterSet
@register_search()
class VRFIndex(SearchIndex):
model = VRF
fields = (
('name', 100),
('rd', 200),
('description', 500),
)
queryset = VRF.objects.prefetch_related('tenant', 'tenant__group')
filterset = ipam.filtersets.VRFFilterSet

View File

@ -1,6 +1,6 @@
from django import forms
from netbox.search.backends import default_search_engine
from netbox.search.backends import search_backend
from utilities.forms import BootstrapMixin
from .base import *
@ -26,13 +26,13 @@ class SearchForm(BootstrapMixin, forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["obj_type"] = forms.ChoiceField(
choices=default_search_engine.get_search_choices(),
choices=search_backend.get_search_choices(),
required=False,
label='Type'
)
def get_options(self):
if not self.options:
self.options = build_options(default_search_engine.get_search_choices())
self.options = build_options(search_backend.get_search_choices())
return self.options

View File

@ -9,6 +9,7 @@ from django.db import models
from taggit.managers import TaggableManager
from extras.choices import CustomFieldVisibilityChoices, ObjectChangeActionChoices
from extras.registry import registry
from extras.utils import is_taggable, register_features
from netbox.signals import post_clean
from utilities.json import CustomFieldJSONEncoder

View File

@ -9,6 +9,7 @@ class SearchIndex:
model: The model class for which this index is used.
"""
model = None
fields = ()
@classmethod
def get_category(cls):
@ -19,6 +20,13 @@ class SearchIndex:
return cls.category
return cls.model._meta.app_config.verbose_name
@classmethod
def to_cache(cls, instance):
return [
(field, str(getattr(instance, field)), weight)
for field, weight in cls.fields
]
def register_search():
def _wrapper(cls):

View File

@ -2,9 +2,11 @@ from collections import defaultdict
from importlib import import_module
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured
from django.urls import reverse
from django.db.models.signals import post_save
from extras.models import CachedValue
from extras.registry import registry
from netbox.constants import SEARCH_MAX_RESULTS
@ -12,6 +14,13 @@ from netbox.constants import SEARCH_MAX_RESULTS
_backends_cache = {}
def get_indexer(model):
app_label = model._meta.app_label
model_name = model._meta.model_name
return registry['search'][app_label][model_name]
class SearchEngineError(Exception):
"""Something went wrong with a search engine."""
pass
@ -21,6 +30,11 @@ class SearchBackend:
"""A search engine capable of performing multi-table searches."""
_search_choice_options = tuple()
def __init__(self):
# Connect cache handler to the model post-save signal
post_save.connect(self.cache)
def get_registry(self):
r = {}
for app_label, models in registry['search'].items():
@ -53,7 +67,8 @@ class SearchBackend:
"""Execute a search query for the given value."""
raise NotImplementedError
def cache(self, instance):
@staticmethod
def cache(sender, instance, **kwargs):
"""Create or update the cached copy of an instance."""
raise NotImplementedError
@ -70,7 +85,6 @@ class FilterSetSearchBackend(SearchBackend):
for obj_type in search_registry.keys():
queryset = search_registry[obj_type].queryset
url = search_registry[obj_type].url
# Restrict the queryset for the current user
if hasattr(queryset, 'restrict'):
@ -81,30 +95,51 @@ class FilterSetSearchBackend(SearchBackend):
# This backend requires a FilterSet class for the model
continue
table = getattr(search_registry[obj_type], 'table', None)
if not table:
# This backend requires a Table class for the model
continue
queryset = filterset({'q': value}, queryset=queryset).qs[:SEARCH_MAX_RESULTS]
# Construct the results table for this object type
filtered_queryset = filterset({'q': value}, queryset=queryset).qs
table = table(filtered_queryset, orderable=False)
table.paginate(per_page=SEARCH_MAX_RESULTS)
if table.page:
results.append({
'name': queryset.model._meta.verbose_name_plural,
'table': table,
'url': f"{reverse(url)}?q={value}"
})
results.extend([
{'object': obj}
for obj in queryset
])
return results
def cache(self, instance):
@staticmethod
def cache(sender, instance, **kwargs):
# This backend does not utilize a cache
pass
class CachedValueSearchBackend(SearchBackend):
def search(self, request, value, **kwargs):
return CachedValue.objects.filter(value__icontains=value)
@staticmethod
def cache(sender, instance, **kwargs):
try:
indexer = get_indexer(instance)
except KeyError:
return
data = indexer.to_cache(instance)
for field, value, weight in data:
if not value:
continue
ct = ContentType.objects.get_for_model(instance)
CachedValue.objects.update_or_create(
defaults={
'value': value,
'weight': weight,
},
object_type=ct,
object_id=instance.pk,
field=field,
type='text' # TODO
)
def get_backend():
"""Initializes and returns the configured search backend."""
backend_name = settings.SEARCH_BACKEND
@ -121,5 +156,4 @@ def get_backend():
return backend_cls()
default_search_engine = get_backend()
search = default_search_engine.search
search_backend = get_backend()

View File

@ -23,7 +23,7 @@ from extras.tables import ObjectChangeTable
from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF
from netbox.constants import SEARCH_MAX_RESULTS
from netbox.forms import SearchForm
from netbox.search.backends import default_search_engine
from netbox.search.backends import search_backend
from tenancy.models import Tenant
from virtualization.models import Cluster, VirtualMachine
from wireless.models import WirelessLAN, WirelessLink
@ -153,14 +153,14 @@ class SearchView(View):
results = []
if form.is_valid():
search_registry = default_search_engine.get_registry()
search_registry = search_backend.get_registry()
# If an object type has been specified, redirect to the dedicated view for it
if form.cleaned_data['obj_type']:
object_type = form.cleaned_data['obj_type']
url = reverse(search_registry[object_type].url)
return redirect(f"{url}?q={form.cleaned_data['q']}")
results = default_search_engine.search(request, form.cleaned_data['q'])
results = search_backend.search(request, form.cleaned_data['q'])
return render(request, 'search.html', {
'form': form,

View File

@ -20,44 +20,29 @@
{% if request.GET.q %}
{% if results %}
<div class="row">
<div class="col col-md-9">
{% for obj_type in results %}
<div class="card">
<h5 class="card-header" id="{{ obj_type.name|lower }}">{{ obj_type.name|bettertitle }}</h5>
<div class="card-body table-responsive">
{% render_table obj_type.table 'inc/table.html' %}
</div>
<div class="card-footer text-end">
<a href="{{ obj_type.url }}" class="btn btn-sm btn-primary my-1">
<i class="mdi mdi-arrow-right-bold" aria-hidden="true"></i>
{% if obj_type.table.page.has_next %}
See All {{ obj_type.table.page.paginator.count }} Results
{% else %}
Refine Search
{% endif %}
</a>
</div>
</div>
{% endfor %}
</div>
<div class="col col-md-3">
<div class="card">
<h5 class="card-header">
Search Results
</h5>
<div class="card-body">
<div class="list-group list-group-flush">
{% for obj_type in results %}
<a href="#{{ obj_type.name|lower }}" class="list-group-item">
<div class="float-end">
{% badge obj_type.table.page.paginator.count %}
</div>
{{ obj_type.name|bettertitle }}
</a>
{% endfor %}
</div>
</div>
<div class="col">
<div class="card">
<div class="card-body table-responsive">
<table class="table table-hover">
<tr>
<th>Type</th>
<th>Object</th>
<th>Field</th>
<th>Value</th>
</tr>
{% for result in results %}
<tr>
<td>{{ result.object|content_type }}</td>
<td>
<a href="{{ result.object.get_absolute_url }}">{{ result.object }}</a>
</td>
<td>{{ result.field|placeholder }}</td>
<td>{{ result.value|placeholder }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
</div>
{% else %}

View File

@ -5,21 +5,32 @@ from tenancy.models import Contact, ContactAssignment, Tenant
from utilities.utils import count_related
@register_search()
class TenantIndex(SearchIndex):
model = Tenant
queryset = Tenant.objects.prefetch_related('group')
filterset = tenancy.filtersets.TenantFilterSet
table = tenancy.tables.TenantTable
url = 'tenancy:tenant_list'
@register_search()
class ContactIndex(SearchIndex):
model = Contact
fields = (
('name', 100),
('title', 200),
('phone', 200),
('email', 200),
('address', 200),
('link', 300),
('comments', 1000),
)
queryset = Contact.objects.prefetch_related('group', 'assignments').annotate(
assignment_count=count_related(ContactAssignment, 'contact')
)
filterset = tenancy.filtersets.ContactFilterSet
table = tenancy.tables.ContactTable
url = 'tenancy:contact_list'
@register_search()
class TenantIndex(SearchIndex):
model = Tenant
fields = (
('name', 100),
('slug', 100),
('description', 500),
('comments', 1000),
)
queryset = Tenant.objects.prefetch_related('group')
filterset = tenancy.filtersets.TenantFilterSet

View File

@ -9,17 +9,23 @@ from virtualization.models import Cluster, VirtualMachine
@register_search()
class ClusterIndex(SearchIndex):
model = Cluster
fields = (
('name', 100),
('comments', 1000),
)
queryset = Cluster.objects.prefetch_related('type', 'group').annotate(
device_count=count_related(Device, 'cluster'), vm_count=count_related(VirtualMachine, 'cluster')
)
filterset = virtualization.filtersets.ClusterFilterSet
table = virtualization.tables.ClusterTable
url = 'virtualization:cluster_list'
@register_search()
class VirtualMachineIndex(SearchIndex):
model = VirtualMachine
fields = (
('name', 100),
('comments', 1000),
)
queryset = VirtualMachine.objects.prefetch_related(
'cluster',
'tenant',
@ -29,5 +35,3 @@ class VirtualMachineIndex(SearchIndex):
'primary_ip6',
)
filterset = virtualization.filtersets.VirtualMachineFilterSet
table = virtualization.tables.VirtualMachineTable
url = 'virtualization:virtualmachine_list'

View File

@ -9,18 +9,24 @@ from wireless.models import WirelessLAN, WirelessLink
@register_search()
class WirelessLANIndex(SearchIndex):
model = WirelessLAN
fields = (
('ssid', 100),
('description', 500),
('auth_psk', 1000),
)
queryset = WirelessLAN.objects.prefetch_related('group', 'vlan').annotate(
interface_count=count_related(Interface, 'wireless_lans')
)
filterset = wireless.filtersets.WirelessLANFilterSet
table = wireless.tables.WirelessLANTable
url = 'wireless:wirelesslan_list'
@register_search()
class WirelessLinkIndex(SearchIndex):
model = WirelessLink
fields = (
('ssid', 100),
('description', 500),
('auth_psk', 1000),
)
queryset = WirelessLink.objects.prefetch_related('interface_a__device', 'interface_b__device')
filterset = wireless.filtersets.WirelessLinkFilterSet
table = wireless.tables.WirelessLinkTable
url = 'wireless:wirelesslink_list'