Repository: DDMAL/CantusDB

Issue:
Chant Edit view - We need to reimplement the Autofill Suggested Fulltext feature
This feature was turned off by https://github.com/DDMAL/CantusDB/pull/1142, and we need to turn it back on again.

https://github.com/DDMAL/CantusDB/commit/10b3bd31138133f7e6f9aeea6cd86386a5bed56b was the commit responsible for removing this functionality. (https://github.com/DDMAL/CantusDB/commit/1947b2c6b8f0d44ab8f1f83a4d3425d4e53f7859 could also be useful for writing tests for this view)

In contrast to #1388 and #1389, I think it would be best to make the necessary request to CI server-side:
- we don't expect an enormous volume of traffic to Chant Edit pages - this could perhaps be ~10r/min when multiple people are logged in and editing chants at the same time
- I can imagine it being bothersome if the page loaded, I began interacting with the fields on the page, and then a response came back from CI causing a field I was typing in to change.

We can use CI's https://cantusindex.uwaterloo.ca/json-cid/{CantusID} API to get the fulltext for the chant in question. 


Discussion:
Assigning this a priority of "high" - this is a feature that OldCantus had that we don't currently have.

Resolving patch:
diff --git a/django/cantusdb_project/cantusindex.py b/django/cantusdb_project/cantusindex.py
--- a/django/cantusdb_project/cantusindex.py
+++ b/django/cantusdb_project/cantusindex.py
@@ -112,6 +112,22 @@ def get_suggested_chant(
     }
 
 
+def get_suggested_fulltext(cantus_id: str) -> str:
+    endpoint_path: str = f"/json-cid/{cantus_id}"
+    json: Union[dict, list, None] = get_json_from_ci_api(endpoint_path)
+
+    if not isinstance(json, dict):
+        # mostly, in case of a timeout within get_json_from_ci_api
+        return None
+
+    try:
+        suggested_fulltext = json["info"]["field_full_text"]
+    except KeyError:
+        return None
+
+    return suggested_fulltext
+
+
 def get_json_from_ci_api(
     path: str, timeout: float = DEFAULT_TIMEOUT
 ) -> Union[dict, list, None]:
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
@@ -44,7 +44,7 @@
     flatten_syllabified_text,
 )
 from volpiano_display_utilities.text_volpiano_alignment import align_text_and_volpiano
-from cantusindex import get_suggested_chants
+from cantusindex import get_suggested_chants, get_suggested_fulltext
 
 CHANT_SEARCH_TEMPLATE_VALUES: tuple[str, ...] = (
     # for views that use chant_search.html, this allows them to
@@ -1116,6 +1116,17 @@ def get_chants_with_folios(chants_in_feast):
 
         user = self.request.user
         context["user_can_proofread_chant"] = user_can_proofread_chant(user, chant)
+        # in case the chant has no manuscript_full_text_std_spelling, we check Cantus Index
+        # for the expected text for chants with the same Cantus ID, and pass it to the context
+        # to suggest it to the user
+        if not chant:
+            return context
+        cantus_id = chant.cantus_id
+        if not cantus_id:
+            return context
+        if not chant.manuscript_full_text_std_spelling:
+            suggested_fulltext = get_suggested_fulltext(chant.cantus_id)
+            context["suggested_fulltext"] = suggested_fulltext
         return context
 
     def form_valid(self, form):


Test patch:
diff --git a/django/cantusdb_project/main_app/tests/test_functions.py b/django/cantusdb_project/main_app/tests/test_functions.py
--- a/django/cantusdb_project/main_app/tests/test_functions.py
+++ b/django/cantusdb_project/main_app/tests/test_functions.py
@@ -19,6 +19,7 @@
     get_suggested_chants,
     get_json_from_ci_api,
     CANTUS_INDEX_DOMAIN,
+    get_suggested_fulltext,
 )
 
 # run with `python -Wa manage.py test main_app.tests.test_functions`
@@ -403,3 +404,17 @@ def test_get_json_from_ci_api(self):
             self.assertRaises(
                 ValueError, get_json_from_ci_api, "path/lacking/a/leading/slash"
             )
+
+    def test_get_suggested_fulltext(self) -> None:
+        with self.subTest("Test CantusID with full text"):
+            with patch("requests.get", mock_requests_get):
+                fulltext = get_suggested_fulltext("008349")
+            self.assertEqual(
+                fulltext,
+                "Nocte surgentes vigilemus omnes semper in psalmis meditemur atque viribus totis domino canamus dulciter hymnos | Ut pio regi pariter canentes cum suis sanctis mereamur aulam ingredi caeli simul et beatam ducere vitam | Praestet hoc nobis deitas beata patris ac nati pariterque sancti spiritus cujus resonat per omnem gloria mundum | Amen",
+            )
+
+        with self.subTest("Test invalid CantusID"):
+            with patch("requests.get", mock_requests_get):
+                fulltext = get_suggested_fulltext("999999")
+            self.assertIsNone(fulltext)
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
@@ -3076,20 +3076,21 @@ def test_url_and_templates(self):
         # must specify folio, or SourceEditChantsView.get_queryset will fail when it tries to default to displaying the first folio
         Chant.objects.create(source=source1, folio="001r")
 
-        response = self.client.get(reverse("source-edit-chants", args=[source1.id]))
+        with patch("requests.get", mock_requests_get):
+            response = self.client.get(reverse("source-edit-chants", args=[source1.id]))
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, "chant_edit.html")
-
-        response = self.client.get(
-            reverse("source-edit-chants", args=[source1.id + 100])
-        )
+        with patch("requests.get", mock_requests_get):
+            response = self.client.get(
+                reverse("source-edit-chants", args=[source1.id + 100])
+            )
         self.assertEqual(response.status_code, 404)
         self.assertTemplateUsed(response, "404.html")
 
         # trying to access chant-edit with a source that has no chant should return 200
         source2 = make_fake_source()
-
-        response = self.client.get(reverse("source-edit-chants", args=[source2.id]))
+        with patch("requests.get", mock_requests_get):
+            response = self.client.get(reverse("source-edit-chants", args=[source2.id]))
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, "chant_edit.html")
 
@@ -3098,10 +3099,10 @@ def test_update_chant(self):
         chant = make_fake_chant(
             source=source, manuscript_full_text_std_spelling="initial"
         )
-
-        response = self.client.get(
-            reverse("source-edit-chants", args=[source.id]), {"pk": chant.id}
-        )
+        with patch("requests.get", mock_requests_get):
+            response = self.client.get(
+                reverse("source-edit-chants", args=[source.id]), {"pk": chant.id}
+            )
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, "chant_edit.html")
 
@@ -3166,7 +3167,10 @@ def test_volpiano_signal(self):
                 "volpiano": "abacadaeafagahaja",
             },
         )
-        chant_2 = Chant.objects.get(manuscript_full_text_std_spelling="resonare foobaz")
+        with patch("requests.get", mock_requests_get):
+            chant_2 = Chant.objects.get(
+                manuscript_full_text_std_spelling="resonare foobaz"
+            )
         self.assertEqual(chant_2.volpiano, expected_volpiano)
         self.assertEqual(chant_2.volpiano_intervals, expected_intervals)