Repository: DDMAL/CantusDB

Issue:
Only sources from the Cantus Database segment should appear in the Browse Chants "Source" select element
Jacob: EDITING this to put some conclusions, which otherwise would be buried in the discussion, near the top:

- Since sources from the Bower segment don't contain any chants, they _appear_ to be buggy when a user attempts to access them via the Browse Chants page (i.e. 0 chants are displayed)
- Thus, for the time being **we should not list sources from the Bower segment in the "Source" select element on the Browse Chants page.**
  - **Also, in the source navigation sidebar, for sources in the Bower segment, we should not display the "view all chants" link that currently links to the Browse Chants page** 
- At some point in the future, we may want to better harmonize how CantusDB handles/displays `Sequence`s (especially as compared to `Chant`s with a genre of "Sq" (i.e. "sequence"))

/jacob

When looking at chants in one source, you can also go to another via a drop down.  However, the dropdown displays all possible sources--including those in the Bower Sequence segment. If you choose one of those sequence sources, though, you end up seeing it has "0 chants", presumably because by default you are looking at "chants in this source in the Cantus DB segment". 
![image](https://user-images.githubusercontent.com/67451875/205420715-97f7805c-5932-4b1f-8ce3-59a986553b9c.png)
You can actually see this source just fine via  the Clavis Sequentiarum source list (http://206.12.88.113/source/124052) ; it's getting there from this dropdown that's a problem. http://206.12.88.113/chants/?source=124052&folio=&feast=&search_text=&genre=

This bug seems to also exist on OldCantus--it's just sufficiently weird that I never noticed it before. Still, either the Bower sources shouldn't be in the dropdown, or they need to correctly display their chants.


Discussion:
One thought that comes to mind is that sources in the Bower segment don't have any chants, only a bunch of sequences.
Which raises the question about which I've been wondering for a while - wouldn't it be more elegant to get rid of the Sequence model and turn all the sequences into chants? We could set them all to have a genre of "Sq", since we already have some bona fide Chants with that genre.

But I guess it's supposed to be a feature, rather than a bug, that the Clavis Sequentiarum is not particularly well-integrated with the rest of CantusDB.
Right, so I guess we assume that only chants that are `Chant`s (and none that are `Sequence`s) are meant to show up on the `/chants/` page, and thus **the expected behavior is that only sources from the `CANTUS Database` segment should appear in the "Source" select element, correct?**
ah, you're right, a "chant" is not a "sequence", hence our problem.
I think you are right about the expected behaviour. If the /chants/ page only has Chants (which seems correct), then only sources from the Cantus Database segment should show up. 
Also, it might indeed be nice to turn all the Sequences into Chants, true--except none of them have full texts...
It is a little odd, though, that they all already (in OldCantus) have a genre of "sequence", which you can click on to get a list of other sequences--but only those that are Chants, and not Sequences. I will consult about what we would actually like the behaviour to be.
just following up on this: yes, let's have only sources from the Cantus Database segment in the select element, so that users can't easily get to these apparently-blank source pages. 
Some day we will figure out how to integrate the Bower sources into this sort of browsing function, but that will take more forethought.

Resolving patch:
diff --git a/django/cantusdb_project/main_app/views/chant.py b/django/cantusdb_project/main_app/views/chant.py
--- a/django/cantusdb_project/main_app/views/chant.py
+++ b/django/cantusdb_project/main_app/views/chant.py
@@ -23,7 +23,7 @@
     ChantProofreadForm,
     ChantEditSyllabificationForm,
 )
-from main_app.models import Chant, Feast, Genre, Source, Sequence
+from main_app.models import Chant, Feast, Genre, Source, Sequence, Segment
 from align_text_mel import syllabize_text_and_melody, syllabize_text_to_string
 from django.contrib.auth.mixins import LoginRequiredMixin
 from django.http import Http404
@@ -586,12 +586,25 @@ def get_queryset(self):
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
         # these are needed in the selectors on the left side of the page
-        context["sources"] = Source.objects.order_by("siglum")
         context["feasts"] = Feast.objects.all().order_by("name")
         context["genres"] = Genre.objects.all().order_by("name")
 
+        # sources in the Bower Segment contain only Sequences and no Chants,
+        # so they should not appear among the list of sources
+        cantus_segment = Segment.objects.get(id=4063)
+        sources = cantus_segment.source_set.order_by(
+            "siglum"
+        )  # to be displayed in the "Source" dropdown in the form
+        context["sources"] = sources
+
         source_id = self.request.GET.get("source")
         source = Source.objects.get(id=source_id)
+        if source not in sources:
+            # the chant list ("Browse Chants") page should only be visitable
+            # for sources in the CANTUS Database segment, as sources in the Bower
+            # segment contain no chants
+            raise Http404()
+
         context["source"] = source
 
         user = self.request.user


Test patch:
diff --git a/django/cantusdb_project/main_app/tests/make_fakes.py b/django/cantusdb_project/main_app/tests/make_fakes.py
--- a/django/cantusdb_project/main_app/tests/make_fakes.py
+++ b/django/cantusdb_project/main_app/tests/make_fakes.py
@@ -263,11 +263,14 @@ def make_fake_rism_siglum() -> RismSiglum:
     return rism_siglum
 
 
-def make_fake_segment(name=None) -> Segment:
+def make_fake_segment(name: str = None, id: int = None) -> Segment:
     """Generates a fake Segment object."""
-    if not name:
-        name = faker.sentence()
-    segment = Segment.objects.create(name=name)
+    if name is None:
+        name = faker.sentence(nb_words=2)
+    if id is None:
+        segment = Segment.objects.create(name=name)
+        return segment
+    segment = Segment.objects.create(name=name, id=id)
     return segment
 
 
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
@@ -413,28 +413,45 @@ def test_unpublished_sources_not_listed(self):
 
 class ChantListViewTest(TestCase):
     def test_url_and_templates(self):
-        source = make_fake_source()
+        cantus_segment = make_fake_segment(id=4063)
+        source = make_fake_source(segment=cantus_segment)
         response = self.client.get(reverse("chant-list"), {"source": source.id})
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, "base.html")
         self.assertTemplateUsed(response, "chant_list.html")
 
     def test_published_vs_unpublished(self):
