mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-25 00:36:11 -06:00
Enable dynamic search registration for plugins
This commit is contained in:
parent
56f404b889
commit
7201bc1d9e
@ -9,6 +9,7 @@ from django.template.loader import get_template
|
|||||||
from extras.plugins.utils import import_object
|
from extras.plugins.utils import import_object
|
||||||
from extras.registry import registry
|
from extras.registry import registry
|
||||||
from netbox.navigation import MenuGroup
|
from netbox.navigation import MenuGroup
|
||||||
|
from netbox.search import register_search
|
||||||
from utilities.choices import ButtonColorChoices
|
from utilities.choices import ButtonColorChoices
|
||||||
|
|
||||||
|
|
||||||
@ -60,6 +61,7 @@ class PluginConfig(AppConfig):
|
|||||||
|
|
||||||
# Default integration paths. Plugin authors can override these to customize the paths to
|
# Default integration paths. Plugin authors can override these to customize the paths to
|
||||||
# integrated components.
|
# integrated components.
|
||||||
|
search = 'search.indexes'
|
||||||
graphql_schema = 'graphql.schema'
|
graphql_schema = 'graphql.schema'
|
||||||
menu = 'navigation.menu'
|
menu = 'navigation.menu'
|
||||||
menu_items = 'navigation.menu_items'
|
menu_items = 'navigation.menu_items'
|
||||||
@ -69,6 +71,11 @@ class PluginConfig(AppConfig):
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
plugin_name = self.name.rsplit('.', 1)[-1]
|
plugin_name = self.name.rsplit('.', 1)[-1]
|
||||||
|
|
||||||
|
# Search extensions
|
||||||
|
search_indexes = import_object(f"{self.__module__}.{self.search}") or []
|
||||||
|
for idx in search_indexes:
|
||||||
|
register_search()(idx)
|
||||||
|
|
||||||
# Register template content (if defined)
|
# Register template content (if defined)
|
||||||
template_extensions = import_object(f"{self.__module__}.{self.template_extensions}")
|
template_extensions = import_object(f"{self.__module__}.{self.template_extensions}")
|
||||||
if template_extensions is not None:
|
if template_extensions is not None:
|
||||||
|
13
netbox/extras/tests/dummy_plugin/search.py
Normal file
13
netbox/extras/tests/dummy_plugin/search.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from netbox.search import SearchIndex
|
||||||
|
from .models import DummyModel
|
||||||
|
|
||||||
|
|
||||||
|
class DummyModelIndex(SearchIndex):
|
||||||
|
model = DummyModel
|
||||||
|
queryset = DummyModel.objects.all()
|
||||||
|
url = 'plugins:dummy_plugin:dummy_models'
|
||||||
|
|
||||||
|
|
||||||
|
indexes = (
|
||||||
|
DummyModelIndex,
|
||||||
|
)
|
@ -1,4 +1,4 @@
|
|||||||
from .register import register_search
|
from extras.registry import registry
|
||||||
|
|
||||||
|
|
||||||
class SearchIndex:
|
class SearchIndex:
|
||||||
@ -12,3 +12,16 @@ class SearchIndex:
|
|||||||
if hasattr(cls, 'category'):
|
if hasattr(cls, 'category'):
|
||||||
return cls.category
|
return cls.category
|
||||||
return cls.model._meta.app_config.verbose_name
|
return cls.model._meta.app_config.verbose_name
|
||||||
|
|
||||||
|
|
||||||
|
def register_search():
|
||||||
|
def _wrapper(cls):
|
||||||
|
model = cls.model
|
||||||
|
app_label = model._meta.app_label
|
||||||
|
model_name = model._meta.model_name
|
||||||
|
|
||||||
|
registry['search'][app_label][model_name] = cls
|
||||||
|
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return _wrapper
|
||||||
|
@ -3,7 +3,6 @@ from importlib import import_module
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db.models.signals import post_save, pre_delete
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from extras.registry import registry
|
from extras.registry import registry
|
||||||
@ -14,16 +13,13 @@ _backends_cache = {}
|
|||||||
|
|
||||||
|
|
||||||
class SearchEngineError(Exception):
|
class SearchEngineError(Exception):
|
||||||
|
|
||||||
"""Something went wrong with a search engine."""
|
"""Something went wrong with a search engine."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SearchBackend(object):
|
class SearchBackend(object):
|
||||||
|
|
||||||
"""A search engine capable of performing multi-table searches."""
|
"""A search engine capable of performing multi-table searches."""
|
||||||
|
|
||||||
_created_engines: dict = dict()
|
_created_engines: dict = dict()
|
||||||
_search_choices = {}
|
|
||||||
_search_choice_options = tuple()
|
_search_choice_options = tuple()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -38,94 +34,44 @@ class SearchBackend(object):
|
|||||||
raise SearchEngineError(f"A search engine has already been created with the slug {engine_slug}")
|
raise SearchEngineError(f"A search engine has already been created with the slug {engine_slug}")
|
||||||
|
|
||||||
# Initialize this engine.
|
# Initialize this engine.
|
||||||
self._registered_models = {}
|
|
||||||
self._engine_slug = engine_slug
|
self._engine_slug = engine_slug
|
||||||
|
|
||||||
# Store a reference to this engine.
|
# Store a reference to this engine.
|
||||||
self.__class__._created_engines[engine_slug] = self
|
self.__class__._created_engines[engine_slug] = self
|
||||||
|
|
||||||
def is_registered(self, key, model):
|
|
||||||
"""Checks whether the given model is registered with this search engine."""
|
|
||||||
return key in self._registered_models
|
|
||||||
|
|
||||||
def register(self, key, 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(key, model):
|
|
||||||
raise RegistrationError(f"{model} is already registered with this search engine")
|
|
||||||
|
|
||||||
self._registered_models[key] = model
|
|
||||||
|
|
||||||
# add to the search choices
|
|
||||||
if model.choice_header not in self._search_choices:
|
|
||||||
self._search_choices[model.choice_header] = {}
|
|
||||||
|
|
||||||
if key not in self._search_choices[model.choice_header]:
|
|
||||||
self._search_choices[model.choice_header][key] = model.queryset.model._meta.verbose_name_plural
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
def get_registry(self):
|
def get_registry(self):
|
||||||
# return self._registered_models
|
|
||||||
|
|
||||||
r = {}
|
r = {}
|
||||||
for app_label, models in registry['search'].items():
|
for app_label, models in registry['search'].items():
|
||||||
r.update(**models)
|
r.update(**models)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
|
||||||
# Signalling hooks.
|
|
||||||
|
|
||||||
def get_search_choices(self):
|
def get_search_choices(self):
|
||||||
if self._search_choice_options:
|
if not self._search_choice_options:
|
||||||
return self._search_choice_options
|
|
||||||
|
|
||||||
# Organize choices by category
|
# Organize choices by category
|
||||||
categories = defaultdict(dict)
|
categories = defaultdict(dict)
|
||||||
for app_label, models in registry['search'].items():
|
for app_label, models in registry['search'].items():
|
||||||
for name, cls in models.items():
|
for name, cls in models.items():
|
||||||
model = cls.queryset.model
|
title = cls.model._meta.verbose_name.title()
|
||||||
title = model._meta.verbose_name.title()
|
categories[cls.get_category()][name] = title
|
||||||
categories[cls.get_category()][name] = title
|
|
||||||
|
|
||||||
# Compile a nested tuple of choices for form rendering
|
# Compile a nested tuple of choices for form rendering
|
||||||
results = (
|
results = (
|
||||||
('', 'All Objects'),
|
('', 'All Objects'),
|
||||||
*[(category, choices.items()) for category, choices in categories.items()]
|
*[(category, choices.items()) for category, choices in categories.items()]
|
||||||
)
|
)
|
||||||
|
|
||||||
self._search_choice_options = results
|
self._search_choice_options = results
|
||||||
|
|
||||||
return self._search_choice_options
|
return self._search_choice_options
|
||||||
|
|
||||||
def _use_hooks(self):
|
def search(self, request, search_text, models=(), exclude=(), ranking=True, backend_name=None):
|
||||||
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."""
|
"""Performs a search using the given text, returning a queryset of SearchEntry."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class PostgresIcontainsSearchBackend(SearchBackend):
|
class PostgresIcontainsSearchBackend(SearchBackend):
|
||||||
def _use_hooks(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def search(self, request, search_text):
|
def search(self, request, search_text):
|
||||||
results = []
|
results = []
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
import importlib
|
|
||||||
import inspect
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.module_loading import module_has_submodule
|
|
||||||
|
|
||||||
from extras.registry import registry
|
|
||||||
from .backends import default_search_engine
|
|
||||||
|
|
||||||
|
|
||||||
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 register():
|
|
||||||
|
|
||||||
# for name, module in SEARCH_TYPES.items():
|
|
||||||
# default_search_engine.register(name, module)
|
|
||||||
|
|
||||||
for app_label, models in registry['search'].items():
|
|
||||||
for name, cls in models.items():
|
|
||||||
default_search_engine.register(name, cls)
|
|
||||||
|
|
||||||
for name, module in get_app_modules():
|
|
||||||
submodule_name = "search_indexes"
|
|
||||||
if module_has_submodule(module, submodule_name):
|
|
||||||
module_name = f"{name}.{submodule_name}"
|
|
||||||
if name in settings.PLUGINS:
|
|
||||||
search_module = importlib.import_module(module_name)
|
|
||||||
else:
|
|
||||||
search_module = sys.modules[module_name]
|
|
||||||
|
|
||||||
cls_objects = inspect.getmembers(search_module, predicate=inspect.isclass)
|
|
||||||
for cls_name, cls_obj in inspect.getmembers(search_module, predicate=inspect.isclass):
|
|
||||||
if getattr(cls_obj, "search_index", False) and getattr(cls_obj, "model", None):
|
|
||||||
cls_name = cls_obj.model.__name__.lower()
|
|
||||||
if not default_search_engine.is_registered(cls_name, cls_obj):
|
|
||||||
default_search_engine.register(cls_name, cls_obj)
|
|
||||||
|
|
||||||
|
|
||||||
def register_search():
|
|
||||||
def _wrapper(cls):
|
|
||||||
model = cls.model
|
|
||||||
app_label = model._meta.app_label
|
|
||||||
model_name = model._meta.model_name
|
|
||||||
|
|
||||||
registry['search'][app_label][model_name] = cls
|
|
||||||
|
|
||||||
return cls
|
|
||||||
|
|
||||||
return _wrapper
|
|
Loading…
Reference in New Issue
Block a user