diff --git a/tom_common/apps.py b/tom_common/apps.py
index b242634f..8eb4ddd9 100644
--- a/tom_common/apps.py
+++ b/tom_common/apps.py
@@ -20,3 +20,15 @@ def ready(self):
plotly_theme = 'plotly_white'
pio.templates.default = plotly_theme
+
+ def profile_details(self):
+ """
+ Integration point for adding items to the user profile page.
+
+ This method should return a list of dictionaries that include a `partial` key pointing to the path of the html
+ profile partial. The `context` key should point to the dot separated string path to the templatetag that will
+ return a dictionary containing new context for the accompanying partial.
+ Typically, this partial will be a bootstrap card displaying some app specific user data.
+ """
+ return [{'partial': 'tom_common/partials/user_data.html',
+ 'context': 'tom_common.templatetags.user_extras.user_data'}]
diff --git a/tom_common/templates/tom_common/partials/app_profiles.html b/tom_common/templates/tom_common/partials/app_profiles.html
new file mode 100644
index 00000000..0d3b8ebd
--- /dev/null
+++ b/tom_common/templates/tom_common/partials/app_profiles.html
@@ -0,0 +1,23 @@
+{% load user_extras tom_common_extras %}
+{% load bootstrap4 %}
+
+
+
+
+ {% for profile in profiles_to_display %}
+ {% show_individual_app_profile profile %}
+
+ {% comment %}
+ Start new column halfway through the list of profiles.
+ {% endcomment %}
+ {% if forloop.counter == profile_list|length|add:"1"|multiplyby:"0.5"|add:"0" %}
+
+
+ {% endif %}
+
+ {% empty %}
+ This user has no profile.
+ {% endfor %}
+
+
+
diff --git a/tom_common/templates/tom_common/partials/include_profile_card.html b/tom_common/templates/tom_common/partials/include_profile_card.html
new file mode 100644
index 00000000..eef696a9
--- /dev/null
+++ b/tom_common/templates/tom_common/partials/include_profile_card.html
@@ -0,0 +1,7 @@
+{% comment %}
+ This partial template includes another partial from the context specific to a specific App's profile.
+ This allows the partial to be rendered with only the context specified by the app, without interference from
+ other app profile contexts.
+{% endcomment %}
+
+{% include profile_partial %}
diff --git a/tom_common/templates/tom_common/user_profile.html b/tom_common/templates/tom_common/user_profile.html
index 8bb78403..09fe3264 100644
--- a/tom_common/templates/tom_common/user_profile.html
+++ b/tom_common/templates/tom_common/user_profile.html
@@ -11,14 +11,6 @@
{% endif %}
-
-
-
- {% user_data user %}
-
-
-
-
-
+{% show_app_profiles user %}
{% endblock %}
diff --git a/tom_common/templatetags/tom_common_extras.py b/tom_common/templatetags/tom_common_extras.py
index 500b6029..04566a69 100644
--- a/tom_common/templatetags/tom_common_extras.py
+++ b/tom_common/templatetags/tom_common_extras.py
@@ -59,7 +59,7 @@ def verbose_name(instance, field_name):
"""
try:
return instance._meta.get_field(field_name).verbose_name.title()
- except FieldDoesNotExist:
+ except (FieldDoesNotExist, AttributeError):
return field_name.title()
@@ -108,6 +108,15 @@ def truncate_number(value):
return value
+@register.filter
+def multiplyby(value, arg):
+ """
+ Multiply the value by a number and return a float.
+ `{% value|multiplyby:"x.y" %}`
+ """
+ return float(value) * float(arg)
+
+
@register.filter
def addstr(arg1, arg2):
"""
diff --git a/tom_common/templatetags/user_extras.py b/tom_common/templatetags/user_extras.py
index 8018d7f3..85b1731e 100644
--- a/tom_common/templatetags/user_extras.py
+++ b/tom_common/templatetags/user_extras.py
@@ -1,8 +1,12 @@
+import logging
from django import template
from django.contrib.auth.models import Group, User
from django.forms.models import model_to_dict
+from django.apps import apps
+from django.utils.module_loading import import_string
register = template.Library()
+logger = logging.getLogger(__name__)
@register.inclusion_tag('auth/partials/group_list.html', takes_context=True)
@@ -42,3 +46,50 @@ def user_data(user):
'user_data': user_dict,
'profile_data': profile_dict,
}
+
+
+@register.inclusion_tag('tom_common/partials/app_profiles.html', takes_context=True)
+def show_app_profiles(context, user):
+ """
+ Imports the profile content from relevant apps into the template.
+
+ Each profile should be contained in a list of dictionaries in an app's apps.py `profile_details` method.
+ Each profile dictionary should contain a 'context' key with the path to the context processor class (typically a
+ templatetag), and a 'partial' key with the path to the html partial template.
+
+ FOR EXAMPLE:
+ [{'partial': 'path/to/partial.html',
+ 'context': 'path/to/context/data/method'}]
+ """
+ profiles_to_display = []
+ for app in apps.get_app_configs():
+ try:
+ profile_details = app.profile_details()
+ except AttributeError:
+ continue
+ if profile_details:
+ for profile in profile_details:
+ try:
+ context_method = import_string(profile['context'])
+ except ImportError:
+ logger.warning(f'WARNING: Could not import context for {app.name} profile from '
+ f'{profile["context"]}.\n'
+ f'Are you sure you have the right path?')
+ continue
+ new_context = context_method(user)
+ profiles_to_display.append({'partial': profile['partial'], 'context': new_context})
+
+ context['user'] = user
+ context['profiles_to_display'] = profiles_to_display
+ return context
+
+
+@register.inclusion_tag('tom_common/partials/include_profile_card.html', takes_context=True)
+def show_individual_app_profile(context, profile_data):
+ """
+ An Inclusion tag for setting the unique context for each app's user profile.
+ """
+ for item in profile_data['context']:
+ context[item] = profile_data['context'][item]
+ context['profile_partial'] = profile_data['partial']
+ return context
diff --git a/tom_common/tests.py b/tom_common/tests.py
index 1bc01749..fc530be1 100644
--- a/tom_common/tests.py
+++ b/tom_common/tests.py
@@ -9,7 +9,7 @@
from django_comments.models import Comment
from tom_targets.tests.factories import SiderealTargetFactory
-from tom_common.templatetags.tom_common_extras import verbose_name
+from tom_common.templatetags.tom_common_extras import verbose_name, multiplyby
class TestCommonViews(TestCase):
@@ -37,6 +37,12 @@ def test_verbose_name(self):
# Check that the verbose name for a non-existent field is returned correctly
self.assertEqual(verbose_name(User, 'definitely_not_a_field'), 'Definitely_Not_A_Field')
+ def test_multiplyby(self):
+ # Check that the multiplyby template filter works correctly
+ self.assertEqual(multiplyby(2, 3), 6)
+ self.assertEqual(multiplyby(-3, 4), -12)
+ self.assertEqual(multiplyby(0.5, 5), 2.5)
+
class TestUserManagement(TestCase):
def setUp(self):