-        published_source = make_fake_source(published=True)
+        cantus_segment = make_fake_segment(id=4063)
+
+        published_source = make_fake_source(segment=cantus_segment, published=True)
         response_1 = self.client.get(
             reverse("chant-list"), {"source": published_source.id}
         )
         self.assertEqual(response_1.status_code, 200)
 
-        unpublished_source = make_fake_source(published=False)
+        unpublished_source = make_fake_source(segment=cantus_segment, published=False)
         response_2 = self.client.get(
             reverse("chant-list"), {"source": unpublished_source.id}
         )
         self.assertEqual(response_2.status_code, 403)
 
+    def test_visibility_by_segment(self):
+        cantus_segment = make_fake_segment(id=4063)
+        cantus_source = make_fake_source(segment=cantus_segment, published=True)
+        response_1 = self.client.get(
+            reverse("chant-list"), {"source": cantus_source.id}
+        )
+        self.assertEqual(response_1.status_code, 200)
+
+        bower_segment = make_fake_segment(id=4064)
+        bower_source = make_fake_source(segment=bower_segment, published=True)
+        response_1 = self.client.get(reverse("chant-list"), {"source": bower_source.id})
+        self.assertEqual(response_1.status_code, 404)
+
     def test_filter_by_source(self):
-        source = make_fake_source()
-        another_source = make_fake_source()
+        cantus_segment = make_fake_segment(id=4063)
+        source = make_fake_source(segment=cantus_segment)
+        another_source = make_fake_source(segment=cantus_segment)
         chant_in_source = Chant.objects.create(source=source)
         chant_in_another_source = Chant.objects.create(source=another_source)
         response = self.client.get(reverse("chant-list"), {"source": source.id})
@@ -443,7 +460,8 @@ def test_filter_by_source(self):
         self.assertNotIn(chant_in_another_source, chants)
 
     def test_filter_by_feast(self):
-        source = make_fake_source()
+        cantus_segment = make_fake_segment(id=4063)
+        source = make_fake_source(segment=cantus_segment)
         feast = make_fake_feast()
         another_feast = make_fake_feast()
         chant_in_feast = Chant.objects.create(source=source, feast=feast)
@@ -458,7 +476,8 @@ def test_filter_by_feast(self):
         self.assertNotIn(chant_in_another_feast, chants)
 
     def test_filter_by_genre(self):
-        source = make_fake_source()
+        cantus_segment = make_fake_segment(id=4063)
+        source = make_fake_source(segment=cantus_segment)
         genre = make_fake_genre()
         another_genre = make_fake_genre()
         chant_in_genre = Chant.objects.create(source=source, genre=genre)
@@ -473,7 +492,8 @@ def test_filter_by_genre(self):
         self.assertNotIn(chant_in_another_genre, chants)
 
     def test_filter_by_folio(self):
