mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-04 16:37:15 +02:00
Clean up object attrs
This commit is contained in:
@@ -74,7 +74,7 @@ class RackReservationPanel(panels.ObjectAttributesPanel):
|
||||
unit_count = attrs.TextAttr('unit_count', label=_("Total U's"))
|
||||
status = attrs.ChoiceAttr('status')
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
user = attrs.RelatedObjectAttr('user')
|
||||
user = attrs.RelatedObjectAttr('user', linkify=True)
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
@@ -219,8 +219,8 @@ class PowerPortPanel(panels.ObjectAttributesPanel):
|
||||
label = attrs.TextAttr('label')
|
||||
type = attrs.ChoiceAttr('type')
|
||||
description = attrs.TextAttr('description')
|
||||
maximum_draw = attrs.TextAttr('maximum_draw')
|
||||
allocated_draw = attrs.TextAttr('allocated_draw')
|
||||
maximum_draw = attrs.TextAttr('maximum_draw', format_string='{}W')
|
||||
allocated_draw = attrs.TextAttr('allocated_draw', format_string='{}W')
|
||||
|
||||
|
||||
class PowerOutletPanel(panels.ObjectAttributesPanel):
|
||||
@@ -243,7 +243,7 @@ class FrontPortPanel(panels.ObjectAttributesPanel):
|
||||
label = attrs.TextAttr('label')
|
||||
type = attrs.ChoiceAttr('type')
|
||||
color = attrs.ColorAttr('color')
|
||||
positions = attrs.TextAttr('positions')
|
||||
positions = attrs.NumericAttr('positions')
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ class RearPortPanel(panels.ObjectAttributesPanel):
|
||||
label = attrs.TextAttr('label')
|
||||
type = attrs.ChoiceAttr('type')
|
||||
color = attrs.ColorAttr('color')
|
||||
positions = attrs.TextAttr('positions')
|
||||
positions = attrs.NumericAttr('positions')
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
@@ -472,7 +472,7 @@ class InterfacePanel(panels.ObjectAttributesPanel):
|
||||
type = attrs.ChoiceAttr('type')
|
||||
speed = attrs.TemplatedAttr('speed', template_name='dcim/interface/attrs/speed.html', label=_('Speed'))
|
||||
duplex = attrs.ChoiceAttr('duplex')
|
||||
mtu = attrs.TextAttr('mtu', label=_('MTU'))
|
||||
mtu = attrs.NumericAttr('mtu', label=_('MTU'))
|
||||
enabled = attrs.BooleanAttr('enabled')
|
||||
mgmt_only = attrs.BooleanAttr('mgmt_only', label=_('Management only'))
|
||||
description = attrs.TextAttr('description')
|
||||
@@ -481,7 +481,7 @@ class InterfacePanel(panels.ObjectAttributesPanel):
|
||||
mode = attrs.ChoiceAttr('mode', label=_('802.1Q mode'))
|
||||
qinq_svlan = attrs.RelatedObjectAttr('qinq_svlan', linkify=True, label=_('Q-in-Q SVLAN'))
|
||||
untagged_vlan = attrs.RelatedObjectAttr('untagged_vlan', linkify=True, label=_('Untagged VLAN'))
|
||||
tx_power = attrs.TextAttr('tx_power', label=_('Transmit power (dBm)'))
|
||||
tx_power = attrs.TextAttr('tx_power', label=_('Transmit power'), format_string='{} dBm')
|
||||
tunnel = attrs.RelatedObjectAttr('tunnel_termination.tunnel', linkify=True, label=_('Tunnel'))
|
||||
l2vpn = attrs.RelatedObjectAttr('l2vpn_termination.l2vpn', linkify=True, label=_('L2VPN'))
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ class VRFDisplayAttr(attrs.ObjectAttribute):
|
||||
"""
|
||||
Renders a VRF reference, displaying 'Global' when no VRF is assigned. Optionally includes
|
||||
the route distinguisher (RD).
|
||||
|
||||
Parameters:
|
||||
show_rd (bool): If true, the VRF's RD will be included. (Default: False)
|
||||
"""
|
||||
template_name = 'ipam/attrs/vrf.html'
|
||||
|
||||
@@ -14,11 +17,17 @@ class VRFDisplayAttr(attrs.ObjectAttribute):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.show_rd = show_rd
|
||||
|
||||
def render(self, obj, context):
|
||||
value = self.get_value(obj)
|
||||
return render_to_string(self.template_name, {
|
||||
**self.get_context(obj, context),
|
||||
'name': context['name'],
|
||||
'value': value,
|
||||
def get_context(self, obj, attr, value, context):
|
||||
return {
|
||||
'show_rd': self.show_rd,
|
||||
}
|
||||
|
||||
def render(self, obj, context):
|
||||
name = context['name']
|
||||
value = self.get_value(obj)
|
||||
|
||||
return render_to_string(self.template_name, {
|
||||
**self.get_context(obj, name, value, context),
|
||||
'name': name,
|
||||
'value': value,
|
||||
})
|
||||
|
||||
@@ -76,7 +76,7 @@ class ChoiceAttrTest(TestCase):
|
||||
self.termination.get_role_display(),
|
||||
)
|
||||
self.assertEqual(
|
||||
attr.get_context(self.termination, {}),
|
||||
attr.get_context(self.termination, 'role', attr.get_value(self.termination), {}),
|
||||
{'bg_color': self.termination.get_role_color()},
|
||||
)
|
||||
|
||||
@@ -88,7 +88,7 @@ class ChoiceAttrTest(TestCase):
|
||||
self.termination.interface.get_type_display(),
|
||||
)
|
||||
self.assertEqual(
|
||||
attr.get_context(self.termination, {}),
|
||||
attr.get_context(self.termination, 'interface.type', attr.get_value(self.termination), {}),
|
||||
{'bg_color': None},
|
||||
)
|
||||
|
||||
@@ -100,7 +100,9 @@ class ChoiceAttrTest(TestCase):
|
||||
self.termination.virtual_circuit.get_status_display(),
|
||||
)
|
||||
self.assertEqual(
|
||||
attr.get_context(self.termination, {}),
|
||||
attr.get_context(
|
||||
self.termination, 'virtual_circuit.status', attr.get_value(self.termination), {}
|
||||
),
|
||||
{'bg_color': self.termination.virtual_circuit.get_status_color()},
|
||||
)
|
||||
|
||||
|
||||
@@ -29,11 +29,27 @@ PLACEHOLDER_HTML = '<span class="text-muted">—</span>'
|
||||
|
||||
IMAGE_DECODING_CHOICES = ('auto', 'async', 'sync')
|
||||
|
||||
|
||||
#
|
||||
# Mixins
|
||||
#
|
||||
|
||||
class MapURLMixin:
|
||||
_map_url = None
|
||||
|
||||
@property
|
||||
def map_url(self):
|
||||
if self._map_url is True:
|
||||
return get_config().MAPS_URL
|
||||
if self._map_url:
|
||||
return self._map_url
|
||||
return None
|
||||
|
||||
|
||||
#
|
||||
# Attributes
|
||||
#
|
||||
|
||||
|
||||
class ObjectAttribute:
|
||||
"""
|
||||
Base class for representing an attribute of an object.
|
||||
@@ -64,17 +80,20 @@ class ObjectAttribute:
|
||||
"""
|
||||
return resolve_attr_path(obj, self.accessor)
|
||||
|
||||
def get_context(self, obj, context):
|
||||
def get_context(self, obj, attr, value, context):
|
||||
"""
|
||||
Return any additional template context used to render the attribute value.
|
||||
|
||||
Parameters:
|
||||
obj (object): The object for which the attribute is being rendered
|
||||
attr (str): The name of the attribute being rendered
|
||||
value: The value of the attribute on the object
|
||||
context (dict): The root template context
|
||||
"""
|
||||
return {}
|
||||
|
||||
def render(self, obj, context):
|
||||
name = context['name']
|
||||
value = self.get_value(obj)
|
||||
|
||||
# If the value is empty, render a placeholder
|
||||
@@ -82,8 +101,8 @@ class ObjectAttribute:
|
||||
return self.placeholder
|
||||
|
||||
return render_to_string(self.template_name, {
|
||||
**self.get_context(obj, context),
|
||||
'name': context['name'],
|
||||
**self.get_context(obj, name, value, context),
|
||||
'name': name,
|
||||
'value': value,
|
||||
})
|
||||
|
||||
@@ -112,7 +131,7 @@ class TextAttr(ObjectAttribute):
|
||||
return self.format_string.format(value)
|
||||
return value
|
||||
|
||||
def get_context(self, obj, context):
|
||||
def get_context(self, obj, attr, value, context):
|
||||
return {
|
||||
'style': self.style,
|
||||
'copy_button': self.copy_button,
|
||||
@@ -134,7 +153,7 @@ class NumericAttr(ObjectAttribute):
|
||||
self.unit_accessor = unit_accessor
|
||||
self.copy_button = copy_button
|
||||
|
||||
def get_context(self, obj, context):
|
||||
def get_context(self, obj, attr, value, context):
|
||||
unit = resolve_attr_path(obj, self.unit_accessor) if self.unit_accessor else None
|
||||
return {
|
||||
'unit': unit,
|
||||
@@ -172,7 +191,7 @@ class ChoiceAttr(ObjectAttribute):
|
||||
|
||||
return resolve_attr_path(target, field_name)
|
||||
|
||||
def get_context(self, obj, context):
|
||||
def get_context(self, obj, attr, value, context):
|
||||
target, field_name = self._resolve_target(obj)
|
||||
if target is None:
|
||||
return {'bg_color': None}
|
||||
@@ -241,7 +260,7 @@ class ImageAttr(ObjectAttribute):
|
||||
decoding = 'async'
|
||||
self.decoding = decoding
|
||||
|
||||
def get_context(self, obj, context):
|
||||
def get_context(self, obj, attr, value, context):
|
||||
return {
|
||||
'decoding': self.decoding,
|
||||
'load_lazy': self.load_lazy,
|
||||
@@ -264,8 +283,7 @@ class RelatedObjectAttr(ObjectAttribute):
|
||||
self.linkify = linkify
|
||||
self.grouped_by = grouped_by
|
||||
|
||||
def get_context(self, obj, context):
|
||||
value = self.get_value(obj)
|
||||
def get_context(self, obj, attr, value, context):
|
||||
group = getattr(value, self.grouped_by, None) if self.grouped_by else None
|
||||
return {
|
||||
'linkify': self.linkify,
|
||||
@@ -300,14 +318,13 @@ class RelatedObjectListAttr(RelatedObjectAttr):
|
||||
self.max_items = max_items
|
||||
self.overflow_indicator = overflow_indicator
|
||||
|
||||
def _get_items(self, obj):
|
||||
def _get_items(self, items):
|
||||
"""
|
||||
Retrieve items from the given object using the accessor path.
|
||||
|
||||
Returns a tuple of (items, has_more) where items is a list of resolved objects
|
||||
and has_more indicates whether additional items exist beyond the max_items limit.
|
||||
"""
|
||||
items = resolve_attr_path(obj, self.accessor)
|
||||
if items is None:
|
||||
return [], False
|
||||
|
||||
@@ -322,8 +339,8 @@ class RelatedObjectListAttr(RelatedObjectAttr):
|
||||
|
||||
return items[:self.max_items], has_more
|
||||
|
||||
def get_context(self, obj, context):
|
||||
items, has_more = self._get_items(obj)
|
||||
def get_context(self, obj, attr, value, context):
|
||||
items, has_more = self._get_items(value)
|
||||
|
||||
return {
|
||||
'linkify': self.linkify,
|
||||
@@ -338,14 +355,15 @@ class RelatedObjectListAttr(RelatedObjectAttr):
|
||||
}
|
||||
|
||||
def render(self, obj, context):
|
||||
context = context or {}
|
||||
context_data = self.get_context(obj, context)
|
||||
name = context['name']
|
||||
value = self.get_value(obj)
|
||||
context_data = self.get_context(obj, name, value, context)
|
||||
|
||||
if not context_data['items']:
|
||||
return self.placeholder
|
||||
|
||||
return render_to_string(self.template_name, {
|
||||
'name': context.get('name'),
|
||||
'name': name,
|
||||
**context_data,
|
||||
})
|
||||
|
||||
@@ -366,11 +384,13 @@ class NestedObjectAttr(ObjectAttribute):
|
||||
self.linkify = linkify
|
||||
self.max_depth = max_depth
|
||||
|
||||
def get_context(self, obj, context):
|
||||
value = self.get_value(obj)
|
||||
nodes = value.get_ancestors(include_self=True)
|
||||
if self.max_depth:
|
||||
nodes = list(nodes)[-self.max_depth:]
|
||||
def get_context(self, obj, attr, value, context):
|
||||
if value is not None:
|
||||
nodes = value.get_ancestors(include_self=True)
|
||||
if self.max_depth:
|
||||
nodes = list(nodes)[-self.max_depth:]
|
||||
else:
|
||||
nodes = []
|
||||
return {
|
||||
'nodes': nodes,
|
||||
'linkify': self.linkify,
|
||||
@@ -394,40 +414,35 @@ class GenericForeignKeyAttr(ObjectAttribute):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.linkify = linkify
|
||||
|
||||
def get_context(self, obj, context):
|
||||
value = self.get_value(obj)
|
||||
content_type = value._meta.verbose_name
|
||||
def get_context(self, obj, attr, value, context):
|
||||
content_type = value._meta.verbose_name if value is not None else None
|
||||
return {
|
||||
'content_type': content_type,
|
||||
'linkify': self.linkify,
|
||||
}
|
||||
|
||||
|
||||
class AddressAttr(ObjectAttribute):
|
||||
class AddressAttr(MapURLMixin, ObjectAttribute):
|
||||
"""
|
||||
A physical or mailing address.
|
||||
|
||||
Parameters:
|
||||
map_url (bool): If true, the address will render as a hyperlink using settings.MAPS_URL
|
||||
map_url (bool/str): The URL to use when rendering the address. If True, the address will render as a
|
||||
hyperlink using settings.MAPS_URL.
|
||||
"""
|
||||
template_name = 'ui/attrs/address.html'
|
||||
|
||||
def __init__(self, *args, map_url=True, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if map_url is True:
|
||||
self.map_url = get_config().MAPS_URL
|
||||
elif map_url:
|
||||
self.map_url = map_url
|
||||
else:
|
||||
self.map_url = None
|
||||
self._map_url = map_url
|
||||
|
||||
def get_context(self, obj, context):
|
||||
def get_context(self, obj, attr, value, context):
|
||||
return {
|
||||
'map_url': self.map_url,
|
||||
}
|
||||
|
||||
|
||||
class GPSCoordinatesAttr(ObjectAttribute):
|
||||
class GPSCoordinatesAttr(MapURLMixin, ObjectAttribute):
|
||||
"""
|
||||
A GPS coordinates pair comprising latitude and longitude values.
|
||||
|
||||
@@ -440,24 +455,18 @@ class GPSCoordinatesAttr(ObjectAttribute):
|
||||
label = _('GPS coordinates')
|
||||
|
||||
def __init__(self, latitude_attr='latitude', longitude_attr='longitude', map_url=True, **kwargs):
|
||||
super().__init__(accessor=None, **kwargs)
|
||||
super().__init__(accessor=latitude_attr, **kwargs)
|
||||
self.latitude_attr = latitude_attr
|
||||
self.longitude_attr = longitude_attr
|
||||
if map_url is True:
|
||||
self.map_url = get_config().MAPS_URL
|
||||
elif map_url:
|
||||
self.map_url = map_url
|
||||
else:
|
||||
self.map_url = None
|
||||
self._map_url = map_url
|
||||
|
||||
def render(self, obj, context=None):
|
||||
context = context or {}
|
||||
def render(self, obj, context):
|
||||
latitude = resolve_attr_path(obj, self.latitude_attr)
|
||||
longitude = resolve_attr_path(obj, self.longitude_attr)
|
||||
if latitude is None or longitude is None:
|
||||
return self.placeholder
|
||||
return render_to_string(self.template_name, {
|
||||
**context,
|
||||
'name': context['name'],
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'map_url': self.map_url,
|
||||
@@ -478,7 +487,7 @@ class DateTimeAttr(ObjectAttribute):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.spec = spec
|
||||
|
||||
def get_context(self, obj, context):
|
||||
def get_context(self, obj, attr, value, context):
|
||||
return {
|
||||
'spec': self.spec,
|
||||
}
|
||||
@@ -504,7 +513,7 @@ class TemplatedAttr(ObjectAttribute):
|
||||
self.template_name = template_name
|
||||
self.context = context or {}
|
||||
|
||||
def get_context(self, obj, context):
|
||||
def get_context(self, obj, attr, value, context):
|
||||
return {
|
||||
**context,
|
||||
**self.context,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% load i18n %}
|
||||
<span{% if style %} class="{{ style }}"{% endif %}>
|
||||
<span>
|
||||
<span{% if name %} id="attr_{{ name }}"{% endif %}>{{ value }}</span>
|
||||
{% if unit %}
|
||||
{{ unit|lower }}
|
||||
|
||||
Reference in New Issue
Block a user