From d90d6254d1c4f1bea9ec3bffeea9d5133dda97f5 Mon Sep 17 00:00:00 2001
From: Saurabh Kumar <theskumar@users.noreply.github.com>
Date: Sun, 31 Mar 2024 03:54:56 +0800
Subject: [PATCH] Update the global notification dropdrown to use htmx (#3836)

- Loads the notification items only after clicking on the bell icon
- This reduces ~30 db queries, on every page load
- The activities display are also on the most costly db queries, on a
simple test db the time spent on processing sql reduces from ~530ms to
~150ms
- The side-effect of ajax based request is the whenever the bell icon is
  clicked it will always display the latest data and not stale, even if
the page is not refreshed.

Fixes #3806
---
 hypha/apply/activity/context_processors.py    | 13 ------
 .../include/notifications_dropdown.html       | 29 ++++++-------
 hypha/apply/activity/views.py                 | 12 +++++-
 hypha/settings/base.py                        |  1 -
 hypha/templates/base-apply.html               | 42 +++++++++++++++++--
 5 files changed, 62 insertions(+), 35 deletions(-)
 delete mode 100644 hypha/apply/activity/context_processors.py

diff --git a/hypha/apply/activity/context_processors.py b/hypha/apply/activity/context_processors.py
deleted file mode 100644
index 0a807d21b..000000000
--- a/hypha/apply/activity/context_processors.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from .models import Activity
-
-
-def notification_context(request):
-    context_data = {}
-    if hasattr(request, "user"):
-        if request.user.is_authenticated and request.user.is_apply_staff:
-            context_data["latest_notifications"] = (
-                Activity.objects.filter(current=True)
-                .latest()
-                .order_by("-timestamp")[:5]
-            )
-    return context_data
diff --git a/hypha/apply/activity/templates/activity/include/notifications_dropdown.html b/hypha/apply/activity/templates/activity/include/notifications_dropdown.html
index 0866fcae5..03003c18c 100644
--- a/hypha/apply/activity/templates/activity/include/notifications_dropdown.html
+++ b/hypha/apply/activity/templates/activity/include/notifications_dropdown.html
@@ -1,20 +1,15 @@
 {% load i18n activity_tags nh3_tags markdown_tags submission_tags apply_tags %}
 
-<div class="notifications notifications--dropdown">
-    <div class="notifications__content zeta" role="activity">
-        <div class="notifications__header">
-            <span>{% trans "Notifications" %}</span>
-            <a class="notifications__more" href="{% url "activity:notifications" %}">{% trans "Show All" %}</a>
-        </div>
+{% for activity in object_list %}
+    <p class="notifications__item">
+        <strong>{{ activity.source_content_type.name|source_type }} </strong>
+        <a href="{{ activity.source.get_absolute_url }}{% if activity.type == 'comment' %}#communications{% endif %}">{{ activity.source.title|capfirst|truncatechars:15 }}</a>
+        : {{ activity.user.get_full_name }} {% if activity.type == 'comment' %}{% trans "made a comment" %}{% else %} {{ activity|display_for:request.user }}{% endif %}
+        {% if activity.related_object %}<a href="{{ activity.related_object.get_absolute_url }}">{{ activity.related_object|model_verbose_name }}</a>{% endif %}
+    </p>
+{% empty %}
+    <p class="notifications__item">
+        {% trans "No notifications available." %}
+    </p>
+{% endfor %}
 
-        {% for activity in latest_notifications %}
-            <p class="notifications__item">
-                <strong>{{ activity.source_content_type.name|source_type }} </strong>
-                <a href="{{ activity.source.get_absolute_url }}{% if activity.type == 'comment' %}#communications{% endif %}">{{ activity.source.title|capfirst|truncatechars:15 }}</a>
-                : {{ activity.user.get_full_name }} {% if activity.type == 'comment' %}{% trans "made a comment" %}{% else %} {{ activity|display_for:request.user }}{% endif %}
-                {% if activity.related_object %}<a href="{{ activity.related_object.get_absolute_url }}">{{ activity.related_object|model_verbose_name }}</a>{% endif %}
-            </p>
-        {% endfor %}
-
-    </div>
-</div>
diff --git a/hypha/apply/activity/views.py b/hypha/apply/activity/views.py
index 573928dd8..6f14dad78 100644
--- a/hypha/apply/activity/views.py
+++ b/hypha/apply/activity/views.py
@@ -88,11 +88,21 @@ class NotificationsView(ListView):
     template_name = "activity/notifications.html"
     filterset_class = NotificationFilter
 
+    def get_template_names(self):
+        if self.request.htmx and self.request.GET.get("type") == "header_dropdown":
+            return ["activity/include/notifications_dropdown.html"]
+        return super().get_template_names()
+
     def get_queryset(self):
         # List only last 30 days' activities
         queryset = Activity.objects.filter(current=True).latest()
+
         self.filterset = self.filterset_class(self.request.GET, queryset=queryset)
-        return self.filterset.qs.distinct().order_by("-timestamp")
+        qs = self.filterset.qs.distinct().order_by("-timestamp")
+
+        if self.request.htmx and self.request.GET.get("type") == "header_dropdown":
+            qs = qs[:5]
+        return qs
 
     def get_context_data(self, *, object_list=None, **kwargs):
         context = super(NotificationsView, self).get_context_data()
diff --git a/hypha/settings/base.py b/hypha/settings/base.py
index e5fb60735..908b37b78 100644
--- a/hypha/settings/base.py
+++ b/hypha/settings/base.py
@@ -216,7 +216,6 @@ TEMPLATES = [
                 "social_django.context_processors.login_redirect",
                 "hypha.apply.projects.context_processors.projects_enabled",
                 "hypha.cookieconsent.context_processors.cookies_accepted",
-                "hypha.apply.activity.context_processors.notification_context",
                 "hypha.core.context_processors.global_vars",
             ],
             "builtins": [
diff --git a/hypha/templates/base-apply.html b/hypha/templates/base-apply.html
index d10900a31..efb63428b 100644
--- a/hypha/templates/base-apply.html
+++ b/hypha/templates/base-apply.html
@@ -101,8 +101,12 @@
             </section>
 
             <div class="header__button-container flex gap-4">
-                {% if latest_notifications %}
-                    <div x-data="{open: false}">
+                {% comment %} Notifications {% endcomment %}
+                {% if request.user.is_authenticated and request.user.is_apply_staff %}
+                    <div
+                        x-data="{open: false}"
+                        x-init="$watch('open', value => { if (value) { document.getElementById('id-notification-list').dispatchEvent(new Event('htmx:fetch')); } })"
+                    >
                         <a href="{% url "activity:notifications" %}"
                            class="p-2 flex items-center hover:opacity-70 transition-opacity"
                            aria-label="{% trans "Notifications" %}"
@@ -114,9 +118,41 @@
                         >
                             {% heroicon_outline "bell-alert" class="inline me-1 text-black" aria_hidden="true" %}
                         </a>
-                        <div x-cloak x-show="open" x-transition @click.outside="open = false">{% include "activity/include/notifications_dropdown.html" %}</div>
+                        <div x-cloak x-show="open" x-transition @click.outside="open = false">
+                            <div class="notifications notifications--dropdown">
+                                <div class="notifications__content zeta" role="activity">
+                                    <div class="notifications__header">
+                                        <span>{% trans "Notifications" %}</span>
+                                        <a class="notifications__more" href="{% url "activity:notifications" %}">{% trans "Show All" %}</a>
+                                    </div>
+
+                                    <div
+                                        id="id-notification-list"
+                                        hx-get="{% url "activity:notifications" %}?type=header_dropdown"
+                                        hx-swap="innerHTML"
+                                        hx-trigger="htmx:fetch"
+                                    >
+                                        <div class="min-h-4 mx-4 my-3 rounded-lg bg-gray-200 animate-pulse"></div>
+                                        <div class="min-h-4 w-2/3 mx-4 my-3 rounded-lg bg-gray-200 animate-pulse"></div>
+                                        <hr>
+                                        <div class="min-h-4 mx-4 my-3 rounded-lg bg-gray-200 animate-pulse"></div>
+                                        <div class="min-h-4 w-2/3 mx-4 my-3 rounded-lg bg-gray-200 animate-pulse"></div>
+                                        <hr>
+                                        <div class="min-h-4 mx-4 my-3 rounded-lg bg-gray-200 animate-pulse"></div>
+                                        <div class="min-h-4 w-2/3 mx-4 my-3 rounded-lg bg-gray-200 animate-pulse"></div>
+                                        <hr>
+                                        <div class="min-h-4 mx-4 my-3 rounded-lg bg-gray-200 animate-pulse"></div>
+                                        <div class="min-h-4 w-2/3 mx-4 my-3 rounded-lg bg-gray-200 animate-pulse"></div>
+                                        <hr>
+                                        <div class="min-h-4 mx-4 my-3 rounded-lg bg-gray-200 animate-pulse"></div>
+                                        <div class="min-h-4 w-2/3 mx-4 my-3 rounded-lg bg-gray-200 animate-pulse"></div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
                     </div>
                 {% endif %}
+                {% comment %} Notifications End{% endcomment %}
 
                 {% if request.path != '/auth/' and request.path != '/login/' %}
                     {% include "includes/login_button.html" %}
-- 
GitLab