Repository: DDMAL/CantusDB

Issue:
Content-Overview page - we should paginate results
In an email from Debra,

> I was wondering if I could get an option to see more than the latest 50 updated items.
> https://cantusdatabase.org/content-overview/
> 
> (Or is what is showing now all that has been updated on NewCantus, and if the page gets longer there will be an option to see 1-50, 50-100 items etc.?  - or however the view might look)


Discussion:
Sounds like Debra wants this implemented pretty soon. Increasing the priority.
On the content overview page, the "last updated by" column is shown as None when the user who created the object is also the one who made any changes. We should include the same user in this cell.

Resolving patch:
diff --git a/django/cantusdb_project/main_app/views/views.py b/django/cantusdb_project/main_app/views/views.py
--- a/django/cantusdb_project/main_app/views/views.py
+++ b/django/cantusdb_project/main_app/views/views.py
@@ -14,7 +14,6 @@
     Office,
     Provenance,
     RismSiglum,
-    Segment,
     Sequence,
     Source,
 )
@@ -29,6 +28,7 @@
 from django.urls import reverse
 from django.contrib.auth import get_user_model
 from typing import List
+from django.core.paginator import Paginator
 
 
 @login_required
@@ -794,30 +794,39 @@ def project_manager_check(user):
 def content_overview(request):
     objects = []
     models = [
-        Century,
+        Source,
         Chant,
         Feast,
-        Genre,
-        Notation,
+        Sequence,
         Office,
         Provenance,
+        Genre,
+        Notation,
+        Century,
         RismSiglum,
-        Segment,
-        Sequence,
-        Source,
     ]
 
-    # get the 50 most recently updated objects for all of the models
-    for model in models:
-        for object in model.objects.all().order_by("-date_updated")[:50]:
-            objects.append(object)
+    model_names = [model._meta.verbose_name_plural for model in models]
+    selected_model_name = request.GET.get("model", None)
+    selected_model = None
+    if selected_model_name in model_names:
+        selected_model = models[model_names.index(selected_model_name)]
 
-    objects.sort(key=lambda x: x.date_updated, reverse=True)
-    recently_updated_50_objects = objects[:50]
+    objects = []
+    if selected_model:
+        objects = selected_model.objects.all().order_by("-date_created")
 
-    return render(
-        request, "content_overview.html", {"objects": recently_updated_50_objects}
-    )
+    paginator = Paginator(objects, 100)
+    page_number = request.GET.get("page")
+    page_obj = paginator.get_page(page_number)
+
+    context = {
+        "models": model_names,
+        "selected_model_name": selected_model_name,
+        "page_obj": page_obj,
+    }
+
+    return render(request, "content_overview.html", context)
 
 
 def redirect_indexer(request, pk: int) -> HttpResponse:
diff --git a/django/cantusdb_project/main_app/widgets.py b/django/cantusdb_project/main_app/widgets.py
--- a/django/cantusdb_project/main_app/widgets.py
+++ b/django/cantusdb_project/main_app/widgets.py
@@ -1,6 +1,7 @@
 from django.forms.widgets import TextInput, Select, Textarea, CheckboxInput
 from django.utils.safestring import mark_safe
 
+
 class TextInputWidget(TextInput):
     def __init__(self):
         self.attrs = {"class": "form-control form-control-sm"}


Test patch:
diff --git a/django/cantusdb_project/main_app/tests/test_views.py b/django/cantusdb_project/main_app/tests/test_views.py
--- a/django/cantusdb_project/main_app/tests/test_views.py
+++ b/django/cantusdb_project/main_app/tests/test_views.py
@@ -21,6 +21,7 @@
     make_fake_genre,
     make_fake_office,
     make_fake_provenance,
+    make_fake_notation,
     make_fake_rism_siglum,
     make_fake_segment,
     make_fake_sequence,
@@ -156,6 +157,9 @@ def test_permissions_project_manager(self):
         response = self.client.get(f"/edit-source/{source.id}")
         self.assertEqual(response.status_code, 200)
 
+        response = self.client.get("content-overview")
+        self.assertEqual(response.status_code, 200)
+
     def test_permissions_contributor(self):
         contributor = Group.objects.get(name="contributor")
         contributor.user_set.add(self.user)
@@ -5132,3 +5136,78 @@ def test_indexer_redirect_bad(self):
             reverse("redirect-indexer", args=[example_bad_indexer_id])
         )
         self.assertEqual(response_1.status_code, 404)
+
+
+class ContentOverviewTest(TestCase):
+    @classmethod
+    def setUpTestData(cls):
+        Group.objects.create(name="project manager")
+
+    def setUp(self):
+        self.user = get_user_model().objects.create(email="test@test.com")
+        self.user.set_password("pass")
+        self.user.save()
+        self.client = Client()
+
+        project_manager = Group.objects.get(name="project manager")
+        project_manager.user_set.add(self.user)
+        self.client.login(email="test@test.com", password="pass")
+
+    def test_templates_used(self):
+        response = self.client.get(reverse("content-overview"))
+        self.assertTemplateUsed(response, "base.html")
+        self.assertTemplateUsed(response, "content_overview.html")
+
+    def test_project_manager_permission(self):
+        response = self.client.get(reverse("content-overview"))
+        self.assertEqual(response.status_code, 200)
+
+    def test_content_overview_view_with_login_required(self):
+        self.client.logout()
+        response = self.client.get(reverse("content-overview"))
+        self.assertRedirects(response, "/login/?next=/content-overview/")
+
+    def test_content_overview_view_for_non_project_manager(self):
+        user = get_user_model().objects.create(email="non_project_manager@test.com")
+        user.set_password("pass")
+        user.save()
+        self.client.login(email="non_project_manager@test.com", password="pass")
+
+        response = self.client.get(reverse("content-overview"))
+        self.assertEqual(response.status_code, 403)
+
+    def test_content_overview_view_selected_model(self):
+        response = self.client.get(reverse("content-overview"), {"model": "sources"})
+        self.assertEqual(response.status_code, 200)
+
+        self.assertIsNotNone(response.context["models"])
+        models = response.context["models"]
+        self.assertIsNotNone(response.context["page_obj"])
+        page_obj = response.context["page_obj"]
+        self.assertEqual(response.context["selected_model_name"], "sources")
+
+    def test_source_selected_model(self):
+        source = make_fake_source(title="Test Source")
+        chant = make_fake_chant(incipit="Test Chant")
+        response = self.client.get(reverse("content-overview"), {"model": "sources"})
+        self.assertContains(response, f"<b>Sources</b>", html=True)
+        self.assertContains(
+            response,
+            f'<a href="?model=chants">Chants</a>',
+            html=True,
+        )
+        self.assertContains(response, "Test Source", html=True)
+        self.assertNotContains(response, "Test Chant", html=True)
+
+    def test_chant_selected_model(self):
+        source = make_fake_source(title="Test Source")
+        chant = make_fake_chant(manuscript_full_text_std_spelling="Test Chant")
+        response = self.client.get(reverse("content-overview"), {"model": "chants"})
+        self.assertContains(response, f"<b>Chants</b>", html=True)
+        self.assertContains(
+            response,
+            f'<a href="?model=sources">Sources</a>',
+            html=True,
+        )
+        self.assertContains(response, "Test Chant", html=True)
+        self.assertNotContains(response, "Test Source", html=True)