Repository: DDMAL/CantusDB

Issue:
Chant edit page: sequence/folio should be marked as required, be non-empty 
Spinning this off from #453 since it is about edits and not creating chants.
-Folio/sequence should be marked as required: 
![image](https://user-images.githubusercontent.com/67451875/205404152-dda8a84a-876a-4e2c-9509-d0dedd81325a.png)

-It is possible to delete folio numbers from the chant edit page: 
![image](https://user-images.githubusercontent.com/67451875/205404011-1c70e178-1e5e-4b90-b954-b1e0195048a0.png)

which then creates a chant you can no longer edit:
![image](https://user-images.githubusercontent.com/67451875/205404064-1d8fed29-3220-48c0-9379-ffd4b669cd88.png)

-Sequences are harder to delete, but this is because they seem to want to stay whatever they were before and not be edited (see #455 ), whether to an empty field or anything else. 




Discussion:

Resolving patch:
diff --git a/django/cantusdb_project/main_app/forms.py b/django/cantusdb_project/main_app/forms.py
--- a/django/cantusdb_project/main_app/forms.py
+++ b/django/cantusdb_project/main_app/forms.py
@@ -61,23 +61,26 @@ class Meta:
             "indexing_notes",
             "addendum",
         ]
+        # the widgets dictionary is ignored for a model field with a non-empty
+        # choices attribute. In this case, you must override the form field to
+        # use a different widget. this goes for all foreignkey and required fields
+        # here, which are written explicitly below to override form field
         widgets = {
             "marginalia": TextInputWidget(),
-            "folio": TextInputWidget(),
-            "c_sequence": TextInputWidget(),
-            # the widgets dictionary is ignored for a model field with a non-empty choices attribute.
-            # In this case, you must override the form field to use a different widget.
-            # this goes for all foreignkey fields here, which are written explicitly below to override form field
+            # folio: defined below (required)
+            # c_sequence: defined below (required)
+            # office: defined below (foreignkey)
+            # genre: defined below (foreignkey)
             "position": TextInputWidget(),
             "cantus_id": TextInputWidget(),
-            #'feast': SelectWidget(),
+            #'feast': defined below (foreignkey)
             "mode": TextInputWidget(),
             "differentia": TextInputWidget(),
             "differentia_new": TextInputWidget(),
             "finalis": TextInputWidget(),
             "extra": TextInputWidget(),
             "chant_range": VolpianoInputWidget(),
-            "manuscript_full_text_std_spelling": TextAreaWidget(),
+            # manuscript_full_text_std_spelling: defined below (required)
             "manuscript_full_text": TextAreaWidget(),
             "volpiano": VolpianoAreaWidget(),
             "image_link": TextInputWidget(),
@@ -86,19 +89,6 @@ class Meta:
             "indexing_notes": TextAreaWidget(),
             "addendum": TextInputWidget(),
         }
-        # error_messages = {
-        #     # specify custom error messages for each field here
-        # }
-
-    manuscript_full_text_std_spelling = forms.CharField(
-        required=True,
-        widget=TextAreaWidget,
-        help_text="Manuscript full text with standardized spelling. Enter the words "
-        "according to the manuscript but normalize their spellings following "
-        "Classical Latin forms. Use upper-case letters for proper nouns, "
-        'the first word of each chant, and the first word after "Alleluia" for '
-        "Mass Alleluias. Punctuation is omitted.",
-    )
 
     folio = forms.CharField(
         required=True, widget=TextInputWidget, help_text="Binding order",
@@ -123,6 +113,16 @@ class Meta:
     )
     feast.widget.attrs.update({"class": "form-control custom-select custom-select-sm"})
 
+    manuscript_full_text_std_spelling = forms.CharField(
+        required=True,
+        widget=TextAreaWidget,
+        help_text="Manuscript full text with standardized spelling. Enter the words "
+        "according to the manuscript but normalize their spellings following "
+        "Classical Latin forms. Use upper-case letters for proper nouns, "
+        'the first word of each chant, and the first word after "Alleluia" for '
+        "Mass Alleluias. Punctuation is omitted.",
+    )
+
     # automatically computed fields
     # source and incipit are mandatory fields in model,
     # but have to be optional in the form, otherwise the field validation won't pass
@@ -247,12 +247,15 @@ class Meta:
             "indexing_notes"
         ]
         widgets = {
-            "manuscript_full_text_std_spelling": TextAreaWidget(),
+            # manuscript_full_text_std_spelling: defined below (required)
             "manuscript_full_text": TextAreaWidget(),
             "volpiano": VolpianoAreaWidget(),
             "marginalia": TextInputWidget(),
-            "folio": TextInputWidget(),
-            "c_sequence": TextInputWidget(),
+            # folio: defined below (required)
+            # c_sequence: defined below (required)
+            # feast: defined below (foreignkey)
+            # office: defined below (foreignkey)
+            # genre: defined below (foreignkey)
             "position": TextInputWidget(),
             "cantus_id": TextInputWidget(),
             "melody_id": TextInputWidget(),
@@ -264,6 +267,25 @@ class Meta:
             "image_link": TextInputWidget(),
             "indexing_notes": TextAreaWidget()
         }
+
+    manuscript_full_text_std_spelling = forms.CharField(
+        required=True,
+        widget=TextAreaWidget,
+        help_text="Manuscript full text with standardized spelling. Enter the words "
+        "according to the manuscript but normalize their spellings following "
+        "Classical Latin forms. Use upper-case letters for proper nouns, "
+        'the first word of each chant, and the first word after "Alleluia" for '
+        "Mass Alleluias. Punctuation is omitted.",
+    )
+
+    folio = forms.CharField(
+        required=True, widget=TextInputWidget, help_text="Binding order",
+    )
+
+    c_sequence = forms.CharField(
+        required=True, widget=TextInputWidget, help_text="Each folio starts with '1'.",
+    )
+
     feast = forms.ModelChoiceField(
         queryset=Feast.objects.all().order_by("name"), required=False
     )
@@ -313,14 +335,14 @@ class Meta:
             "differentia_new"
         ]
         widgets = {
-            "manuscript_full_text_std_spelling": TextAreaWidget(),
+            # manuscript_full_text_std_spelling: defined below (required)
             "manuscript_full_text": TextAreaWidget(),
             "volpiano": VolpianoAreaWidget(),
             "marginalia": TextInputWidget(),
-            "folio": TextInputWidget(),
-            "c_sequence": TextInputWidget(),
-            "office": TextInputWidget(),
-            "genre": TextInputWidget(),
+            # folio: defined below (required)
+            # c_sequence: defined below (required)
+            # "office": defined below (foreignkey)
+            # "genre": defined below (foreignkey)
             "position": TextInputWidget(),
             "cantus_id": TextInputWidget(),
             "melody_id": TextInputWidget(),
@@ -340,10 +362,52 @@ class Meta:
             "addendum": TextInputWidget(),
             "differentia_new": TextInputWidget()
         }
+
+    manuscript_full_text_std_spelling = forms.CharField(
+        required=True,
+        widget=TextAreaWidget,
+        help_text="Manuscript full text with standardized spelling. Enter the words "
+        "according to the manuscript but normalize their spellings following "
+        "Classical Latin forms. Use upper-case letters for proper nouns, "
+        'the first word of each chant, and the first word after "Alleluia" for '
+        "Mass Alleluias. Punctuation is omitted.",
+    )
+
+    folio = forms.CharField(
+        required=True,
+        widget=TextInputWidget,
+        help_text="Binding order",
+    )
+
+    c_sequence = forms.CharField(
+        required=True,
+        widget=TextInputWidget,
+        help_text="Each folio starts with '1'.", 
+    )
+
     feast = forms.ModelChoiceField(
-        queryset=Feast.objects.all().order_by("name"), required=False
+        queryset=Feast.objects.all().order_by("name"),
+        required=False,
+    )
+    feast.widget.attrs.update(
+        {"class": "form-control custom-select custom-select-sm"}
+    )
+
+    office = forms.ModelChoiceField(
+        queryset=Office.objects.all().order_by("name"),
+        required=False,
+    )
+    office.widget.attrs.update(
+        {"class": "form-control custom-select custom-select-sm"}
+    )
+
+    genre = forms.ModelChoiceField(
+        queryset=Genre.objects.all().order_by("name"),
+        required=False
+    )
+    genre.widget.attrs.update(
+        {"class": "form-control custom-select custom-select-sm"}
     )
-    feast.widget.attrs.update({"class": "form-control custom-select custom-select-sm"})
 
     proofread_by = forms.ModelMultipleChoiceField(
         queryset=get_user_model().objects.filter(
@@ -351,7 +415,9 @@ class Meta:
             Q(groups__name="editor")
         ).order_by("last_name"), required=False
     )
-    proofread_by.widget.attrs.update({"class": "form-control custom-select custom-select-sm"})
+    proofread_by.widget.attrs.update(
+        {"class": "form-control custom-select custom-select-sm"}
+    )
 
 class SourceEditForm(forms.ModelForm):
     class Meta:


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
@@ -1192,7 +1192,7 @@ def test_url_and_templates(self):
 
     def test_update_chant(self):
         source = make_fake_source()
-        chant = Chant.objects.create(source=source, manuscript_full_text_std_spelling="initial")
+        chant = make_fake_chant(source=source, manuscript_full_text_std_spelling="initial")
 
         response = self.client.get(
             reverse('source-edit-chants', args=[source.id]), 
@@ -1200,9 +1200,16 @@ def test_update_chant(self):
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, "chant_edit.html")
 
+        folio = chant.folio
+        c_sequence = chant.c_sequence
         response = self.client.post(
             reverse('source-edit-chants', args=[source.id]), 
-            {'manuscript_full_text_std_spelling': 'test', 'pk': chant.id})
+            {
+                'manuscript_full_text_std_spelling': 'test',
+                'pk': chant.id,
+                "folio": folio,
+                "c_sequence": c_sequence,
+            })
         self.assertEqual(response.status_code, 302)
         # Check that after the edit, the user is redirected to the source-edit-chants page
         self.assertRedirects(response, reverse('source-edit-chants', args=[source.id]))  
@@ -1320,10 +1327,19 @@ def test_view_url_and_templates(self):
     
     def test_proofread_chant(self):
         chant = make_fake_chant(manuscript_full_text_std_spelling="lorem ipsum", folio="001r")
+        folio = chant.folio
+        c_sequence = chant.c_sequence
+        ms_std = chant.manuscript_full_text_std_spelling
         self.assertIs(chant.manuscript_full_text_std_proofread, False)
         source = chant.source
         response = self.client.post(f"/proofread-chant/{source.id}?pk={chant.id}&folio=001r", 
-            {'manuscript_full_text_std_proofread': 'True'})
+            {
+                'manuscript_full_text_std_proofread': 'True',
+                "folio": folio,
+                "c_sequence": c_sequence,
+                "manuscript_full_text_std_spelling": ms_std,
+            },
+        )
         self.assertEqual(response.status_code, 302) # 302 Found
         chant.refresh_from_db()
         self.assertIs(chant.manuscript_full_text_std_proofread, True)