Repository: DDMAL/CantusDB

Issue:
edit-volpiano page lacks Edit Syllabification link, preview of Melody and Text
We have `latin_syllabification.py` in `/`, and this is where I imagine the functions therein would be used, but it doesn't seem to be implemented.


Discussion:
Yes. This has not been implemented. The `latin_syllabification.py` script has the utilities for actually doing the syllabification. It takes a full text and return a list of syllables. We need to implement the "edit syllabification" and "preview of melody and text" using the `latin_syllabification.py` script.

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
@@ -501,3 +501,15 @@ class Meta:
         queryset=Source.objects.all().order_by("title"), required = False
     )
     source.widget.attrs.update({"class": "form-control custom-select custom-select-sm"})
+
+class ChantEditSyllabificationForm(forms.ModelForm):
+    class Meta:
+        model = Chant
+        fields = [
+            "manuscript_full_text",
+            "manuscript_syllabized_full_text",
+        ]
+        widgets = {
+            "manuscript_full_text": TextAreaWidget(),
+            "manuscript_syllabized_full_text": TextAreaWidget(),
+        }
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
+from main_app.views.chant import ChantEditVolpianoView, ChantProofreadView, ChantEditSyllabificationView
 from main_app.views.user import UserDetailView, UserSourceListView, CustomLogoutView, UserListView, CustomLoginView
 
 urlpatterns = [
@@ -37,6 +37,11 @@
         "proofread-chant/<int:source_id>", 
         ChantProofreadView.as_view(), 
         name="chant-proofread"
+    ),
+        path(
+        "edit-syllabification/<int:chant_id>",
+        ChantEditSyllabificationView.as_view(),
+        name="source-edit-syllabification"
     ),
     # feast
     path("feasts/", FeastListView.as_view(), name="feast-list"),
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
@@ -16,7 +16,7 @@
     UpdateView,
 )
 from django.core.exceptions import PermissionDenied
-from main_app.forms import ChantCreateForm, ChantEditForm, ChantProofreadForm
+from main_app.forms import ChantCreateForm, ChantEditForm, ChantProofreadForm, ChantEditSyllabificationForm
 from main_app.models import Chant, Feast, Genre, Source, Sequence
 from align_text_mel import *
 from django.contrib.auth.mixins import LoginRequiredMixin
@@ -1180,6 +1180,33 @@ def get_chants_with_folios(chants_in_feast):
             )
             context["suggested_fulltext"] = json.loads(request.text[2:])[0]["fulltext"]
 
+        chant = self.get_object()
+
+        # Preview of melody and text:
+        # in the old CantusDB,
+        # 'manuscript_syllabized_full_text' exists => preview constructed from 'manuscript_syllabized_full_text'
+        # no 'manuscript_syllabized_full_text', but 'manuscript_full_text' exists => preview constructed from 'manuscript_full_text'
+        # no 'manuscript_syllabized_full_text' and no 'manuscript_full_text' => preview constructed from 'manuscript_full_text_std_spelling'
+
+        if chant.volpiano:
+            syls_melody = syllabize_melody(chant.volpiano)
+
+            if chant.manuscript_syllabized_full_text:
+                syls_text = syllabize_text(
+                    chant.manuscript_syllabized_full_text, pre_syllabized=True
+                )
+            elif chant.manuscript_full_text:
+                syls_text = syllabize_text(
+                    chant.manuscript_full_text, pre_syllabized=False
+                )
+                syls_text, syls_melody = postprocess(syls_text, syls_melody)
+            elif chant.manuscript_full_text_std_spelling:
+                syls_text = syllabize_text(chant.manuscript_full_text_std_spelling, pre_syllabized=False)
+                syls_text, syls_melody = postprocess(syls_text, syls_melody)
+
+            word_zip = align(syls_text, syls_melody)
+            context["syllabized_text_with_melody"] = word_zip
+
         return context
     
     def form_valid(self, form):
@@ -1218,3 +1245,76 @@ def test_func(self):
             return True
         else:
             return False
+
+class ChantEditSyllabificationView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
+    template_name = "chant_syllabification_edit.html"
+    model = Chant
+    context_object_name = "chant"
+    form_class = ChantEditSyllabificationForm
+    pk_url_kwarg = "chant_id"
+
+    def test_func(self):
+        chant = self.get_object()
+        source = chant.source
+        user = self.request.user
+
+        assigned_to_source = user.sources_user_can_edit.filter(id=source.id)
+
+        # checks if the user is a project manager
+        is_project_manager = user.groups.filter(name="project manager").exists()
+        # checks if the user is an editor,
+        is_editor = user.groups.filter(name="editor").exists()
+        # checks if the user is a contributor,
+        is_contributor = user.groups.filter(name="contributor").exists()
+
+        if ((is_project_manager)
+            or (is_editor and assigned_to_source)
+            or (is_editor and source.created_by == user)
+            or (is_contributor and assigned_to_source)
+            or (is_contributor and source.created_by == user)):
+            return True
+        else:
+            return False
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        chant = self.get_object()
+
+        # Preview of melody and text:
+        # in the old CantusDB,
+        # 'manuscript_syllabized_full_text' exists => preview constructed from 'manuscript_syllabized_full_text'
+        # no 'manuscript_syllabized_full_text', but 'manuscript_full_text' exists => preview constructed from 'manuscript_full_text'
+        # no 'manuscript_syllabized_full_text' and no 'manuscript_full_text' => preview constructed from 'manuscript_full_text_std_spelling'
+
+        if chant.volpiano:
+            syls_melody = syllabize_melody(chant.volpiano)
+
+            if chant.manuscript_syllabized_full_text:
+                syls_text = syllabize_text(
+                    chant.manuscript_syllabized_full_text, pre_syllabized=True
+                )
+            elif chant.manuscript_full_text:
+                syls_text = syllabize_text(
+                    chant.manuscript_full_text, pre_syllabized=False
+                )
+                syls_text, syls_melody = postprocess(syls_text, syls_melody)
+            elif chant.manuscript_full_text_std_spelling:
+                syls_text = syllabize_text(chant.manuscript_full_text_std_spelling, pre_syllabized=False)
+                syls_text, syls_melody = postprocess(syls_text, syls_melody)
+
+            word_zip = align(syls_text, syls_melody)
+            context["syllabized_text_with_melody"] = word_zip
+
+        return context
+
+    def form_valid(self, form):
+        form.instance.last_updated_by = self.request.user
+        messages.success(
+            self.request,
+            "Syllabification updated successfully!",
+        )
+        return super().form_valid(form)
+
+    def get_success_url(self):
+        # stay on the same page after save
+        return self.request.get_full_path()


Test patch: