Coverage for netbox/ui/breadcrumbs.py: 28%
60 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from django.template.loader import render_to_string
2from django.urls import NoReverseMatch, reverse
4from utilities.data import resolve_attr_path
6__all__ = (
7 'Breadcrumb',
8 'filtered_list_url',
9 'get_root_breadcrumb',
10 'object_view_url',
11)
14def filtered_list_url(viewname, filter_param):
15 """
16 Return a callable suitable for a `Breadcrumb`'s `url`, linking to a list view filtered by the
17 resolved object's primary key. For example, `filtered_list_url('dcim:rack_list', 'site_id')`
18 produces a URL of the form `{rack_list}?site_id=<pk>`.
19 """
20 return lambda obj: f"{reverse(viewname)}?{filter_param}={obj.pk}"
23def object_view_url(viewname):
24 """
25 Return a callable suitable for a `Breadcrumb`'s `url`, linking to a view keyed by the resolved
26 object's primary key. For example, `object_view_url('dcim:device_interfaces')` produces the URL
27 `reverse('dcim:device_interfaces', kwargs={'pk': <pk>})`.
28 """
29 return lambda obj: reverse(viewname, kwargs={'pk': obj.pk})
32def get_root_breadcrumb(instance):
33 """
34 Return the default root `Breadcrumb` for a model instance: a link to the model's list view,
35 labeled with the model's plural verbose name. This is prepended automatically to every object
36 view's trail unless the view's layout opts out via `root_breadcrumb=False`.
37 """
38 from utilities.templatetags.builtins.filters import bettertitle
39 from utilities.views import get_action_url
41 label = bettertitle(instance._meta.verbose_name_plural)
42 try:
43 url = get_action_url(instance, 'list')
44 except NoReverseMatch:
45 url = None
46 return Breadcrumb(label=label, url=url)
49class Breadcrumb:
50 """
51 A navigation breadcrumb rendered at the top of an object view.
53 Rather than wrapping a static value, a breadcrumb typically references an attribute on the object being viewed.
54 This allows breadcrumbs to be declared once on a layout (alongside its panels) and rendered dynamically for each
55 object. A breadcrumb whose resolved value is empty renders as an empty string and is omitted, which simplifies
56 conditional breadcrumbs (e.g. where a device may or may not be assigned to a rack).
58 A breadcrumb may instead define a static `label`, omitting the accessor entirely. This renders a single
59 breadcrumb describing the viewed object directly (or a fixed destination) rather than a related object, which is
60 useful for linking to a parent view that isn't a related object (e.g. a user's personal token list) or for an
61 unlinked descriptive crumb (e.g. "Units 1-5" on a rack reservation).
63 Attributes:
64 template_name (str): The name of the template used to render the breadcrumb
66 Parameters:
67 accessor: The dotted path to the related object on the viewed instance (e.g. "site" or "device.rack"),
68 or a callable which accepts the instance and returns the related object. If the resolved value is an
69 iterable of objects, a breadcrumb is rendered for each (e.g. to represent a hierarchy of ancestors).
70 Omit this (and pass `label`) to render a single breadcrumb describing the viewed object itself.
71 label: A label for the breadcrumb. Required when `accessor` is omitted. May be a string, or a callable
72 which accepts the relevant object (the resolved related object when an accessor is given, otherwise the
73 viewed instance) and returns the label. When an accessor is given and no label is set, the resolved
74 object's string representation is used.
75 url: An optional URL for the breadcrumb's link. May be a string, or a callable which accepts the relevant
76 object and returns a URL. When an accessor is given and no url is set, the resolved object's
77 `get_absolute_url()` is used where available; an accessor-less breadcrumb is left unlinked instead.
78 """
79 template_name = 'ui/breadcrumb.html'
81 def __init__(self, accessor=None, label=None, url=None):
82 if accessor is None and label is None: 82 ↛ 83line 82 didn't jump to line 83 because the condition on line 82 was never true
83 raise ValueError("A Breadcrumb must define an accessor, a static label, or both.")
84 self.accessor = accessor
85 self.label = label
86 self.url = url
88 def resolve(self, instance):
89 """
90 Resolve the breadcrumb's accessor against the viewed instance and return the related object(s).
91 """
92 if callable(self.accessor):
93 return self.accessor(instance)
94 return resolve_attr_path(instance, self.accessor)
96 def get_label(self, obj):
97 """
98 Return the breadcrumb's label for the given object, falling back to its string representation.
99 """
100 if callable(self.label):
101 return self.label(obj) if obj is not None else None
102 if self.label is not None:
103 return self.label
104 return str(obj) if obj is not None else None
106 def get_url(self, obj, fallback=True):
107 """
108 Return the URL to link the given object to, or None for an unlinked breadcrumb. When `fallback` is True,
109 an object's `get_absolute_url()` is used in the absence of an explicit url.
110 """
111 if self.url is not None:
112 return self.url(obj) if callable(self.url) else self.url
113 if fallback and obj is not None and hasattr(obj, 'get_absolute_url'):
114 return obj.get_absolute_url()
115 return None
117 def render(self, context=None):
118 instance = context.get('object') if context else None
120 # A breadcrumb without an accessor describes the viewed object directly (or a fixed destination), rather
121 # than a related object, and renders a single crumb. It is left unlinked unless an explicit url is given.
122 if self.accessor is None:
123 label = self.get_label(instance)
124 if not label:
125 return ''
126 return render_to_string(self.template_name, {
127 'url': self.get_url(instance, fallback=False),
128 'label': label,
129 })
131 if instance is None:
132 return ''
133 value = self.resolve(instance)
134 if value is None:
135 return ''
137 # A resolved iterable (e.g. a queryset of ancestors) yields one breadcrumb per object
138 objects = value if self._is_iterable(value) else [value]
140 return ''.join(
141 render_to_string(self.template_name, {
142 'url': self.get_url(obj),
143 'label': self.get_label(obj),
144 })
145 for obj in objects if obj is not None
146 )
148 @staticmethod
149 def _is_iterable(value):
150 if isinstance(value, (str, bytes)):
151 return False
152 return hasattr(value, '__iter__')