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 }}
-
{{ object.assigned_object }}
+ {{ object.assigned_object }}
{% 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):
"""