Repository: DDMAL/CantusDB

Issue:
Include "Edit" button on source pages for logged-in contributors
If a contributor/proofreader/etc. is logged in, there should be an "Edit" button built into the template for each source that that user has permission to edit.

Context: when I worked as a transcriber for Cantus, I remember needing to follow a very specific series of links to reach the edit page for whatever manuscript I was working on at the time - if I navigated to the source page via search or clicked on the wrong link pointing to the source page, I would reach a dead-end where I would be on the page associated with the correct source, but had no option to edit the source. I remember being coached when I was first being oriented, along the lines of "click on _this_, even though it may seem reasonable to click on _that_", and still getting tripped up and confused after having being coached in this way - this should be an easy fix!


Discussion:
There is already an edit button for each source; what's a little weird is that there are two paths that take you to fractionally different edit forms. (This is bizarre, please do not replicate.)

For example, there is an edit button for this source, and also for its chant contents. 
![image](https://user-images.githubusercontent.com/67451875/171062500-054f0d6e-213e-41af-ad72-aff7b87989ba.png)

![image](https://user-images.githubusercontent.com/67451875/171062480-1de5ca41-e679-4e90-bbaf-77b1ca9a3574.png)

However, there is also a "source edit" button: 
![image](https://user-images.githubusercontent.com/67451875/171062559-84f83141-947b-4090-821d-a28af0464409.png). 

One edit page looks like this:

![image](https://user-images.githubusercontent.com/67451875/171062645-248d1335-3c2d-4bbd-9205-a1bff60eb86d.png)


and the other like this (which also lets you edit syllabification, though the first does not!)
![image](https://user-images.githubusercontent.com/67451875/171062733-3d38afd8-644e-49b7-aaf9-8d1b8e036eca.png)

To me it looks like these two chant-edit interfaces can be harmonized into one with the edit-syllabification feature. @jiwoojeongjj (the creator of the chant-edit page on new Cantus), any thoughts or questions on this?
This came up in our pair-programming discussion today. Apparently, the first interface: https://cantus.uwaterloo.ca/node/243157/edit and the second interface https://cantus.uwaterloo.ca/edit-volpiano/699578 are using almost the same form, but different sidebars. 

The first interface (accessed via the chant detail page) has the source information in the sidebar. 
![Screen Shot 2022-06-01 at 17 22 33](https://user-images.githubusercontent.com/54256124/171504119-a61b47a1-d17c-4ad9-ab3e-30dbc3b2fcd3.png)

The second interface (accessed via my-sources) has a list of all chants in the sidebar.
![Screen Shot 2022-06-01 at 17 24 10](https://user-images.githubusercontent.com/54256124/171504323-e1134fd7-3f07-4709-aa82-e6bdf2dfa88f.png)

@jacobdgm mentioned that these two interfaces might be used for different purposes. The first one is good for quickly fixing errors spotted on the chant detail page. The second one is good for systematically editing a series of chants within a manuscript, going from top to bottom. 

We plan to make the two forms look exactly the same (same fields, same layout) but with different sidebars. And the two forms are still accessed at different URLs, just like in the old Cantus. 
 
I have a suggestion: we should implement an `edit-chant` (singular) page, and also an `edit-chants` (plural) page.

- `edit-chant` would correspond to the current `currentnode/<chant ID>/edit` path - it should be reachable from any corresponding chant-detail page. Its purpose would be for doing things like making one-off corrections to individual chants. It would have a save button at the bottom of the page. Its url path would be something like `~/edit-chant/<chant ID>`.
- `edit-chants` would correspond to the current `edit-volpiano/<source ID>` path - it should be reachable from the corresponding source-detail page. Its purpose would be for transcribers/proofreaders working sequentially through all the chants in a source. It would have the sidebar with the column of EDIT buttons, and it would have a save-and-next-chant button at the bottom of the page. Its url path would be something like `~/edit-chants/<source ID>`. It would include the suggested-full-text feature as described in [issue 167](https://github.com/DDMAL/CantusDB/issues/167).

on the `edit-chant` (sing) page, we could have an "Edit surrounding chants" link which would lead to the corresponding `edit-chants` (pl) page.

Anyways, it looks like there are two similar-but-distinct editing-related tasks (1: making isolated edits, and 2: updating records sequentially), for which there are two different-but-similar pages. If this is indeed the purpose for these two pages, it's understandable that we're confused, since their URLs don't provide any information on their purposes (both of the current `node/12345/edit` and `edit_volpiano` edit chants, and you can edit the volpiano field on each page). The above suggestion would preserve the current functionality while clarifying what the different pages are meant to do.
@jacobdgm I quite like this idea, though I wonder if having the paths be one letter off would be confusing. It would certainly less confusing than it is now, though!
@annamorphism - this is a good point. Here's a way that we could possibly implement it, with the URL indicating more clearly the purpose of the page:

spot edits: `~/edit-chant/?chant=<chant ID>`
working through manuscript: `~/edit-chants/?source=<source ID>`


Resolving patch:
diff --git a/django/cantusdb_project/main_app/templatetags/helper_tags.py b/django/cantusdb_project/main_app/templatetags/helper_tags.py
--- a/django/cantusdb_project/main_app/templatetags/helper_tags.py
+++ b/django/cantusdb_project/main_app/templatetags/helper_tags.py
@@ -49,7 +49,7 @@ def make_add_new_chants_link(source):
         return link
     def make_edit_chants_link(source):
         id = source.id
-        url = reverse("source-edit-volpiano", args=[id])
+        url = reverse("source-edit-chants", args=[id])
         link = '<a href="{}">Edit chants (Fulltext & Volpiano editor)</a>'.format(url) 
         return link
     def make_links_for_source(source):
diff --git a/django/cantusdb_project/main_app/urls.py b/django/cantusdb_project/main_app/urls.py
--- a/django/cantusdb_project/main_app/urls.py
+++ b/django/cantusdb_project/main_app/urls.py
@@ -3,7 +3,7 @@
 from main_app.views import views
 from main_app.views.sequence import SequenceEditView
 from main_app.views.source import SourceCreateView, SourceEditView
-from main_app.views.chant import ChantEditVolpianoView, ChantProofreadView, ChantEditSyllabificationView
+from main_app.views.chant import SourceEditChantsView, ChantProofreadView, ChantEditSyllabificationView, SourceEditChantsView
 from main_app.views.user import UserDetailView, UserSourceListView, CustomLogoutView, UserListView, CustomLoginView
 
 urlpatterns = [
@@ -27,9 +27,9 @@
     ),
     path("chant-delete/<int:pk>", ChantDeleteView.as_view(), name="chant-delete"),
     path(
-        "edit-volpiano/<int:source_id>", 
-        ChantEditVolpianoView.as_view(), 
-        name="source-edit-volpiano"
+        "edit-chants/<int:source_id>", 
+        SourceEditChantsView.as_view(), 
+        name="source-edit-chants"
     ),
     path(
         "proofread-chant/<int:source_id>", 
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
@@ -857,7 +857,7 @@ def test_func(self):
             return False
 
     def get_success_url(self):
-        return reverse("source-edit-volpiano", args=[self.object.source.id])
+        return reverse("source-edit-chants", args=[self.object.source.id])
 
 class CISearchView(TemplateView):
     """search in CI and write results in get_context_data
@@ -938,7 +938,7 @@ def get_context_data(self, **kwargs):
 
         return context
 
-class ChantEditVolpianoView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
+class SourceEditChantsView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
     template_name = "chant_edit.html"
     model = Chant
     form_class = ChantEditForm
@@ -1267,7 +1267,7 @@ def get_success_url(self):
         # stay on the same page after save
         return self.request.get_full_path()
 
-class ChantProofreadView(ChantEditVolpianoView):
+class ChantProofreadView(SourceEditChantsView):
     template_name = "chant_proofread.html"
     model = Chant
     form_class = ChantProofreadForm


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
@@ -102,9 +102,9 @@ def test_login(self):
         response = self.client.get(f'/chant-delete/{chant.id}')
         self.assertRedirects(response, f'/login/?next=/chant-delete/{chant.id}')        
         
-        # ChantEditVolpianoView
-        response = self.client.get(f'/edit-volpiano/{source.id}')
-        self.assertRedirects(response, f'/login/?next=/edit-volpiano/{source.id}')        
+        # SourceEditChantsView
+        response = self.client.get(f'/edit-chants/{source.id}')
+        self.assertRedirects(response, f'/login/?next=/edit-chants/{source.id}')        
 
         # SequenceEditView
         response = self.client.get(f'/edit-sequence/{sequence.id}')
@@ -144,8 +144,8 @@ def test_permissions_project_manager(self):
         response = self.client.get(f'/chant-delete/{chant.id}')
         self.assertEqual(response.status_code, 200)
 
-        # ChantEditVolpianoView
-        response = self.client.get(f'/edit-volpiano/{source.id}')
+        # SourceEditChantsView
+        response = self.client.get(f'/edit-chants/{source.id}')
         self.assertEqual(response.status_code, 200)
 
         # SequenceEditView
@@ -207,14 +207,14 @@ def test_permissions_contributor(self):
         response = self.client.get(f'/chant-delete/{chant_in_assigned_source.id}')
         self.assertEqual(response.status_code, 200)
 
-        # ChantEditVolpianoView
-        response = self.client.get(f'/edit-volpiano/{restricted_source.id}')
+        # SourceEditChantsView
+        response = self.client.get(f'/edit-chants/{restricted_source.id}')
         self.assertEqual(response.status_code, 403)
 
-        response = self.client.get(f'/edit-volpiano/{source_created_by_contributor.id}')
+        response = self.client.get(f'/edit-chants/{source_created_by_contributor.id}')
         self.assertEqual(response.status_code, 200)
 
-        response = self.client.get(f'/edit-volpiano/{assigned_source.id}')
+        response = self.client.get(f'/edit-chants/{assigned_source.id}')
         self.assertEqual(response.status_code, 200)
 
         # SequenceEditView
@@ -282,14 +282,14 @@ def test_permissions_editor(self):
         response = self.client.get(f'/chant-delete/{chant_in_assigned_source.id}')
         self.assertEqual(response.status_code, 200)
 
-        # ChantEditVolpianoView
-        response = self.client.get(f'/edit-volpiano/{restricted_source.id}')
+        # SourceEditChantsView
+        response = self.client.get(f'/edit-chants/{restricted_source.id}')
         self.assertEqual(response.status_code, 403)
 
-        response = self.client.get(f'/edit-volpiano/{source_created_by_contributor.id}')
+        response = self.client.get(f'/edit-chants/{source_created_by_contributor.id}')
         self.assertEqual(response.status_code, 200)
 
-        response = self.client.get(f'/edit-volpiano/{assigned_source.id}')
+        response = self.client.get(f'/edit-chants/{assigned_source.id}')
         self.assertEqual(response.status_code, 200)
 
         # SequenceEditView
@@ -326,8 +326,8 @@ def test_permissions_default(self):
         response = self.client.get(f'/chant-delete/{chant.id}')
         self.assertEqual(response.status_code, 403)
 
-        # ChantEditVolpianoView
-        response = self.client.get(f'/edit-volpiano/{source.id}')
+        # SourceEditChantsView
+        response = self.client.get(f'/edit-chants/{source.id}')
         self.assertEqual(response.status_code, 403)
 
         # SequenceEditView
@@ -1119,7 +1119,7 @@ def test_non_existing_chant(self):
         self.assertEqual(response.status_code, 404)
 
 
-class ChantEditVolpianoViewTest(TestCase):
+class SourceEditChantsViewTest(TestCase):
     @classmethod
     def setUpTestData(cls):
         Group.objects.create(name="project manager")
@@ -1136,21 +1136,21 @@ def setUp(self):
     def test_url_and_templates(self):
         source1 = make_fake_source()
 
-        # must specify folio, or ChantEditVolpianoView.get_queryset will fail when it tries to default to displaying the first folio
+        # 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-volpiano", args=[source1.id]))
+        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-volpiano", args=[source1.id + 100]))
+        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 404
         source2 = make_fake_source()
 
-        response = self.client.get(reverse("source-edit-volpiano", args=[source2.id]))
+        response = self.client.get(reverse("source-edit-chants", args=[source2.id]))
         self.assertEqual(response.status_code, 404)
         self.assertTemplateUsed(response, "404.html")
 
@@ -1159,17 +1159,17 @@ def test_update_chant(self):
         chant = Chant.objects.create(source=source, manuscript_full_text_std_spelling="initial")
 
         response = self.client.get(
-            reverse('source-edit-volpiano', args=[source.id]), 
+            reverse('source-edit-chants', args=[source.id]), 
             {'pk': chant.id})
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, "chant_edit.html")
 
         response = self.client.post(
-            reverse('source-edit-volpiano', args=[source.id]), 
+            reverse('source-edit-chants', args=[source.id]), 
             {'manuscript_full_text_std_spelling': 'test', 'pk': chant.id})
         self.assertEqual(response.status_code, 302)
-        # Check that after the edit, the user is redirected to the source-edit-volpiano page
-        self.assertRedirects(response, reverse('source-edit-volpiano', args=[source.id]))  
+        # 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]))  
         chant.refresh_from_db()
         self.assertEqual(chant.manuscript_full_text_std_spelling, 'test')
     
@@ -1178,7 +1178,7 @@ def test_provide_suggested_fulltext(self):
         chant = make_fake_chant(source=source, manuscript_full_text_std_spelling="",
         cantus_id="007450")
         response = self.client.get(
-            reverse('source-edit-volpiano', args=[source.id]), 
+            reverse('source-edit-chants', args=[source.id]), 
             {'pk': chant.id}
         )
         # expected_suggestion is copied from Cantus Index. If this test is failing,
@@ -1197,7 +1197,7 @@ def test_volpiano_signal(self):
             sequence_number=1
         )
         self.client.post(
-            reverse('source-edit-volpiano', args=[source.id]),
+            reverse('source-edit-chants', args=[source.id]),
             {
                 "manuscript_full_text_std_spelling": "ut queant lactose",
                 "folio": "001r",
@@ -1222,7 +1222,7 @@ def test_volpiano_signal(self):
             sequence_number=2
         )
         self.client.post(
-            reverse('source-edit-volpiano', args=[source.id]),  
+            reverse('source-edit-chants', args=[source.id]),  
             {
                 "manuscript_full_text_std_spelling": "resonare foobaz",
                 "folio": "001r",