Repository: mozilla/pontoon

Issue:
Notifications page takes forever to load, or causes application error
*This issue was created automatically by a [script](https://github.com/mathjazz/bugzilla2github/).*
## [Bug 1699610](https://bugzilla.mozilla.org/show_bug.cgi?id=1699610)
Bug Reporter: @flodolo
CC:   @mathjazz

My notifications page takes forever to load, and in some cases triggers an application error. I guess I'm an edge case, having too many notifications (2703 right now).

The resulting HTML is 1.65 MB, and takes 17 seconds to load.

In particular, it looks like the page has no pagination for results, which might be the main cause.


Discussion:
Comment Author: @flodolo

I think this one is fixed?


Comment Author: @mathjazz

Nope, none of the speedups have landed.

(Any improvements you might have noticed are due to variable Heroku/AWS performance).



Resolving patch:
diff --git a/pontoon/base/models.py b/pontoon/base/models.py
--- a/pontoon/base/models.py
+++ b/pontoon/base/models.py
@@ -17,6 +17,7 @@
 
 from django.conf import settings
 from django.contrib.auth.models import User, Group
+from django.contrib.contenttypes.models import ContentType
 from django.contrib.postgres.fields import ArrayField
 
 from django.core.exceptions import ValidationError
@@ -309,6 +310,30 @@ def is_new_contributor(self, locale):
     )
 
 
+@property
+def notification_list(self):
+    """A list of notifications to display in the notifications menu."""
+    notifications = self.notifications.prefetch_related(
+        "actor", "target", "action_object"
+    )
+
+    # In order to prefetch Resource and Project data for Entities, we need to split the
+    # QuerySet into two parts: one for comment notifications, which store Entity objects
+    # into the Notification.target field, and one for other notifications.
+    comment_query = {
+        "target_content_type": ContentType.objects.get(app_label="base", model="entity")
+    }
+    comment_notifications = notifications.filter(**comment_query).prefetch_related(
+        "target__resource__project"
+    )
+    other_notifications = notifications.exclude(**comment_query)
+    notifications = list(comment_notifications) + list(other_notifications)
+
+    notifications.sort(key=lambda x: x.timestamp, reverse=True)
+
+    return notifications
+
+
 @property
 def menu_notifications(self):
     """A list of notifications to display in the notifications menu."""
@@ -476,6 +501,7 @@ def latest_action(self):
 User.add_to_class("top_contributed_locale", top_contributed_locale)
 User.add_to_class("can_translate", can_translate)
 User.add_to_class("is_new_contributor", is_new_contributor)
+User.add_to_class("notification_list", notification_list)
 User.add_to_class("menu_notifications", menu_notifications)
 User.add_to_class("unread_notifications_display", unread_notifications_display)
 User.add_to_class("serialized_notifications", serialized_notifications)
diff --git a/pontoon/contributors/urls.py b/pontoon/contributors/urls.py
--- a/pontoon/contributors/urls.py
+++ b/pontoon/contributors/urls.py
@@ -50,6 +50,12 @@ class UsernameConverter(StringConverter):
         views.notifications,
         name="pontoon.contributors.notifications",
     ),
+    # Current user's remaining notifications
+    path(
+        "ajax/notifications/",
+        views.ajax_notifications,
+        name="pontoon.contributors.ajax.notifications",
+    ),
     # Mark current user's notifications as read
     path(
         "notifications/mark-all-as-read/",
diff --git a/pontoon/contributors/views.py b/pontoon/contributors/views.py
--- a/pontoon/contributors/views.py
+++ b/pontoon/contributors/views.py
@@ -386,10 +386,13 @@ def verify_email_address(request, token):
 
 @login_required(redirect_field_name="", login_url="/403")
 def notifications(request):
-    """View and edit user notifications."""
-    notifications = request.user.notifications.prefetch_related(
-        "actor", "target"
-    ).order_by("-pk")
+    """View user notifications.
+
+    Only first 100 notifications are displayed for performance reasons. The rest are
+    loaded via AJAX.
+    """
+    notifications = request.user.notification_list
+
     projects = {}
 
     for notification in notifications:
@@ -425,13 +428,33 @@ def notifications(request):
         request,
         "contributors/notifications.html",
         {
-            "notifications": notifications,
+            "has_more": len(notifications) > 100,
+            "notifications": notifications[:100],
             "projects": projects,
             "ordered_projects": ordered_projects,
         },
     )
 
 
+@login_required(redirect_field_name="", login_url="/403")
+@require_AJAX
+def ajax_notifications(request):
+    """View (remaining) user notifications.
+
+    The first 100 notifictions are displayed on the page load. The rest are loaded via
+    this AJAX view.
+    """
+    notifications = request.user.notification_list
+
+    return render(
+        request,
+        "contributors/includes/notifications_remaining.html",
+        {
+            "notifications": notifications[100:],
+        },
+    )
+
+
 @login_required(redirect_field_name="", login_url="/403")
 @require_AJAX
 @transaction.atomic


Test patch: