Repository: DDMAL/CantusDB

Issue:
Certain Genre Detail pages are _very_ slow to load
I think the problem is trying to display the "chants by genre" table - for a genre with lots of example chants, e.g. [Antiphon](http://206.12.88.113/genre/122), I've tried loading the page several times and keep getting a 502 error.


Discussion:
I see what's going on here: for the genre in question, we're building a tally in Python of the Cantus IDs of all that genre's chants, sorting them by the tally, and then displaying the first hundred of them (assuming the user tried to load page 1). It should be possible to do this all on the database level and load only a hundred results at a time, rather than loading all the data from the database and munging through the Genre's chant set in Python. I will continue to think about this.

down to the nearest hundredth of a second, `get_genre_cantus_ids` (i.e. the algorithm outlined above) takes up the entirety of the runtime of `get_queryset`.
I do think that this is going to be a heavy calculation no matter how we slice it. It might make most sense to automatically run this calculation from time to time for each Genre in our database, and cache it in a field on our model.
Honestly I'm not too concerned about it, since a page containing a list of every antiphon in Cantus DB doesn't seem like a terribly urgent thing to have (and OldCantus doesn't have it.) For that matter, if the genre page were just a page with a description of the genre and maybe a few random examples, it would probably be fine, at least for the time being--and would still be better than what OldCantus has. 

Resolving patch:
diff --git a/django/cantusdb_project/main_app/views/genre.py b/django/cantusdb_project/main_app/views/genre.py
--- a/django/cantusdb_project/main_app/views/genre.py
+++ b/django/cantusdb_project/main_app/views/genre.py
@@ -1,81 +1,13 @@
-from typing import Dict, List
-
-from django.views.generic import ListView
-from django.views.generic.detail import SingleObjectMixin
+from django.views.generic import DetailView, ListView
 from extra_views import SearchableListMixin
 from main_app.models import Genre
 
 
-class GenreDetailView(SingleObjectMixin, ListView):
-    paginate_by = 100
+class GenreDetailView(DetailView):
+    model = Genre
+    context_object_name = "genre"
     template_name = "genre_detail.html"
 
-    def get_genre_cantus_ids(self, display_unpublished=True) -> List[Dict]:
-        """
-        Get a list with data on each unique ``cantus_id`` related to this Genre.
-
-        The list contains dicts and each dict has the following keys:
-
-        ``cantus_id``: The ``cantus_id``
-        ``num_chants``: The number of Chants that have this ``cantus_id``
-        ``first_incipit``: The incipit of first Chant with this ``cantus_id``
-        ``first_incipit_url``: The url of first Chant with this ``cantus_id``
-
-        Returns:
-            List[Dict]: A list of dicts with data on each unique ``cantus_id``
-        """
-        cantus_ids = (self.object.chant_set
-            .exclude(cantus_id=None)
-            .values_list("cantus_id", flat=True)
-            .distinct("cantus_id")
-        )
-        if not display_unpublished:
-            cantus_ids = cantus_ids.filter(source__published=True)
-        
-        cantus_ids_list = list(cantus_ids)
-
-        chant_list = []
-        for cantus_id in cantus_ids_list:
-            chants = self.object.chant_set.filter(cantus_id=cantus_id)
-            num_chants = chants.count()
-            first_chant = chants.first()
-            first_incipit_url = first_chant.get_absolute_url()
-            first_incipit = first_chant.incipit
-            chant_list.append(
-                {
-                    "cantus_id": cantus_id,
-                    "num_chants": num_chants,
-                    "first_incipit": first_incipit,
-                    "first_incipit_url": first_incipit_url,
-                }
-            )
-        # Sort list based on number of Chants per cantus_id (descending)
-        chant_list = sorted(chant_list, key=lambda k: k["num_chants"], reverse=True)
-        return chant_list
-
-    def get(self, request, *args, **kwargs):
-        self.object = self.get_object(queryset=Genre.objects.all())
-        return super().get(request, *args, **kwargs)
-
-    def get_context_data(self, **kwargs):
-        context = super().get_context_data(**kwargs)
-        context["genre"] = self.object
-        return context
-
-    def get_queryset(self):
-        display_unpublished = self.request.user.is_authenticated
-        search_term = self.request.GET.get("incipit")
-        if not search_term:
-            return self.get_genre_cantus_ids(display_unpublished=display_unpublished)
-        else:
-            search_term = search_term.strip(" ")
-            filtered_chants = [
-                chant
-                for chant in self.get_genre_cantus_ids(display_unpublished=display_unpublished)
-                if search_term.lower() in chant["first_incipit"].lower()
-            ]
-            return filtered_chants
-
 
 class GenreListView(SearchableListMixin, ListView):
     model = Genre


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
@@ -1755,75 +1755,6 @@ def test_context(self):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(genre, response.context["genre"])
 
-    def test_chants_by_genre(self):
-        genre = make_fake_genre()
-        # create a source because if chants1, 2 and 3 don't have a source,
-        # source.published will be None, and they won't be included in
-        # the response context
-        source = make_fake_source()
-        chant1 = Chant.objects.create(incipit="chant1", genre=genre, cantus_id="100000", source=source)
-        chant2 = Chant.objects.create(incipit="chant2", genre=genre, cantus_id="100000", source=source)
-        chant3 = Chant.objects.create(incipit="chant3", genre=genre, cantus_id="123456", source=source)
-        response = self.client.get(reverse("genre-detail", args=[genre.id]))
-        self.assertEqual(response.status_code, 200)
-        # the context should be a list of dicts, each corresponding to one cantus id
-        chant_info_list = response.context["object_list"]
-        # the chant info list should be ordered by the number of chants
-        # the first one should be the one that has two chants
-        self.assertEqual(chant_info_list[0]["cantus_id"], "100000")
-        self.assertEqual(chant_info_list[0]["num_chants"], 2)
-        self.assertEqual(chant_info_list[1]["cantus_id"], "123456")
-        self.assertEqual(chant_info_list[1]["num_chants"], 1)
-    
-    def test_genre_cantus_ids_published_vs_unpublished(self):
-        genre = make_fake_genre()
-        published_source = make_fake_source(published=True)
-        unpublished_source = make_fake_source(published=False)
-        published_chant = make_fake_chant(
-            source=published_source,
-            genre=genre,
-            cantus_id="111111",
-        )
-        unpublished_chant = make_fake_chant(
-            source=unpublished_source,
-            genre=genre,
-            cantus_id="999999",
-        )
-        response = self.client.get(reverse("genre-detail", args=[genre.id]))
-        chant_info_list = response.context["object_list"]
-        self.assertEqual(len(chant_info_list), 1)
-        self.assertEqual(chant_info_list[0]["cantus_id"], "111111")
-
-    def test_search_incipit(self):
-        genre = make_fake_genre()
-        # create a source because if chants1 and 2 don't have a source,
-        # source.published will be None, and they won't be included in
-        # the response context
-        source = make_fake_source()
-        chant1 = Chant.objects.create(incipit="chant1", genre=genre, cantus_id="100000", source=source)
-        chant2 = Chant.objects.create(incipit="chant2", genre=genre, cantus_id="123456", source=source)
-        response = self.client.get(reverse("genre-detail", args=[genre.id]))
-        self.assertEqual(response.status_code, 200)
-
-        # search for the common part of incipit
-        response = self.client.get(
-            reverse("genre-detail", args=[genre.id]), {"incipit": "chant"}
-        )
-        # both cantus_ids should be in the list
-        self.assertEqual(len(response.context["object_list"]), 2)
-        # search for the unique part of incipit
-        response = self.client.get(
-            reverse("genre-detail", args=[genre.id]), {"incipit": "chant1"}
-        )
-        # only one record should be in the list
-        self.assertEqual(len(response.context["object_list"]), 1)
-        # search for random incipit that don't exist
-        response = self.client.get(
-            reverse("genre-detail", args=[genre.id]), {"incipit": "random"}
-        )
-        # the list should be empty
-        self.assertEqual(len(response.context["object_list"]), 0)
-
 
 class OfficeListViewTest(TestCase):
     OFFICE_COUNT = 10