|
|
@@ -29,10 +29,26 @@ PLACEHOLDER_HTML = '<span class="text-muted">—</span>'
|
|
|
|
|
|
IMAGE_DECODING_CHOICES = ('auto', 'async', 'sync')
|
|
|
|
|
|
+
|
|
|
#
|
|
|
-# Attributes
|
|
|
+# 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:
|
|
|
"""
|
|
|
@@ -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,
|