diff --git a/netbox/core/api/serializers_/object_types.py b/netbox/core/api/serializers_/object_types.py index e272509ec..1c17c6e44 100644 --- a/netbox/core/api/serializers_/object_types.py +++ b/netbox/core/api/serializers_/object_types.py @@ -1,13 +1,13 @@ import inspect -from django.urls import NoReverseMatch, reverse +from django.urls import NoReverseMatch from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from core.models import ObjectType from netbox.api.serializers import BaseModelSerializer -from utilities.views import get_viewname +from utilities.views import get_action_url __all__ = ( 'ObjectTypeSerializer', @@ -34,11 +34,10 @@ class ObjectTypeSerializer(BaseModelSerializer): def get_rest_api_endpoint(self, obj): if not (model := obj.model_class()): return - if viewname := get_viewname(model, action='list', rest_api=True): - try: - return reverse(viewname) - except NoReverseMatch: - return + try: + return get_action_url(model, action='list', rest_api=True) + except NoReverseMatch: + return @extend_schema_field(OpenApiTypes.STR) def get_description(self, obj): diff --git a/netbox/extras/views.py b/netbox/extras/views.py index a2664a2c2..cf197b55e 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -31,7 +31,7 @@ from utilities.querydict import normalize_querydict from utilities.request import copy_safe_request from utilities.rqworker import get_workers_for_queue from utilities.templatetags.builtins.filters import render_markdown -from utilities.views import ContentTypePermissionRequiredMixin, get_viewname, register_model_view +from utilities.views import ContentTypePermissionRequiredMixin, get_action_url, register_model_view from virtualization.models import VirtualMachine from . import filtersets, forms, tables from .constants import LOG_LEVEL_RANK @@ -1103,8 +1103,7 @@ class JournalEntryEditView(generic.ObjectEditView): if not instance.assigned_object: return reverse('extras:journalentry_list') obj = instance.assigned_object - viewname = get_viewname(obj, 'journal') - return reverse(viewname, kwargs={'pk': obj.pk}) + return get_action_url(obj, action='journal', kwargs={'pk': obj.pk}) @register_model_view(JournalEntry, 'delete') @@ -1113,8 +1112,7 @@ class JournalEntryDeleteView(generic.ObjectDeleteView): def get_return_url(self, request, instance): obj = instance.assigned_object - viewname = get_viewname(obj, 'journal') - return reverse(viewname, kwargs={'pk': obj.pk}) + return get_action_url(obj, action='journal', kwargs={'pk': obj.pk}) @register_model_view(JournalEntry, 'bulk_import', path='import', detail=False) diff --git a/netbox/netbox/object_actions.py b/netbox/netbox/object_actions.py index 51687eee1..4bb1630a0 100644 --- a/netbox/netbox/object_actions.py +++ b/netbox/netbox/object_actions.py @@ -1,12 +1,11 @@ from django.template import loader -from django.urls import reverse from django.urls.exceptions import NoReverseMatch from django.utils.translation import gettext as _ from core.models import ObjectType from extras.models import ExportTemplate from utilities.querydict import prepare_cloned_fields -from utilities.views import get_viewname +from utilities.views import get_action_url __all__ = ( 'AddObject', @@ -43,12 +42,11 @@ class ObjectAction: @classmethod def get_url(cls, obj): - viewname = get_viewname(obj, action=cls.name) kwargs = { kwarg: getattr(obj, kwarg) for kwarg in cls.url_kwargs } try: - return reverse(viewname, kwargs=kwargs) + return get_action_url(obj, action=cls.name, kwargs=kwargs) except NoReverseMatch: return diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index 3d36ec8f8..cf5c11188 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -21,7 +21,7 @@ from extras.choices import CustomFieldTypeChoices from utilities.object_types import object_type_identifier, object_type_name from utilities.permissions import get_permission_for_model from utilities.templatetags.builtins.filters import render_markdown -from utilities.views import get_viewname +from utilities.views import get_action_url __all__ = ( 'ActionsColumn', @@ -285,7 +285,7 @@ class ActionsColumn(tables.Column): for idx, (action, attrs) in enumerate(self.actions.items()): permission = get_permission_for_model(model, attrs.permission) if attrs.permission is None or user.has_perm(permission): - url = reverse(get_viewname(model, action), kwargs={'pk': record.pk}) + url = get_action_url(record, action=action, kwargs={'pk': record.pk}) # Render a separate button if a) only one action exists, or b) if split_actions is True if len(self.actions) == 1 or (self.split_actions and idx == 0): diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 63fd0ea0e..89a9c1ac2 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -8,7 +8,6 @@ from django.contrib.contenttypes.fields import GenericForeignKey from django.core.exceptions import FieldDoesNotExist from django.db.models.fields.related import RelatedField from django.db.models.fields.reverse_related import ManyToOneRel -from django.urls import reverse from django.urls.exceptions import NoReverseMatch from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ @@ -23,7 +22,7 @@ from netbox.tables import columns from utilities.html import highlight from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.string import title -from utilities.views import get_viewname +from utilities.views import get_action_url from .template_code import * __all__ = ( @@ -261,9 +260,8 @@ class NetBoxTable(BaseTable): Return the base HTML request URL for embedded tables. """ if self.embedded: - viewname = get_viewname(self._meta.model, action='list') try: - return reverse(viewname) + return get_action_url(self._meta.model, action='list') except NoReverseMatch: pass return '' diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 1263874c4..0c509d63d 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -282,6 +282,21 @@ def get_viewname(model, action=None, rest_api=False): return viewname +def get_action_url(model, action=None, rest_api=False, kwargs=None): + """ + Return the url for the given model and action, if valid. + + :param model: The model or instance to which the view applies + :param action: A string indicating the desired action (if any); e.g. "add" or "list" + :param rest_api: A boolean indicating whether this is a REST API view + :param kwargs: A dictionary of keyword arguments for the view to include when registering its URL path (optional). + """ + if hasattr(model, '_get_action_url'): + return model._get_action_url(action, rest_api, kwargs) + + return reverse(get_viewname(model, action, rest_api), kwargs=kwargs) + + def register_model_view(model, name='', path=None, detail=True, kwargs=None): """ This decorator can be used to "attach" a view to any model in NetBox. This is typically used to inject