mirror of
https://github.com/netbox-community/netbox.git
synced 2025-09-06 14:23:36 -06:00
20048 cleanup get_viewname URL resolution (#20050)
* #20048 add get_action_url utility function * #20048 add get_action_url utility function * #20048 add get_action_url utility function * #20048 add get_action_url utility function * #20048 add get_action_url utility function * #20048 action_url template tag * #20048 action_url template tag * #20048 fix test * #20048 review feedback * #20048 fix tags
This commit is contained in:
parent
1242ad68f7
commit
a585bc044e
@ -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):
|
||||
|
@ -11,7 +11,7 @@ from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Model
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import NoReverseMatch, resolve, reverse
|
||||
from django.urls import NoReverseMatch, resolve
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from core.models import ObjectType
|
||||
@ -21,7 +21,7 @@ from utilities.permissions import get_permission_for_model
|
||||
from utilities.proxy import resolve_proxies
|
||||
from utilities.querydict import dict_to_querydict
|
||||
from utilities.templatetags.builtins.filters import render_markdown
|
||||
from utilities.views import get_viewname
|
||||
from utilities.views import get_action_url
|
||||
from .utils import register_widget
|
||||
|
||||
__all__ = (
|
||||
@ -53,9 +53,9 @@ def object_list_widget_supports_model(model: Model) -> bool:
|
||||
"""
|
||||
def can_resolve_model_list_view(model: Model) -> bool:
|
||||
try:
|
||||
reverse(get_viewname(model, action='list'))
|
||||
get_action_url(model, action='list')
|
||||
return True
|
||||
except Exception:
|
||||
except NoReverseMatch:
|
||||
return False
|
||||
|
||||
tests = [
|
||||
@ -206,7 +206,7 @@ class ObjectCountsWidget(DashboardWidget):
|
||||
permission = get_permission_for_model(model, 'view')
|
||||
if request.user.has_perm(permission):
|
||||
try:
|
||||
url = reverse(get_viewname(model, 'list'))
|
||||
url = get_action_url(model, action='list')
|
||||
except NoReverseMatch:
|
||||
url = None
|
||||
qs = model.objects.restrict(request.user, 'view')
|
||||
@ -275,15 +275,13 @@ class ObjectListWidget(DashboardWidget):
|
||||
logger.debug(f"Dashboard Widget model_class not found: {app_label}:{model_name}")
|
||||
return
|
||||
|
||||
viewname = get_viewname(model, action='list')
|
||||
|
||||
# Evaluate user's permission. Note that this controls only whether the HTMX element is
|
||||
# embedded on the page: The view itself will also evaluate permissions separately.
|
||||
permission = get_permission_for_model(model, 'view')
|
||||
has_permission = request.user.has_perm(permission)
|
||||
|
||||
try:
|
||||
htmx_url = reverse(viewname)
|
||||
htmx_url = get_action_url(model, action='list')
|
||||
except NoReverseMatch:
|
||||
htmx_url = None
|
||||
parameters = self.config.get('url_params') or {}
|
||||
@ -297,7 +295,7 @@ class ObjectListWidget(DashboardWidget):
|
||||
except ValueError:
|
||||
pass
|
||||
return render_to_string(self.template_name, {
|
||||
'viewname': viewname,
|
||||
'model_name': model_name,
|
||||
'has_permission': has_permission,
|
||||
'htmx_url': htmx_url,
|
||||
})
|
||||
|
@ -45,4 +45,4 @@ class ObjectListWidgetTests(TestCase):
|
||||
mock_request = Request()
|
||||
widget = ObjectListWidget(id='2829fd9b-5dee-4c9a-81f2-5bd84c350a27', **config)
|
||||
rendered = widget.render(mock_request)
|
||||
self.assertTrue('Unable to load content. Invalid view name:' in rendered)
|
||||
self.assertTrue('Unable to load content. Could not resolve list URL for:' in rendered)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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(model, 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):
|
||||
|
@ -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 ''
|
||||
|
@ -12,7 +12,6 @@ from django.db.models.fields.reverse_related import ManyToManyRel
|
||||
from django.forms import ModelMultipleChoiceField, MultipleHiddenInput
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext as _
|
||||
from mptt.models import MPTTModel
|
||||
@ -34,7 +33,7 @@ from utilities.permissions import get_permission_for_model
|
||||
from utilities.query import reapply_model_ordering
|
||||
from utilities.request import safe_for_redirect
|
||||
from utilities.tables import get_table_configs
|
||||
from utilities.views import GetReturnURLMixin, get_viewname
|
||||
from utilities.views import GetReturnURLMixin, get_action_url
|
||||
from .base import BaseMultiObjectView
|
||||
from .mixins import ActionsMixin, TableMixin
|
||||
from .utils import get_prerequisite_model
|
||||
@ -130,7 +129,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
||||
redirect_url = f'{request.path}?{query_params.urlencode()}'
|
||||
if safe_for_redirect(redirect_url):
|
||||
return redirect(redirect_url)
|
||||
return redirect(get_viewname(self.queryset.model, 'list'))
|
||||
return redirect(get_action_url(self.queryset.model, action='list'))
|
||||
|
||||
#
|
||||
# Request handlers
|
||||
@ -513,7 +512,7 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
|
||||
if form.is_valid():
|
||||
logger.debug("Import form validation was successful")
|
||||
redirect_url = reverse(get_viewname(model, action='list'))
|
||||
redirect_url = get_action_url(model, action='list')
|
||||
|
||||
# If indicated, defer this request to a background job & redirect the user
|
||||
if form.cleaned_data['background_job']:
|
||||
|
@ -25,7 +25,7 @@ from utilities.permissions import get_permission_for_model
|
||||
from utilities.querydict import normalize_querydict, prepare_cloned_fields
|
||||
from utilities.request import safe_for_redirect
|
||||
from utilities.tables import get_table_configs
|
||||
from utilities.views import GetReturnURLMixin, get_viewname
|
||||
from utilities.views import GetReturnURLMixin, get_action_url
|
||||
from .base import BaseObjectView
|
||||
from .mixins import ActionsMixin, TableMixin
|
||||
from .utils import get_prerequisite_model
|
||||
@ -436,8 +436,7 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView):
|
||||
|
||||
# If this is an HTMX request, return only the rendered deletion form as modal content
|
||||
if htmx_partial(request):
|
||||
viewname = get_viewname(self.queryset.model, action='delete')
|
||||
form_url = reverse(viewname, kwargs={'pk': obj.pk})
|
||||
form_url = get_action_url(self.queryset.model, action='delete', kwargs={'pk': obj.pk})
|
||||
return render(request, 'htmx/delete_form.html', {
|
||||
'object': obj,
|
||||
'object_type': self.queryset.model._meta.verbose_name,
|
||||
|
@ -10,11 +10,9 @@
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ object.object|meta:"verbose_name_plural"|bettertitle }}</a>
|
||||
</li>
|
||||
{% with parent_jobs_viewname=object.object|viewname:"jobs" %}
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url parent_jobs_viewname pk=object.object.pk %}">{{ object.object }}</a>
|
||||
</li>
|
||||
{% endwith %}
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% action_url object.object 'jobs' pk=object.object.pk %}">{{ object.object }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'core:job_list' %}?name={{ object.name|urlencode }}">{{ object.name }}</a>
|
||||
|
@ -23,11 +23,9 @@
|
||||
{{ term.device|linkify }}
|
||||
<i class="mdi mdi-chevron-right" aria-hidden="true"></i>
|
||||
{{ term|linkify }}
|
||||
{% with trace_url=term|viewname:"trace" %}
|
||||
<a href="{% url trace_url pk=term.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
|
||||
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endwith %}
|
||||
<a href="{% action_url term 'trace' pk=term.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
|
||||
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% if not forloop.last %}<br/>{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
@ -47,11 +45,9 @@
|
||||
<td>
|
||||
{% for term in terminations %}
|
||||
{{ term|linkify }}
|
||||
{% with trace_url=term|viewname:"trace" %}
|
||||
<a href="{% url trace_url pk=term.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
|
||||
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endwith %}
|
||||
<a href="{% action_url term 'trace' pk=term.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
|
||||
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% if not forloop.last %}<br/>{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
@ -67,11 +63,9 @@
|
||||
<td>
|
||||
{% for term in terminations %}
|
||||
{{ term.circuit|linkify }} ({{ term }})
|
||||
{% with trace_url=term|viewname:"trace" %}
|
||||
<a href="{% url trace_url pk=term.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
|
||||
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endwith %}
|
||||
<a href="{% action_url term 'trace' pk=term.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
|
||||
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% if not forloop.last %}<br/>{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
|
@ -7,6 +7,6 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-danger text-center">
|
||||
<i class="mdi mdi-alert"></i> {% trans "Unable to load content. Invalid view name" %}: <span class="font-monospace">{{ viewname }}</span>
|
||||
<i class="mdi mdi-alert"></i> {% trans "Unable to load content. Could not resolve list URL for:" %} <span class="font-monospace">{{ model_name }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li class="breadcrumb-item"><a href="{% url object.assigned_object|viewname:'journal' pk=object.assigned_object.pk %}">{{ object.assigned_object }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% action_url object.assigned_object 'journal' pk=object.assigned_object.pk %}">{{ object.assigned_object }}</a></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -6,11 +6,9 @@
|
||||
|
||||
{% block extra_controls %}
|
||||
{% if perms.extras.add_imageattachment %}
|
||||
{% with viewname=object|viewname:"image-attachments" %}
|
||||
<a href="{% url 'extras:imageattachment_add' %}?object_type={{ object|content_type_id }}&object_id={{ object.pk }}&return_url={% url viewname pk=object.pk %}" class="btn btn-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Attach an Image" %}
|
||||
</a>
|
||||
{% endwith %}
|
||||
<a href="{% url 'extras:imageattachment_add' %}?object_type={{ object|content_type_id }}&object_id={{ object.pk }}&return_url={% action_url object 'image-attachments' pk=object.pk %}" class="btn btn-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Attach an Image" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -61,19 +61,18 @@
|
||||
<h2 class="card-header">{% trans "Tagged Item Types" %}</h2>
|
||||
<ul class="list-group list-group-flush" role="presentation">
|
||||
{% for object_type in object_types %}
|
||||
{% with viewname=object_type.content_type.model_class|validated_viewname:"list" %}
|
||||
{% if viewname %}
|
||||
<a href="{% url viewname %}?tag={{ object.slug }}" class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||
{{ object_type.content_type.name|bettertitle }}
|
||||
<span class="badge text-bg-primary rounded-pill">{{ object_type.item_count }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<li class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||
{{ object_type.content_type.name|bettertitle }}
|
||||
<span class="badge text-bg-primary rounded-pill">{{ object_type.item_count }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% action_url object_type.content_type.model_class 'list' as list_url %}
|
||||
{% if list_url %}
|
||||
<a href="{{ list_url }}?tag={{ object.slug }}" class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||
{{ object_type.content_type.name|bettertitle }}
|
||||
<span class="badge text-bg-primary rounded-pill">{{ object_type.item_count }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<li class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||
{{ object_type.content_type.name|bettertitle }}
|
||||
<span class="badge text-bg-primary rounded-pill">{{ object_type.item_count }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -33,7 +33,7 @@ Context:
|
||||
<ol class="breadcrumb" aria-label="breadcrumbs">
|
||||
{% block breadcrumbs %}
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url object|viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a>
|
||||
<a href="{% action_url object 'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a>
|
||||
</li>
|
||||
{% endblock breadcrumbs %}
|
||||
</ol>
|
||||
|
@ -10,7 +10,7 @@
|
||||
</div>
|
||||
<div class="modal-body row">
|
||||
<form
|
||||
hx-post="{% url model|viewname:"add" %}?_quickadd=True&target={{ request.GET.target }}"
|
||||
hx-post="{% action_url model 'add' %}?_quickadd=True&target={{ request.GET.target }}"
|
||||
hx-target="#htmx-modal-content"
|
||||
enctype="multipart/form-data"
|
||||
>
|
||||
|
@ -5,9 +5,9 @@
|
||||
<h2 class="card-header">{% trans "Related Objects" %}</h2>
|
||||
<ul class="list-group list-group-flush" role="presentation">
|
||||
{% for qs, filter_param in related_models %}
|
||||
{% with viewname=qs.model|validated_viewname:"list" %}
|
||||
{% if viewname is not None %}
|
||||
<a href="{% url viewname %}?{{ filter_param }}={{ object.pk }}" class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||
{% action_url qs.model 'list' as list_url %}
|
||||
{% if list_url %}
|
||||
<a href="{{ list_url }}?{{ filter_param }}={{ object.pk }}" class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||
{{ qs.model|meta:"verbose_name_plural"|bettertitle }}
|
||||
{% with count=qs.count %}
|
||||
{% if count %}
|
||||
@ -17,8 +17,7 @@
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% empty %}
|
||||
<span class="list-group-item text-muted">{% trans "None" %}</span>
|
||||
{% endfor %}
|
||||
|
@ -4,10 +4,8 @@
|
||||
|
||||
{% block extra_controls %}
|
||||
{% if perms.tenancy.add_contactassignment %}
|
||||
{% with viewname=object|viewname:"contacts" %}
|
||||
<a href="{% url 'tenancy:contactassignment_add' %}?object_type={{ object|content_type_id }}&object_id={{ object.pk }}&return_url={% url viewname pk=object.pk %}" class="btn btn-primary">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a contact" %}
|
||||
</a>
|
||||
{% endwith %}
|
||||
<a href="{% url 'tenancy:contactassignment_add' %}?object_type={{ object|content_type_id }}&object_id={{ object.pk }}&return_url={% action_url object 'contacts' pk=object.pk %}" class="btn btn-primary">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a contact" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
@ -2,10 +2,9 @@ import django_filters
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.forms import BoundField
|
||||
from django.urls import reverse
|
||||
|
||||
from utilities.forms import widgets
|
||||
from utilities.views import get_viewname
|
||||
from utilities.views import get_action_url
|
||||
|
||||
__all__ = (
|
||||
'DynamicChoiceField',
|
||||
@ -173,13 +172,12 @@ class DynamicModelChoiceMixin:
|
||||
|
||||
# Set the data URL on the APISelect widget (if not already set)
|
||||
if not widget.attrs.get('data-url'):
|
||||
viewname = get_viewname(self.queryset.model, action='list', rest_api=True)
|
||||
widget.attrs['data-url'] = reverse(viewname)
|
||||
widget.attrs['data-url'] = get_action_url(self.queryset.model, action='list', rest_api=True)
|
||||
|
||||
# Include quick add?
|
||||
if self.quick_add:
|
||||
widget.quick_add_context = {
|
||||
'url': reverse(get_viewname(self.model, 'add')),
|
||||
'url': get_action_url(self.model, action='add'),
|
||||
'params': {},
|
||||
}
|
||||
for k, v in self.quick_add_params.items():
|
||||
|
@ -8,7 +8,7 @@ from core.models import ObjectType
|
||||
from extras.models import Bookmark, ExportTemplate, Subscription
|
||||
from netbox.models.features import NotificationsMixin
|
||||
from utilities.querydict import prepare_cloned_fields
|
||||
from utilities.views import get_viewname
|
||||
from utilities.views import get_action_url
|
||||
|
||||
__all__ = (
|
||||
'action_buttons',
|
||||
@ -110,9 +110,8 @@ def subscribe_button(context, instance):
|
||||
@register.inclusion_tag('buttons/clone.html')
|
||||
def clone_button(instance):
|
||||
# Resolve URL path
|
||||
viewname = get_viewname(instance, 'add')
|
||||
try:
|
||||
url = reverse(viewname)
|
||||
url = get_action_url(instance, action='add')
|
||||
except NoReverseMatch:
|
||||
return {
|
||||
'url': None,
|
||||
@ -128,8 +127,7 @@ def clone_button(instance):
|
||||
# TODO: Remove in NetBox v4.6
|
||||
@register.inclusion_tag('buttons/edit.html')
|
||||
def edit_button(instance):
|
||||
viewname = get_viewname(instance, 'edit')
|
||||
url = reverse(viewname, kwargs={'pk': instance.pk})
|
||||
url = get_action_url(instance, action='edit', kwargs={'pk': instance.pk})
|
||||
|
||||
return {
|
||||
'url': url,
|
||||
@ -140,8 +138,7 @@ def edit_button(instance):
|
||||
# TODO: Remove in NetBox v4.6
|
||||
@register.inclusion_tag('buttons/delete.html')
|
||||
def delete_button(instance):
|
||||
viewname = get_viewname(instance, 'delete')
|
||||
url = reverse(viewname, kwargs={'pk': instance.pk})
|
||||
url = get_action_url(instance, action='delete', kwargs={'pk': instance.pk})
|
||||
|
||||
return {
|
||||
'url': url,
|
||||
@ -152,8 +149,7 @@ def delete_button(instance):
|
||||
# TODO: Remove in NetBox v4.6
|
||||
@register.inclusion_tag('buttons/sync.html')
|
||||
def sync_button(instance):
|
||||
viewname = get_viewname(instance, 'sync')
|
||||
url = reverse(viewname, kwargs={'pk': instance.pk})
|
||||
url = get_action_url(instance, action='sync', kwargs={'pk': instance.pk})
|
||||
|
||||
return {
|
||||
'label': _('Sync'),
|
||||
@ -169,7 +165,7 @@ def sync_button(instance):
|
||||
@register.inclusion_tag('buttons/add.html')
|
||||
def add_button(model, action='add'):
|
||||
try:
|
||||
url = reverse(get_viewname(model, action))
|
||||
url = get_action_url(model, action=action)
|
||||
except NoReverseMatch:
|
||||
url = None
|
||||
|
||||
@ -183,7 +179,7 @@ def add_button(model, action='add'):
|
||||
@register.inclusion_tag('buttons/import.html')
|
||||
def import_button(model, action='bulk_import'):
|
||||
try:
|
||||
url = reverse(get_viewname(model, action))
|
||||
url = get_action_url(model, action=action)
|
||||
except NoReverseMatch:
|
||||
url = None
|
||||
|
||||
@ -219,7 +215,7 @@ def export_button(context, model):
|
||||
@register.inclusion_tag('buttons/bulk_edit.html', takes_context=True)
|
||||
def bulk_edit_button(context, model, action='bulk_edit', query_params=None):
|
||||
try:
|
||||
url = reverse(get_viewname(model, action))
|
||||
url = get_action_url(model, action=action)
|
||||
if query_params:
|
||||
url = f'{url}?{query_params.urlencode()}'
|
||||
except NoReverseMatch:
|
||||
@ -236,7 +232,7 @@ def bulk_edit_button(context, model, action='bulk_edit', query_params=None):
|
||||
@register.inclusion_tag('buttons/bulk_delete.html', takes_context=True)
|
||||
def bulk_delete_button(context, model, action='bulk_delete', query_params=None):
|
||||
try:
|
||||
url = reverse(get_viewname(model, action))
|
||||
url = get_action_url(model, action=action)
|
||||
if query_params:
|
||||
url = f'{url}?{query_params.urlencode()}'
|
||||
except NoReverseMatch:
|
||||
|
@ -4,13 +4,15 @@ from urllib.parse import quote
|
||||
|
||||
from django import template
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django.utils.html import conditional_escape
|
||||
|
||||
from core.models import ObjectType
|
||||
from utilities.forms import get_selected_values, TableConfigForm
|
||||
from utilities.views import get_viewname
|
||||
from utilities.views import get_viewname, get_action_url
|
||||
from netbox.settings import DISK_BASE_UNIT, RAM_BASE_UNIT
|
||||
|
||||
__all__ = (
|
||||
'action_url',
|
||||
'applied_filters',
|
||||
'as_range',
|
||||
'divide',
|
||||
@ -63,6 +65,125 @@ def validated_viewname(model, action):
|
||||
return None
|
||||
|
||||
|
||||
class ActionURLNode(template.Node):
|
||||
"""Template node for the {% action_url %} template tag."""
|
||||
|
||||
child_nodelists = ()
|
||||
|
||||
def __init__(self, model, action, kwargs, asvar=None):
|
||||
self.model = model
|
||||
self.action = action
|
||||
self.kwargs = kwargs
|
||||
self.asvar = asvar
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<{self.__class__.__qualname__} "
|
||||
f"model='{self.model}' "
|
||||
f"action='{self.action}' "
|
||||
f"kwargs={repr(self.kwargs)} "
|
||||
f"as={repr(self.asvar)}>"
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
"""
|
||||
Render the action URL node.
|
||||
|
||||
Args:
|
||||
context: The template context
|
||||
|
||||
Returns:
|
||||
The resolved URL or empty string if using 'as' syntax
|
||||
|
||||
Raises:
|
||||
NoReverseMatch: If the URL cannot be resolved and not using 'as' syntax
|
||||
"""
|
||||
# Resolve model and kwargs from context
|
||||
model = self.model.resolve(context)
|
||||
kwargs = {k: v.resolve(context) for k, v in self.kwargs.items()}
|
||||
|
||||
# Get the action URL using the utility function
|
||||
try:
|
||||
url = get_action_url(model, action=self.action, kwargs=kwargs)
|
||||
except NoReverseMatch:
|
||||
if self.asvar is None:
|
||||
raise
|
||||
url = ""
|
||||
|
||||
# Handle variable assignment or return escaped URL
|
||||
if self.asvar:
|
||||
context[self.asvar] = url
|
||||
return ""
|
||||
|
||||
return conditional_escape(url) if context.autoescape else url
|
||||
|
||||
|
||||
@register.tag
|
||||
def action_url(parser, token):
|
||||
"""
|
||||
Return an absolute URL matching the given model and action.
|
||||
|
||||
This is a way to define links that aren't tied to a particular URL
|
||||
configuration::
|
||||
|
||||
{% action_url model "action_name" %}
|
||||
|
||||
or
|
||||
|
||||
{% action_url model "action_name" pk=object.pk %}
|
||||
|
||||
or
|
||||
|
||||
{% action_url model "action_name" pk=object.pk as variable_name %}
|
||||
|
||||
The first argument is a model or instance. The second argument is the action name.
|
||||
Additional keyword arguments can be passed for URL parameters.
|
||||
|
||||
For example, if you have a Device model and want to link to its edit action::
|
||||
|
||||
{% action_url device "edit" %}
|
||||
|
||||
This will generate a URL like ``/dcim/devices/123/edit/``.
|
||||
|
||||
You can also pass additional parameters::
|
||||
|
||||
{% action_url device "journal" pk=device.pk %}
|
||||
|
||||
Or assign the URL to a variable::
|
||||
|
||||
{% action_url device "edit" as edit_url %}
|
||||
"""
|
||||
# Parse the token contents
|
||||
bits = token.split_contents()
|
||||
if len(bits) < 3:
|
||||
raise template.TemplateSyntaxError(
|
||||
f"'{bits[0]}' takes at least two arguments, a model and an action."
|
||||
)
|
||||
|
||||
# Extract model and action
|
||||
model = parser.compile_filter(bits[1])
|
||||
action = bits[2].strip('"\'') # Remove quotes from literal string
|
||||
kwargs = {}
|
||||
asvar = None
|
||||
bits = bits[3:]
|
||||
|
||||
# Handle 'as' syntax for variable assignment
|
||||
if len(bits) >= 2 and bits[-2] == "as":
|
||||
asvar = bits[-1]
|
||||
bits = bits[:-2]
|
||||
|
||||
# Parse remaining arguments as kwargs
|
||||
for bit in bits:
|
||||
if '=' not in bit:
|
||||
raise template.TemplateSyntaxError(
|
||||
f"'{token.contents.split()[0]}' keyword arguments must be in the format 'name=value'"
|
||||
)
|
||||
name, value = bit.split('=', 1)
|
||||
kwargs[name] = parser.compile_filter(value)
|
||||
|
||||
return ActionURLNode(model, action, kwargs, asvar)
|
||||
|
||||
|
||||
@register.filter()
|
||||
def humanize_speed(speed):
|
||||
"""
|
||||
|
@ -1,10 +1,9 @@
|
||||
from django import template
|
||||
from django.urls import reverse
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from netbox.registry import registry
|
||||
from utilities.views import get_viewname
|
||||
from utilities.views import get_action_url
|
||||
|
||||
__all__ = (
|
||||
'model_view_tabs',
|
||||
@ -39,10 +38,9 @@ def model_view_tabs(context, instance):
|
||||
continue
|
||||
|
||||
if attrs := tab.render(instance):
|
||||
viewname = get_viewname(instance, action=config['name'])
|
||||
active_tab = context.get('tab')
|
||||
try:
|
||||
url = reverse(viewname, args=[instance.pk])
|
||||
url = get_action_url(instance, action=config['name'], kwargs={'pk': instance.pk})
|
||||
except NoReverseMatch:
|
||||
# No URL has been registered for this view; skip
|
||||
continue
|
||||
|
@ -20,6 +20,7 @@ __all__ = (
|
||||
'GetReturnURLMixin',
|
||||
'ObjectPermissionRequiredMixin',
|
||||
'ViewTab',
|
||||
'get_action_url',
|
||||
'get_viewname',
|
||||
'register_model_view',
|
||||
)
|
||||
@ -150,7 +151,7 @@ class GetReturnURLMixin:
|
||||
# Attempt to dynamically resolve the list view for the object
|
||||
if hasattr(self, 'queryset'):
|
||||
try:
|
||||
return reverse(get_viewname(self.queryset.model, 'list'))
|
||||
return get_action_url(self.queryset.model, action='list')
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
|
||||
@ -282,6 +283,22 @@ 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; otherwise raise NoReverseMatch.
|
||||
Will defer to _get_action_url() on the model if it exists.
|
||||
|
||||
:param model: The model or instance to which the URL belongs
|
||||
: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 action
|
||||
:param kwargs: A dictionary of keyword arguments for the view to include when resolving 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
|
||||
|
Loading…
Reference in New Issue
Block a user