#20048 review feedback

This commit is contained in:
Arthur 2025-08-08 14:53:48 -07:00
parent 025afb6b1c
commit 0f53bfdc19
6 changed files with 67 additions and 64 deletions

View File

@ -11,7 +11,7 @@ from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Model from django.db.models import Model
from django.template.loader import render_to_string 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 django.utils.translation import gettext as _
from core.models import ObjectType from core.models import ObjectType
@ -53,9 +53,9 @@ def object_list_widget_supports_model(model: Model) -> bool:
""" """
def can_resolve_model_list_view(model: Model) -> bool: def can_resolve_model_list_view(model: Model) -> bool:
try: try:
reverse(get_action_url(model, action='list')) get_action_url(model, action='list')
return True return True
except Exception: except NoReverseMatch:
return False return False
tests = [ tests = [

View File

@ -45,4 +45,4 @@ class ObjectListWidgetTests(TestCase):
mock_request = Request() mock_request = Request()
widget = ObjectListWidget(id='2829fd9b-5dee-4c9a-81f2-5bd84c350a27', **config) widget = ObjectListWidget(id='2829fd9b-5dee-4c9a-81f2-5bd84c350a27', **config)
rendered = widget.render(mock_request) rendered = widget.render(mock_request)
self.assertTrue('Unable to load content. Invalid list viewname for:' in rendered) self.assertTrue('Unable to load content. Could not resolve list URL for:' in rendered)

View File

@ -285,7 +285,7 @@ class ActionsColumn(tables.Column):
for idx, (action, attrs) in enumerate(self.actions.items()): for idx, (action, attrs) in enumerate(self.actions.items()):
permission = get_permission_for_model(model, attrs.permission) permission = get_permission_for_model(model, attrs.permission)
if attrs.permission is None or user.has_perm(permission): if attrs.permission is None or user.has_perm(permission):
url = get_action_url(record, action=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 # 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): if len(self.actions) == 1 or (self.split_actions and idx == 0):

View File

@ -7,6 +7,6 @@
</div> </div>
{% else %} {% else %}
<div class="text-danger text-center"> <div class="text-danger text-center">
<i class="mdi mdi-alert"></i> {% trans "Unable to load content. Invalid list viewname for:" %} <span class="font-monospace">{{ model_name }}</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> </div>
{% endif %} {% endif %}

View File

@ -12,6 +12,7 @@ from utilities.views import get_viewname, get_action_url
from netbox.settings import DISK_BASE_UNIT, RAM_BASE_UNIT from netbox.settings import DISK_BASE_UNIT, RAM_BASE_UNIT
__all__ = ( __all__ = (
'action_url',
'applied_filters', 'applied_filters',
'as_range', 'as_range',
'divide', 'divide',
@ -64,6 +65,59 @@ def validated_viewname(model, action):
return None 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 @register.tag
def action_url(parser, token): def action_url(parser, token):
""" """
@ -82,7 +136,7 @@ def action_url(parser, token):
{% action_url model "action_name" pk=object.pk as variable_name %} {% action_url model "action_name" pk=object.pk as variable_name %}
The first argument is a model instance. The second argument is the action 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. 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:: For example, if you have a Device model and want to link to its edit action::
@ -99,59 +153,6 @@ def action_url(parser, token):
{% action_url device "edit" as edit_url %} {% action_url device "edit" as edit_url %}
""" """
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
# Parse the token contents # Parse the token contents
bits = token.split_contents() bits = token.split_contents()
if len(bits) < 3: if len(bits) < 3:

View File

@ -20,6 +20,7 @@ __all__ = (
'GetReturnURLMixin', 'GetReturnURLMixin',
'ObjectPermissionRequiredMixin', 'ObjectPermissionRequiredMixin',
'ViewTab', 'ViewTab',
'get_action_url',
'get_viewname', 'get_viewname',
'register_model_view', 'register_model_view',
) )
@ -284,12 +285,13 @@ def get_viewname(model, action=None, rest_api=False):
def get_action_url(model, action=None, rest_api=False, kwargs=None): def get_action_url(model, action=None, rest_api=False, kwargs=None):
""" """
Return the url for the given model and action, if valid. 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 view applies :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 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 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 registering its URL path (optional). :param kwargs: A dictionary of keyword arguments for the view to include when resolving its URL path (optional)
""" """
if hasattr(model, '_get_action_url'): if hasattr(model, '_get_action_url'):
return model._get_action_url(action, rest_api, kwargs) return model._get_action_url(action, rest_api, kwargs)