mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-01 07:03:22 +02:00
Merge branch 'feature' into 8366-job-scheduling
This commit is contained in:
@@ -5,5 +5,4 @@ class ExtrasConfig(AppConfig):
|
||||
name = "extras"
|
||||
|
||||
def ready(self):
|
||||
import extras.lookups
|
||||
import extras.signals
|
||||
from . import lookups, search, signals
|
||||
|
||||
@@ -59,3 +59,10 @@ class TagsMixin:
|
||||
|
||||
def resolve_tags(self, info):
|
||||
return self.tags.all()
|
||||
|
||||
|
||||
class ContactsMixin:
|
||||
contacts = graphene.List('tenancy.graphql.types.ContactAssignmentType')
|
||||
|
||||
def resolve_contacts(self, info):
|
||||
return list(self.contacts.all())
|
||||
|
||||
@@ -27,7 +27,7 @@ class CustomFieldType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.CustomField
|
||||
fields = '__all__'
|
||||
exclude = ('content_types', )
|
||||
filterset_class = filtersets.CustomFieldFilterSet
|
||||
|
||||
|
||||
@@ -83,5 +83,5 @@ class WebhookType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.Webhook
|
||||
fields = '__all__'
|
||||
exclude = ('content_types', )
|
||||
filterset_class = filtersets.WebhookFilterSet
|
||||
|
||||
@@ -5,10 +5,11 @@ from packaging import version
|
||||
from django.apps import AppConfig
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.template.loader import get_template
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from extras.plugins.utils import import_object
|
||||
from extras.registry import registry
|
||||
from netbox.navigation import MenuGroup
|
||||
from netbox.search import register_search
|
||||
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
|
||||
# integrated components.
|
||||
search_indexes = 'search.indexes'
|
||||
graphql_schema = 'graphql.schema'
|
||||
menu = 'navigation.menu'
|
||||
menu_items = 'navigation.menu_items'
|
||||
@@ -69,26 +71,46 @@ class PluginConfig(AppConfig):
|
||||
def ready(self):
|
||||
plugin_name = self.name.rsplit('.', 1)[-1]
|
||||
|
||||
# Register template content (if defined)
|
||||
template_extensions = import_object(f"{self.__module__}.{self.template_extensions}")
|
||||
if template_extensions is not None:
|
||||
register_template_extensions(template_extensions)
|
||||
# Register search extensions (if defined)
|
||||
try:
|
||||
search_indexes = import_string(f"{self.__module__}.{self.search_indexes}")
|
||||
for idx in search_indexes:
|
||||
register_search()(idx)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Register navigation menu or menu items (if defined)
|
||||
if menu := import_object(f"{self.__module__}.{self.menu}"):
|
||||
# Register template content (if defined)
|
||||
try:
|
||||
template_extensions = import_string(f"{self.__module__}.{self.template_extensions}")
|
||||
register_template_extensions(template_extensions)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Register navigation menu and/or menu items (if defined)
|
||||
try:
|
||||
menu = import_string(f"{self.__module__}.{self.menu}")
|
||||
register_menu(menu)
|
||||
if menu_items := import_object(f"{self.__module__}.{self.menu_items}"):
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
menu_items = import_string(f"{self.__module__}.{self.menu_items}")
|
||||
register_menu_items(self.verbose_name, menu_items)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Register GraphQL schema (if defined)
|
||||
graphql_schema = import_object(f"{self.__module__}.{self.graphql_schema}")
|
||||
if graphql_schema is not None:
|
||||
try:
|
||||
graphql_schema = import_string(f"{self.__module__}.{self.graphql_schema}")
|
||||
register_graphql_schema(graphql_schema)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Register user preferences (if defined)
|
||||
user_preferences = import_object(f"{self.__module__}.{self.user_preferences}")
|
||||
if user_preferences is not None:
|
||||
try:
|
||||
user_preferences = import_string(f"{self.__module__}.{self.user_preferences}")
|
||||
register_user_preferences(plugin_name, user_preferences)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def validate(cls, user_config, netbox_version):
|
||||
|
||||
@@ -3,8 +3,7 @@ from django.conf import settings
|
||||
from django.conf.urls import include
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.urls import path
|
||||
|
||||
from extras.plugins.utils import import_object
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from . import views
|
||||
|
||||
@@ -25,15 +24,19 @@ for plugin_path in settings.PLUGINS:
|
||||
base_url = getattr(app, 'base_url') or app.label
|
||||
|
||||
# Check if the plugin specifies any base URLs
|
||||
urlpatterns = import_object(f"{plugin_path}.urls.urlpatterns")
|
||||
if urlpatterns is not None:
|
||||
try:
|
||||
urlpatterns = import_string(f"{plugin_path}.urls.urlpatterns")
|
||||
plugin_patterns.append(
|
||||
path(f"{base_url}/", include((urlpatterns, app.label)))
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Check if the plugin specifies any API URLs
|
||||
urlpatterns = import_object(f"{plugin_path}.api.urls.urlpatterns")
|
||||
if urlpatterns is not None:
|
||||
try:
|
||||
urlpatterns = import_string(f"{plugin_path}.api.urls.urlpatterns")
|
||||
plugin_api_patterns.append(
|
||||
path(f"{base_url}/", include((urlpatterns, f"{app.label}-api")))
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import importlib.util
|
||||
import sys
|
||||
|
||||
|
||||
def import_object(module_and_object):
|
||||
"""
|
||||
Import a specific object from a specific module by name, such as "extras.plugins.utils.import_object".
|
||||
|
||||
Returns the imported object, or None if it doesn't exist.
|
||||
"""
|
||||
target_module_name, object_name = module_and_object.rsplit('.', 1)
|
||||
module_hierarchy = target_module_name.split('.')
|
||||
|
||||
# Iterate through the module hierarchy, checking for the existence of each successive submodule.
|
||||
# We have to do this rather than jumping directly to calling find_spec(target_module_name)
|
||||
# because find_spec will raise a ModuleNotFoundError if any parent module of target_module_name does not exist.
|
||||
module_name = ""
|
||||
for module_component in module_hierarchy:
|
||||
module_name = f"{module_name}.{module_component}" if module_name else module_component
|
||||
spec = importlib.util.find_spec(module_name)
|
||||
if spec is None:
|
||||
# No such module
|
||||
return None
|
||||
|
||||
# Okay, target_module_name exists. Load it if not already loaded
|
||||
if target_module_name in sys.modules:
|
||||
module = sys.modules[target_module_name]
|
||||
else:
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[target_module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
return getattr(module, object_name, None)
|
||||
@@ -29,4 +29,5 @@ registry['model_features'] = {
|
||||
feature: collections.defaultdict(set) for feature in EXTRAS_FEATURES
|
||||
}
|
||||
registry['denormalized_fields'] = collections.defaultdict(list)
|
||||
registry['search'] = collections.defaultdict(dict)
|
||||
registry['views'] = collections.defaultdict(dict)
|
||||
|
||||
14
netbox/extras/search.py
Normal file
14
netbox/extras/search.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import extras.filtersets
|
||||
import extras.tables
|
||||
from extras.models import JournalEntry
|
||||
from netbox.search import SearchIndex, register_search
|
||||
|
||||
|
||||
@register_search()
|
||||
class JournalEntryIndex(SearchIndex):
|
||||
model = JournalEntry
|
||||
queryset = JournalEntry.objects.prefetch_related('assigned_object', 'created_by')
|
||||
filterset = extras.filtersets.JournalEntryFilterSet
|
||||
table = extras.tables.JournalEntryTable
|
||||
url = 'extras:journalentry_list'
|
||||
category = 'Journal'
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user