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