-        source = make_fake_source()
+        cantus_segment = make_fake_segment(id=4063)
+        source = make_fake_source(segment=cantus_segment)
         chant_on_folio = Chant.objects.create(source=source, folio="001r")
         chant_on_another_folio = Chant.objects.create(source=source, folio="002r")
         response = self.client.get(
@@ -484,7 +504,8 @@ def test_filter_by_folio(self):
         self.assertNotIn(chant_on_another_folio, chants)
 
     def test_search_full_text(self):
-        source = make_fake_source()
+        cantus_segment = make_fake_segment(id=4063)
+        source = make_fake_source(segment=cantus_segment)
         chant = Chant.objects.create(
             source=source, manuscript_full_text=faker.sentence()
         )
@@ -495,7 +516,8 @@ def test_search_full_text(self):
         self.assertIn(chant, response.context["chants"])
 
     def test_search_incipit(self):
-        source = make_fake_source()
+        cantus_segment = make_fake_segment(id=4063)
+        source = make_fake_source(segment=cantus_segment)
         chant = Chant.objects.create(
             source=source,
             incipit=faker.sentence(),
@@ -507,7 +529,8 @@ def test_search_incipit(self):
         self.assertIn(chant, response.context["chants"])
 
     def test_search_full_text_std_spelling(self):
-        source = make_fake_source()
+        cantus_segment = make_fake_segment(id=4063)
+        source = make_fake_source(segment=cantus_segment)
         chant = Chant.objects.create(
             source=source,
             manuscript_full_text_std_spelling=faker.sentence(),
@@ -519,28 +542,28 @@ def test_search_full_text_std_spelling(self):
         self.assertIn(chant, response.context["chants"])
 
     def test_context_source(self):
-        source = make_fake_source()
+        cantus_segment = make_fake_segment(id=4063)
+        source = make_fake_source(segment=cantus_segment)
         response = self.client.get(reverse("chant-list"), {"source": source.id})
         self.assertEqual(source, response.context["source"])
 
     def test_context_folios(self):
-        # create a source and several chants in it
-        source = make_fake_source()
+        cantus_segment = make_fake_segment(id=4063)
+        source = make_fake_source(segment=cantus_segment)
         Chant.objects.create(source=source, folio="001r")
         Chant.objects.create(source=source, folio="001r")
         Chant.objects.create(source=source, folio="001v")
         Chant.objects.create(source=source, folio="001v")
         Chant.objects.create(source=source, folio="002r")
         Chant.objects.create(source=source, folio="002v")
-        # request the page
         response = self.client.get(reverse("chant-list"), {"source": source.id})
         # the element in "folios" should be unique and ordered in this way
         folios = response.context["folios"]
         self.assertEqual(list(folios), ["001r", "001v", "002r", "002v"])
 
     def test_context_feasts_with_folios(self):
-        # create a source and several chants (associated with feasts) in it
-        source = make_fake_source()
+        cantus_segment = make_fake_segment(id=4063)
+        source = make_fake_source(segment=cantus_segment)
         feast_1 = make_fake_feast()
         feast_2 = make_fake_feast()
         Chant.objects.create(source=source, folio="001r", feast=feast_1)
@@ -549,7 +572,6 @@ def test_context_feasts_with_folios(self):
         Chant.objects.create(source=source, folio="001v")
         Chant.objects.create(source=source, folio="001v", feast=feast_2)
         Chant.objects.create(source=source, folio="002r", feast=feast_1)
-        # request the page
         response = self.client.get(reverse("chant-list"), {"source": source.id})
         # context "feasts_with_folios" is a list of tuples
         # it records the folios where the feast changes
@@ -3330,6 +3352,28 @@ def test_published_vs_unpublished(self):
         response_2 = self.client.get(reverse("source-detail", args=[source.id]))
         self.assertEqual(response_2.status_code, 200)
 
+    def test_chant_list_link(self):
+        chant_list_link = reverse("chant-list")
+
+        cantus_segment = make_fake_segment(id=4063)
+        cantus_source = make_fake_source(segment=cantus_segment)
+        cantus_chant_list_link = chant_list_link + f"?source={cantus_source.id}"
+
+        cantus_source_response = self.client.get(
+            reverse("source-detail", args=[cantus_source.id])
+        )
+        cantus_source_html = str(cantus_source_response.content)
+        self.assertIn(cantus_chant_list_link, cantus_source_html)
+
+        bower_segment = make_fake_segment(id=4064)
+        bower_source = make_fake_source(segment=bower_segment)
+        bower_chant_list_link = chant_list_link + f"?source={bower_source.id}"
+        bower_source_response = self.client.get(
+            reverse("source-detail", args=[bower_source.id])
+        )
+        bower_source_html = str(bower_source_response.content)
+        self.assertNotIn(bower_chant_list_link, bower_source_html)
+
 
 class JsonMelodyExportTest(TestCase):
     def test_json_melody_response(self):