diff --git a/netbox/templates/extras/journalentry.html b/netbox/templates/extras/journalentry.html index 7d75ab2dc..23aa881ff 100644 --- a/netbox/templates/extras/journalentry.html +++ b/netbox/templates/extras/journalentry.html @@ -5,7 +5,7 @@ {% block breadcrumbs %} {{ block.super }} - + {% endblock %} {% block content %} diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 2f175d2b6..00df1e829 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -4,10 +4,11 @@ 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__ = ( @@ -63,6 +64,125 @@ def validated_viewname(model, action): return None +@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 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 %} + """ + + 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 + 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): """