From 3655c68a4c2cc2ffedf1b51dc2340b502463776c Mon Sep 17 00:00:00 2001
From: Antoine Lambert <anlambert@softwareheritage.org>
Date: Tue, 16 Apr 2024 14:50:04 +0200
Subject: [PATCH] save_code_now: Add pending requests notification icon in
 admin menu

When there is pending save code now requests to be reviewed by staff users,
add a notification icon in the top right corner of the save code now admin
menu entry icon in left sidebar.
---
 .../save_code_now/assets/origin-save-admin.js |  7 ++++
 swh/web/save_code_now/origin_save.py          |  9 +++++
 swh/web/utils/__init__.py                     | 13 ++++++-
 swh/web/webapp/assets/webapp/webapp.css       | 22 +++++++++++
 swh/web/webapp/templates/layout.html          | 13 +++++--
 swh/web/webapp/tests/test_templates.py        | 38 ++++++++++++++++++-
 6 files changed, 97 insertions(+), 5 deletions(-)

diff --git a/swh/web/save_code_now/assets/origin-save-admin.js b/swh/web/save_code_now/assets/origin-save-admin.js
index 1755cde0f..0818ea9bd 100644
--- a/swh/web/save_code_now/assets/origin-save-admin.js
+++ b/swh/web/save_code_now/assets/origin-save-admin.js
@@ -265,6 +265,13 @@ export function acceptOriginSaveRequest() {
       const acceptSaveRequestUrl = Urls.admin_origin_save_request_accept(rowData['visit_type'], encodeURI(rowData['origin_url']));
       await csrfPost(acceptSaveRequestUrl);
       pendingSaveRequestsTable.ajax.reload(null, false);
+      // ensure to remove notification icon in sidebar admin menu when
+      // there is no remaining pending requests
+      setTimeout(() => {
+        if ($('td.dt-empty').is(':visible')) {
+          location.reload(true);
+        }
+      }, 100);
     };
 
     swh.webapp.showModalConfirm(
diff --git a/swh/web/save_code_now/origin_save.py b/swh/web/save_code_now/origin_save.py
index e472b0d13..e9600601f 100644
--- a/swh/web/save_code_now/origin_save.py
+++ b/swh/web/save_code_now/origin_save.py
@@ -885,3 +885,12 @@ def schedule_origins_recurrent_visits(
         listed_origins = scheduler().record_listed_origins(listed_origins)
 
     return len(listed_origins)
+
+
+def has_pending_save_code_now_requests() -> bool:
+    """Return :const:`True` if at least one submitted save request requires
+    manual validation by staff member."""
+    return (
+        SaveOriginRequest.objects.filter(status=SAVE_REQUEST_PENDING).first()
+        is not None
+    )
diff --git a/swh/web/utils/__init__.py b/swh/web/utils/__init__.py
index 4d2828b84..f2b65b946 100644
--- a/swh/web/utils/__init__.py
+++ b/swh/web/utils/__init__.py
@@ -309,7 +309,7 @@ def context_processor(request):
         # when rendering templates when standard Django user is logged in.
         request.user.backend = "django.contrib.auth.backends.ModelBackend"
 
-    return {
+    context = {
         "swh_object_icons": swh_object_icons,
         "available_languages": None,
         "swh_client_config": config["client_config"],
@@ -338,6 +338,17 @@ def context_processor(request):
         "show_corner_ribbon": config.get("show_corner_ribbon", False),
     }
 
+    if (
+        "swh.web.save_code_now" in settings.SWH_DJANGO_APPS
+        and hasattr(request, "user")
+        and request.user.is_staff
+    ):
+        from swh.web.save_code_now.origin_save import has_pending_save_code_now_requests
+
+        context["pending_save_code_now_requests"] = has_pending_save_code_now_requests()
+
+    return context
+
 
 def resolve_branch_alias(
     snapshot: Dict[str, Any], branch: Optional[Dict[str, Any]]
diff --git a/swh/web/webapp/assets/webapp/webapp.css b/swh/web/webapp/assets/webapp/webapp.css
index a31a5d50c..a552a5c64 100644
--- a/swh/web/webapp/assets/webapp/webapp.css
+++ b/swh/web/webapp/assets/webapp/webapp.css
@@ -413,6 +413,11 @@ ul.nav-sidebar a {
     color: #fecd1b ! important;
 }
 
+.swh-notification-icon {
+    color: #e20026 !important;
+    transform: translate(8px, -6px);
+}
+
 .swh-sidebar .nav-link.active,
 .swh-sidebar .nav-link:focus {
     color: #323232 !important;
@@ -728,6 +733,23 @@ div.d3-tooltip {
     width: 1.25em;
 }
 
+.mdi-stack {
+    position: relative;
+    display: inline-block;
+    width: 1em;
+    height: 1em;
+    line-height: 1em;
+    vertical-align: middle;
+}
+
+.mdi-stack .mdi {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    text-align: center;
+}
+
 .main-header .nav-link {
     height: inherit;
 }
diff --git a/swh/web/webapp/templates/layout.html b/swh/web/webapp/templates/layout.html
index 4442d3020..a0c12bf04 100644
--- a/swh/web/webapp/templates/layout.html
+++ b/swh/web/webapp/templates/layout.html
@@ -1,5 +1,5 @@
 {% comment %}
-Copyright (C) 2015-2023  The Software Heritage developers
+Copyright (C) 2015-2024  The Software Heritage developers
 See the AUTHORS file at the top-level directory of this distribution
 License: GNU Affero General Public License version 3, or any later version
 See top-level LICENSE file for more information
@@ -38,7 +38,7 @@ See top-level LICENSE file for more information
 /*
 @licstart  The following is the entire license notice for the JavaScript code in this page.
 
-Copyright (C) 2015-2023  The Software Heritage developers
+Copyright (C) 2015-2024  The Software Heritage developers
 
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as
@@ -275,7 +275,14 @@ along with this program.  If not, see <https://www.gnu.org/licenses />.
                     role="menuitem">
                   <a href="{% url 'admin-origin-save-requests' %}"
                      class="nav-link swh-origin-save-admin-link">
-                    <i class="nav-icon mdi mdi-24px mdi-camera"></i>
+                    {% if pending_save_code_now_requests %}
+                      <div class="nav-icon mdi-stack">
+                        <i class="mdi mdi-24px mdi-camera"></i>
+                        <i class="mdi mdi-18px mdi-bell swh-notification-icon"></i>
+                      </div>
+                    {% else %}
+                      <i class="nav-icon mdi mdi-24px mdi-camera"></i>
+                    {% endif %}
                     <p>Save code now</p>
                   </a>
                 </li>
diff --git a/swh/web/webapp/tests/test_templates.py b/swh/web/webapp/tests/test_templates.py
index 57a037ce8..908512239 100644
--- a/swh/web/webapp/tests/test_templates.py
+++ b/swh/web/webapp/tests/test_templates.py
@@ -1,9 +1,10 @@
-# Copyright (C) 2021-2023  The Software Heritage developers
+# Copyright (C) 2021-2024  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU Affero General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 from copy import deepcopy
+from datetime import datetime, timezone
 from importlib.metadata import version
 import random
 
@@ -17,6 +18,11 @@ from swh.web.config import (
     SWH_WEB_STAGING_SERVER_NAMES,
     get_config,
 )
+from swh.web.save_code_now.models import (
+    SAVE_REQUEST_ACCEPTED,
+    SAVE_REQUEST_PENDING,
+    SaveOriginRequest,
+)
 from swh.web.tests.django_asserts import assert_contains, assert_not_contains
 from swh.web.tests.helpers import check_http_get_response, create_django_permission
 from swh.web.utils import reverse
@@ -178,3 +184,33 @@ def test_top_bar_custom_links(client, config_updater):
     assert_contains(resp, doc_link)
     assert_contains(resp, donate_link)
     assert_contains(resp, status_link)
+
+
+@pytest.mark.django_db
+def test_save_code_now_pending_requests_notification(client, staff_user):
+    client.force_login(staff_user)
+
+    url = reverse("swh-web-homepage")
+
+    # no pending save requests, notification icon should not be displayed
+    assert SaveOriginRequest.objects.first() is None
+    resp = check_http_get_response(client, url, status_code=200)
+    assert_not_contains(resp, "mdi-bell swh-notification-icon")
+
+    # new pending save request, notification icon should be displayed
+    sor = SaveOriginRequest.objects.create(
+        request_date=datetime.now(tz=timezone.utc),
+        visit_type="git",
+        origin_url="https://git.example.org/user/project",
+        status=SAVE_REQUEST_PENDING,
+    )
+    assert SaveOriginRequest.objects.first() == sor
+    resp = check_http_get_response(client, url, status_code=200)
+    assert_contains(resp, "mdi-bell swh-notification-icon")
+
+    # pending save request got accepted, notification icon should no longer
+    # be displayed
+    sor.status = SAVE_REQUEST_ACCEPTED
+    sor.save()
+    resp = check_http_get_response(client, url, status_code=200)
+    assert_not_contains(resp, "mdi-bell swh-notification-icon")
-- 
GitLab