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

1from django.template.loader import render_to_string 

2from django.urls import NoReverseMatch, reverse 

3 

4from utilities.data import resolve_attr_path 

5 

6__all__ = ( 

7 'Breadcrumb', 

8 'filtered_list_url', 

9 'get_root_breadcrumb', 

10 'object_view_url', 

11) 

12 

13 

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}" 

21 

22 

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}) 

30 

31 

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 

40 

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) 

47 

48 

49class Breadcrumb: 

50 """ 

51 A navigation breadcrumb rendered at the top of an object view. 

52 

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). 

57 

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). 

62 

63 Attributes: 

64 template_name (str): The name of the template used to render the breadcrumb 

65 

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' 

80 

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 

87 

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) 

95 

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 

105 

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 

116 

117 def render(self, context=None): 

118 instance = context.get('object') if context else None 

119 

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 }) 

130 

131 if instance is None: 

132 return '' 

133 value = self.resolve(instance) 

134 if value is None: 

135 return '' 

136 

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] 

139 

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 ) 

147 

148 @staticmethod 

149 def _is_iterable(value): 

150 if isinstance(value, (str, bytes)): 

151 return False 

152 return hasattr(value, '__iter__')