mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-24 08:25:17 -06:00
#7016 base search classes
This commit is contained in:
parent
656f0b7d82
commit
c146ab7963
148
netbox/dcim/search_indexes.py
Normal file
148
netbox/dcim/search_indexes.py
Normal file
@ -0,0 +1,148 @@
|
||||
import dcim.filtersets
|
||||
import dcim.tables
|
||||
from dcim.models import (
|
||||
Cable,
|
||||
Device,
|
||||
DeviceType,
|
||||
Interface,
|
||||
Location,
|
||||
Module,
|
||||
ModuleType,
|
||||
PowerFeed,
|
||||
Rack,
|
||||
RackReservation,
|
||||
Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
from django.db import models
|
||||
from search.models import SearchMixin
|
||||
|
||||
|
||||
class SiteIndex(SearchMixin):
|
||||
model = Site
|
||||
queryset = Site.objects.prefetch_related('region', 'tenant', 'tenant__group')
|
||||
filterset = dcim.filtersets.SiteFilterSet
|
||||
table = dcim.tables.SiteTable
|
||||
url = 'dcim:site_list'
|
||||
|
||||
|
||||
class RackIndex(SearchMixin):
|
||||
model = Rack
|
||||
queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate(
|
||||
device_count=count_related(Device, 'rack')
|
||||
)
|
||||
filterset = dcim.filtersets.RackFilterSet
|
||||
table = dcim.tables.RackTable
|
||||
url = 'dcim:rack_list'
|
||||
|
||||
|
||||
class RackReservationIndex(SearchMixin):
|
||||
model = RackReservation
|
||||
queryset = RackReservation.objects.prefetch_related('rack', 'user')
|
||||
filterset = dcim.filtersets.RackReservationFilterSet
|
||||
table = dcim.tables.RackReservationTable
|
||||
url = 'dcim:rackreservation_list'
|
||||
|
||||
|
||||
class LocationIndex(SearchMixin):
|
||||
model = Site
|
||||
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'
|
||||
|
||||
|
||||
class DeviceTypeIndex(SearchMixin):
|
||||
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'
|
||||
|
||||
|
||||
class DeviceIndex(SearchMixin):
|
||||
model = DeviceIndex
|
||||
queryset = Device.objects.prefetch_related(
|
||||
'device_type__manufacturer',
|
||||
'device_role',
|
||||
'tenant',
|
||||
'tenant__group',
|
||||
'site',
|
||||
'rack',
|
||||
'primary_ip4',
|
||||
'primary_ip6',
|
||||
)
|
||||
filterset = dcim.filtersets.DeviceFilterSet
|
||||
table = dcim.tables.DeviceTable
|
||||
url = 'dcim:device_list'
|
||||
|
||||
|
||||
class ModuleTypeIndex(SearchMixin):
|
||||
model = ModuleType
|
||||
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
|
||||
instance_count=count_related(Module, 'module_type')
|
||||
)
|
||||
filterset = dcim.filtersets.ModuleTypeFilterSet
|
||||
table = dcim.tables.ModuleTypeTable
|
||||
url = 'dcim:moduletype_list'
|
||||
|
||||
|
||||
class ModuleIndex(SearchMixin):
|
||||
model = Module
|
||||
queryset = Module.objects.prefetch_related(
|
||||
'module_type__manufacturer',
|
||||
'device',
|
||||
'module_bay',
|
||||
)
|
||||
filterset = dcim.filtersets.ModuleFilterSet
|
||||
table = dcim.tables.ModuleTable
|
||||
url = 'dcim:module_list'
|
||||
|
||||
|
||||
class VirtualChassisIndex(SearchMixin):
|
||||
model = VirtualChassis
|
||||
queryset = VirtualChassis.objects.prefetch_related('master').annotate(
|
||||
member_count=count_related(Device, 'virtual_chassis')
|
||||
)
|
||||
filterset = dcim.filtersets.VirtualChassisFilterSet
|
||||
table = dcim.tables.VirtualChassisTable
|
||||
url = 'dcim:virtualchassis_list'
|
||||
|
||||
|
||||
class CableIndex(SearchMixin):
|
||||
model = Cable
|
||||
queryset = Cable.objects.all()
|
||||
filterset = dcim.filtersets.CableFilterSet
|
||||
table = dcim.tables.CableTable
|
||||
url = 'dcim:cable_list'
|
||||
|
||||
|
||||
class PowerFeedIndex(SearchMixin):
|
||||
model = PowerFeed
|
||||
queryset = PowerFeed.objects.all()
|
||||
filterset = dcim.filtersets.PowerFeedFilterSet
|
||||
table = dcim.tables.PowerFeedTable
|
||||
url = 'dcim:powerfeed_list'
|
||||
|
||||
|
||||
DCIM_SEARCH_ORDERING = [
|
||||
SiteIndex,
|
||||
RackIndex,
|
||||
RackReservationIndex,
|
||||
LocationIndex,
|
||||
DeviceTypeIndex,
|
||||
DeviceIndex,
|
||||
ModuleTypeIndex,
|
||||
ModuleIndex,
|
||||
VirtualChassisIndex,
|
||||
CableIndex,
|
||||
PowerFeedIndex,
|
||||
]
|
@ -330,6 +330,7 @@ INSTALLED_APPS = [
|
||||
'wireless',
|
||||
'django_rq', # Must come after extras to allow overriding management commands
|
||||
'drf_yasg',
|
||||
'search',
|
||||
]
|
||||
|
||||
# Middleware
|
||||
|
0
netbox/search/__init__.py
Normal file
0
netbox/search/__init__.py
Normal file
33
netbox/search/apps.py
Normal file
33
netbox/search/apps.py
Normal file
@ -0,0 +1,33 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
from django.apps import apps
|
||||
from django.utils.module_loading import module_has_submodule
|
||||
from netbox import denormalized
|
||||
|
||||
|
||||
def get_app_modules():
|
||||
"""
|
||||
Returns all app modules (installed apps) - yields tuples of (app_name, module)
|
||||
"""
|
||||
for app in apps.get_app_configs():
|
||||
yield app.name, app.module
|
||||
|
||||
|
||||
def get_app_submodules(submodule_name):
|
||||
"""
|
||||
Searches each app module for the specified submodule - yields tuples of (app_name, module)
|
||||
"""
|
||||
for name, module in get_app_modules():
|
||||
if module_has_submodule(module, submodule_name):
|
||||
yield name, import_module(f"{name}.{submodule_name}")
|
||||
|
||||
|
||||
class SearchConfig(AppConfig):
|
||||
name = "search"
|
||||
verbose_name = "search"
|
||||
|
||||
def ready(self):
|
||||
for name, module in get_app_modules():
|
||||
submodule_name = "search_indexes"
|
||||
if module_has_submodule(module, submodule_name):
|
||||
print(f"{name}.{submodule_name}")
|
113
netbox/search/backends.py
Normal file
113
netbox/search/backends.py
Normal file
@ -0,0 +1,113 @@
|
||||
from abc import ABC
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
|
||||
# The cache for the initialized backend.
|
||||
_backends_cache = {}
|
||||
|
||||
|
||||
def get_backend(backend_name=None):
|
||||
"""Initializes and returns the search backend."""
|
||||
global _backends_cache
|
||||
if not backend_name:
|
||||
backend_name = getattr(settings, "SEARCH_BACKEND", "search.backends.PostgresIcontainsSearchBackend")
|
||||
|
||||
# Try to use the cached backend.
|
||||
if backend_name in _backends_cache:
|
||||
return _backends_cache[backend_name]
|
||||
|
||||
# Load the backend class.
|
||||
backend_module_name, backend_cls_name = backend_name.rsplit(".", 1)
|
||||
backend_module = import_module(backend_module_name)
|
||||
try:
|
||||
backend_cls = getattr(backend_module, backend_cls_name)
|
||||
except AttributeError:
|
||||
raise ImproperlyConfigured(f"Could not find a class named {backend_module_name} in {backend_cls_name}")
|
||||
|
||||
# Initialize the backend.
|
||||
backend = backend_cls()
|
||||
_backends_cache[backend_name] = backend
|
||||
return backend
|
||||
|
||||
|
||||
class SearchEngineError(Exception):
|
||||
|
||||
"""Something went wrong with a search engine."""
|
||||
|
||||
|
||||
class SearchBackend(object):
|
||||
|
||||
"""A search engine capable of performing multi-table searches."""
|
||||
|
||||
_created_engines: dict = dict()
|
||||
|
||||
@classmethod
|
||||
def get_created_engines(cls):
|
||||
"""Returns all created search engines."""
|
||||
return list(cls._created_engines.items())
|
||||
|
||||
def __init__(self, engine_slug: str):
|
||||
"""Initializes the search engine."""
|
||||
# Check the slug is unique for this project.
|
||||
if engine_slug in SearchBackend._created_engines:
|
||||
raise SearchEngineError(f"A search engine has already been created with the slug {engine_slug}")
|
||||
|
||||
# Initialize this engine.
|
||||
self._registered_models = {}
|
||||
self._engine_slug = engine_slug
|
||||
|
||||
# Store a reference to this engine.
|
||||
self.__class__._created_engines[engine_slug] = self
|
||||
|
||||
def is_registered(self, model):
|
||||
"""Checks whether the given model is registered with this search engine."""
|
||||
return model in self._registered_models
|
||||
|
||||
def register(self, model):
|
||||
"""
|
||||
Registers the given model with this search engine.
|
||||
|
||||
If the given model is already registered with this search engine, a
|
||||
RegistrationError will be raised.
|
||||
"""
|
||||
# Check for existing registration.
|
||||
if self.is_registered(model):
|
||||
raise RegistrationError(f"{model} is already registered with this search engine")
|
||||
|
||||
# Connect to the signalling framework.
|
||||
if self._use_hooks():
|
||||
post_save.connect(self._post_save_receiver, model)
|
||||
pre_delete.connect(self._pre_delete_receiver, model)
|
||||
|
||||
# Signalling hooks.
|
||||
|
||||
def _use_hooks(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _post_save_receiver(self, instance, **kwargs):
|
||||
"""Signal handler for when a registered model has been saved."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _pre_delete_receiver(self, instance, **kwargs):
|
||||
"""Signal handler for when a registered model has been deleted."""
|
||||
raise NotImplementedError
|
||||
|
||||
# Searching.
|
||||
|
||||
def search(self, search_text, models=(), exclude=(), ranking=True, backend_name=None):
|
||||
"""Performs a search using the given text, returning a queryset of SearchEntry."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PostgresIcontainsSearchBackend(SearchBackend):
|
||||
def _use_hooks(self):
|
||||
return False
|
||||
|
||||
|
||||
# The main search methods.
|
||||
default_search_engine = SearchBackend("default")
|
||||
search = default_search_engine.search
|
102
netbox/search/models.py
Normal file
102
netbox/search/models.py
Normal file
@ -0,0 +1,102 @@
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import models
|
||||
|
||||
|
||||
class SearchMixin(models.Model):
|
||||
"""
|
||||
Base class for building search indexes.
|
||||
"""
|
||||
|
||||
model = None
|
||||
queryset = None
|
||||
filterset = None
|
||||
table = None
|
||||
url = None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def get_model(self):
|
||||
"""
|
||||
Should return the ``Model`` class (not an instance) that the rest of the
|
||||
``SearchIndex`` should use.
|
||||
This method is required & you must override it to return the correct class.
|
||||
"""
|
||||
if self.model is not None:
|
||||
model = self.model
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
f"{self.__class__.__name__}s is missing a Model. Define "
|
||||
f"{self.__class__.__name__}s.model or override "
|
||||
f"{self.__class__.__name__}s.get_model()."
|
||||
)
|
||||
|
||||
return model
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Should return the ``QuerySet`` class (not an instance) that the rest of the
|
||||
``SearchIndex`` should use.
|
||||
This method is required & you must override it to return the correct class.
|
||||
"""
|
||||
if self.queryset is not None:
|
||||
queryset = self.queryset
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
f"{self.__class__.__name__}s is missing a QuerySet. Define "
|
||||
f"{self.__class__.__name__}s.queryset or override "
|
||||
f"{self.__class__.__name__}s.get_queryset()."
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
def get_filterset(self):
|
||||
"""
|
||||
Should return the ``FilterSet`` class (not an instance) that the rest of the
|
||||
``SearchIndex`` should use.
|
||||
This method is required & you must override it to return the correct class.
|
||||
"""
|
||||
if self.filterset is not None:
|
||||
filterset = self.filterset
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
f"{self.__class__.__name__}s is missing a FilterSet. Define "
|
||||
f"{self.__class__.__name__}s.filterset or override "
|
||||
f"{self.__class__.__name__}s.get_filterset()."
|
||||
)
|
||||
|
||||
return filterset
|
||||
|
||||
def get_table(self):
|
||||
"""
|
||||
Should return the ``Table`` class (not an instance) that the rest of the
|
||||
``SearchIndex`` should use.
|
||||
This method is required & you must override it to return the correct class.
|
||||
"""
|
||||
if self.table is not None:
|
||||
table = self.table
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
f"{self.__class__.__name__}s is missing a Table. Define "
|
||||
f"{self.__class__.__name__}s.table or override "
|
||||
f"{self.__class__.__name__}s.get_table()."
|
||||
)
|
||||
|
||||
return table
|
||||
|
||||
def get_url(self):
|
||||
"""
|
||||
Should return the ``URL`` class (not an instance) that the rest of the
|
||||
``SearchIndex`` should use.
|
||||
This method is required & you must override it to return the correct class.
|
||||
"""
|
||||
if self.url is not None:
|
||||
url = self.url
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
f"{self.__class__.__name__}s is missing a URL. Define "
|
||||
f"{self.__class__.__name__}s.url or override "
|
||||
f"{self.__class__.__name__}s.get_url()."
|
||||
)
|
||||
|
||||
return url
|
Loading…
Reference in New Issue
Block a user