Repository: DDMAL/CantusDB

Issue:
We need to rework our json-cid API
Jan recently sent an email saying that work is progressing on his One True API, and he requested that we update our /json-cid/{cantus_id} API with some new information required to make the new system work.

He included a specification:

> To obtain concordances and analysis data from the new CD, I would need a URL for a JSON export that includes all CD chants with specific columns. This file should be updated at least daily (we can later discuss the update frequency). There is also a "sequence" field necessary for suggesting new chants and feasts in the Cantus Input Tool.
> 
> List of columns:   
> 
> |   |   |
> |---|---|
> |siglum|varchar(255)|
> |srclink|varchar(255)|
> |chantlink|varchar(255)|
> |folio|varchar(255)|
> |sequence|smallint|
> |incipit|varchar(255)|
> |feast|varchar(255)|
> |genre|varchar(255)|
> |office|varchar(255)|
> |position|varchar(255)|
> |cantus_id|varchar(255)|
> |image|longtext|
> |mode|varchar(255)|
> |full_text|longtext|
> |melody|longtext|
> |db|varchar(255)|


Discussion:

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
@@ -610,22 +610,22 @@ def build_json_cid_dictionary(chant, request) -> dict:
         "siglum": chant.source.siglum,
         "srclink": source_absolute_url,
         "chantlink": chant_absolute_url,
-        # "chantlinkOLD":  # OldCantus included a URL using http:// here,
-        #                  # whereas "chantlink" had a URL with https://
         "folio": chant.folio if chant.folio else "",
+        "sequence": chant.c_sequence if chant.c_sequence else 0,
         "incipit": chant.incipit if chant.incipit else "",
         "feast": chant.feast.name if chant.feast else "",
         "genre": chant.genre.name if chant.genre else "",
         "office": chant.office.name if chant.office else "",
         "position": chant.position if chant.position else "",
-        "mode": chant.mode if chant.mode else "",
+        "cantus_id": chant.cantus_id if chant.cantus_id else "",
         "image": chant.image_link if chant.image_link else "",
-        "melody": chant.volpiano if chant.volpiano else "",
-        "fulltext": (
+        "mode": chant.mode if chant.mode else "",
+        "full_text": (
             chant.manuscript_full_text_std_spelling
             if chant.manuscript_full_text_std_spelling
             else ""
         ),
+        "melody": chant.volpiano if chant.volpiano else "",
         "db": "CD",
     }
     return dictionary


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
@@ -4854,19 +4854,21 @@ def test_structure(self):
         {
             "chants": [
                 "chant": {
-                    "siglum": "some value"
-                    "srclink": "some value"
-                    "chantlink": "some value"
-                    "folio": "some value"
-                    "incipit": "some value"
-                    "feast": "some value"
-                    "genre": "some value"
-                    "office": "some value"
-                    "position": "some value"
-                    "mode": "some value"
-                    "image": "some value"
-                    "melody": "some value"
-                    "fulltext": "some value"
+                    "siglum": "some string"
+                    "srclink": "some string"
+                    "chantlink": "some string"
+                    "folio": "some string"
+                    "sequence": some_integer
+                    "incipit": "some string"
+                    "feast": "some string"
+                    "genre": "some string"
+                    "office": "some string"
+                    "position": "some string"
+                    "cantus_id": "some string"
+                    "image": "some string"
+                    "mode": "some string"
+                    "full_text": "some string"
+                    "melody": "some string"
                     "db": "CD"
                 },
                 "chant": {
@@ -4874,6 +4876,8 @@ def test_structure(self):
                 },
             ]
         }
+        A more complete specification can be found at
+        https://github.com/DDMAL/CantusDB/issues/1170.
         """
         for _ in range(7):
             make_fake_chant(cantus_id="3.14159")
@@ -4895,29 +4899,34 @@ def test_structure(self):
 
         first_chant = first_item["chant"]
         chant_keys = first_chant.keys()
-        expected_keys = [
+        expected_keys = {
             "siglum",
             "srclink",
             "chantlink",
             "folio",
+            "sequence",
             "incipit",
             "feast",
             "genre",
             "office",
             "position",
-            "mode",
+            "cantus_id",
             "image",
+            "mode",
+            "full_text",
             "melody",
-            "fulltext",
             "db",
-        ]
-        self.assertEqual(list(chant_keys), expected_keys)
+        }
+        self.assertEqual(set(chant_keys), expected_keys)
 
     def test_values(self):
         chant = make_fake_chant(cantus_id="100000")
         expected_values = {
             "siglum": chant.source.siglum,
+            "srclink": f"http://testserver/source/{chant.source.id}",
+            "chantlink": f"http://testserver/chant/{chant.id}",
             "folio": chant.folio,
+            "sequence": chant.c_sequence,
             "incipit": chant.incipit,
             "feast": chant.feast.name,
             "genre": chant.genre.name,
@@ -4926,7 +4935,7 @@ def test_values(self):
             "mode": chant.mode,
             "image": chant.image_link,
             "melody": chant.volpiano,
-            "fulltext": chant.manuscript_full_text_std_spelling,
+            "full_text": chant.manuscript_full_text_std_spelling,
             "db": "CD",
         }
         response_1 = self.client.get(
@@ -4954,8 +4963,18 @@ def test_values(self):
             reverse("json-cid-export", args=["100000"]),
         )
         json_for_one_chant_2 = response_2.json()["chants"][0]["chant"]
-        for item in json_for_one_chant_2.items():
-            self.assertIsInstance(item[1], str)  # we shouldn't see any Nones or nulls
+
+        sequence_value = json_for_one_chant_2.pop("sequence")
+        self.assertIsInstance(sequence_value, int)
+
+        for key, value in json_for_one_chant_2.items():
+            with self.subTest(key=key):
+                self.assertIsInstance(
+                    value,
+                    str,  # we've already removed ["sequence"], which should
+                    # be an int. All other keys should be strings, and there should
+                    # be no Nones or nulls
+                )
 
         chant.manuscript_full_text = "nahn-staendrd spillynge"
         chant.manuscript_full_text_std_spelling = "standard spelling"
@@ -4964,7 +4983,7 @@ def test_values(self):
             reverse("json-cid-export", args=["100000"]),
         )
         json_for_one_chant_3 = response_3.json()["chants"][0]["chant"]
-        self.assertEqual(json_for_one_chant_3["fulltext"], "standard spelling")
+        self.assertEqual(json_for_one_chant_3["full_text"], "standard spelling")
 
 
 class CsvExportTest(TestCase):