diff --git a/django/cantusdb_project/main_app/forms.py b/django/cantusdb_project/main_app/forms.py index ad150622b..322a70870 100644 --- a/django/cantusdb_project/main_app/forms.py +++ b/django/cantusdb_project/main_app/forms.py @@ -42,6 +42,15 @@ (False, "Partial inventory"), ) +# Define choices for Chant model's +# various proofreading fields: manuscript_full_text_std_proofread, +# manuscript_full_text_proofread, volpiano_proofread +PROOFREAD_CHOICES = [ + (None, "Any"), + (True, "Yes"), + (False, "No"), +] + class NameModelChoiceField(forms.ModelChoiceField): """ @@ -520,6 +529,28 @@ class Meta: ) +class SourceBrowseChantsProofreadForm(forms.Form): + manuscript_full_text_std_proofread = forms.ChoiceField( + label="Full text as in Source (standardized spelling) proofread", + choices=PROOFREAD_CHOICES, + widget=forms.RadioSelect, + required=False, + ) + manuscript_full_text_proofread = forms.ChoiceField( + label="Full text as in Source (source spelling) proofread", + choices=PROOFREAD_CHOICES, + widget=forms.RadioSelect, + required=False, + ) + + volpiano_proofread = forms.ChoiceField( + label="Volpiano proofread", + choices=PROOFREAD_CHOICES, + widget=forms.RadioSelect, + required=False, + ) + + class SequenceEditForm(forms.ModelForm): class Meta: model = Sequence diff --git a/django/cantusdb_project/main_app/permissions.py b/django/cantusdb_project/main_app/permissions.py index 6b48b7253..bd36e909e 100644 --- a/django/cantusdb_project/main_app/permissions.py +++ b/django/cantusdb_project/main_app/permissions.py @@ -51,6 +51,8 @@ def user_can_proofread_chant(user: User, chant: Chant) -> bool: return False source_id = chant.source.id + user_can_proofread_src = user_can_proofread_source(user, chant.source) + user_is_assigned_to_source: bool = user.sources_user_can_edit.filter( # noqa id=source_id ).exists() @@ -58,6 +60,30 @@ def user_can_proofread_chant(user: User, chant: Chant) -> bool: user_is_project_manager: bool = user.groups.filter(name="project manager").exists() user_is_editor: bool = user.groups.filter(name="editor").exists() + return user_can_proofread_src and ( + user_is_project_manager or (user_is_editor and user_is_assigned_to_source) + ) + + +def user_can_proofread_source(user: User, source: Source) -> bool: + """ + Checks if the user can access the proofreading page of a given Source. + Used in SourceBrowseChantsView. + """ + if user.is_superuser: + return True + + if user.is_anonymous: + return False + + source_id = source.id + user_is_assigned_to_source: bool = user.sources_user_can_edit.filter( + id=source_id + ).exists() + + user_is_project_manager: bool = user.groups.filter(name="project manager").exists() + user_is_editor: bool = user.groups.filter(name="editor").exists() + return user_is_project_manager or (user_is_editor and user_is_assigned_to_source) diff --git a/django/cantusdb_project/main_app/templates/browse_chants.html b/django/cantusdb_project/main_app/templates/browse_chants.html index 771aceac0..d8d63bb19 100644 --- a/django/cantusdb_project/main_app/templates/browse_chants.html +++ b/django/cantusdb_project/main_app/templates/browse_chants.html @@ -58,6 +58,21 @@

Browse Chants

+ {% if user_can_proofread_source %} +
+ {% for field in proofread_filter_form %} +
+ + {% for radio in field %} + + {% endfor %} +
+ {% endfor %} +
+ {% endif %} {% with exists_on_cantus_ultimus=source.exists_on_cantus_ultimus %} diff --git a/django/cantusdb_project/main_app/tests/test_views/test_chant.py b/django/cantusdb_project/main_app/tests/test_views/test_chant.py index 09b995cb3..5cfeb3340 100644 --- a/django/cantusdb_project/main_app/tests/test_views/test_chant.py +++ b/django/cantusdb_project/main_app/tests/test_views/test_chant.py @@ -305,7 +305,9 @@ def test_chant_with_volpiano_with_no_incipit(self): def test_proofread_chant(self): chant = make_fake_chant( - manuscript_full_text_std_spelling="lorem ipsum", folio="001r" + manuscript_full_text_std_spelling="lorem ipsum", + folio="001r", + manuscript_full_text_std_proofread=False, ) folio = chant.folio c_sequence = chant.c_sequence diff --git a/django/cantusdb_project/main_app/tests/test_views/test_source.py b/django/cantusdb_project/main_app/tests/test_views/test_source.py index 11e7565cf..41180e6ea 100644 --- a/django/cantusdb_project/main_app/tests/test_views/test_source.py +++ b/django/cantusdb_project/main_app/tests/test_views/test_source.py @@ -602,6 +602,82 @@ def test_search_full_text_std_spelling(self): ) self.assertIn(chant, response.context["chants"]) + def test_search_proofread(self): + cantus_segment = make_fake_segment(id=4063) + source = make_fake_source(segment=cantus_segment) + chant_std_proofread = make_fake_chant( + source=source, + manuscript_full_text_std_proofread=True, + manuscript_full_text_proofread=False, + volpiano_proofread=False, + ) + chant_ft_proofread = make_fake_chant( + source=source, + manuscript_full_text_std_proofread=False, + manuscript_full_text_proofread=True, + volpiano_proofread=False, + ) + chant_volpiano_proofread = make_fake_chant( + source=source, + manuscript_full_text_std_proofread=False, + manuscript_full_text_proofread=False, + volpiano_proofread=True, + ) + response = self.client.get( + reverse("browse-chants", args=[source.id]), + ) + self.assertIn(chant_std_proofread, response.context["chants"]) + self.assertIn(chant_ft_proofread, response.context["chants"]) + self.assertIn(chant_volpiano_proofread, response.context["chants"]) + + response = self.client.get( + reverse("browse-chants", args=[source.id]), + {"manuscript_full_text_std_proofread": True}, + ) + self.assertIn(chant_std_proofread, response.context["chants"]) + self.assertNotIn(chant_ft_proofread, response.context["chants"]) + self.assertNotIn(chant_volpiano_proofread, response.context["chants"]) + + response = self.client.get( + reverse("browse-chants", args=[source.id]), + {"manuscript_full_text_std_proofread": False}, + ) + self.assertNotIn(chant_std_proofread, response.context["chants"]) + self.assertIn(chant_ft_proofread, response.context["chants"]) + self.assertIn(chant_volpiano_proofread, response.context["chants"]) + + response = self.client.get( + reverse("browse-chants", args=[source.id]), + {"manuscript_full_text_proofread": True}, + ) + self.assertNotIn(chant_std_proofread, response.context["chants"]) + self.assertIn(chant_ft_proofread, response.context["chants"]) + self.assertNotIn(chant_volpiano_proofread, response.context["chants"]) + + response = self.client.get( + reverse("browse-chants", args=[source.id]), + {"manuscript_full_text_proofread": False}, + ) + self.assertIn(chant_std_proofread, response.context["chants"]) + self.assertNotIn(chant_ft_proofread, response.context["chants"]) + self.assertIn(chant_volpiano_proofread, response.context["chants"]) + + response = self.client.get( + reverse("browse-chants", args=[source.id]), + {"volpiano_proofread": True}, + ) + self.assertNotIn(chant_std_proofread, response.context["chants"]) + self.assertNotIn(chant_ft_proofread, response.context["chants"]) + self.assertIn(chant_volpiano_proofread, response.context["chants"]) + + response = self.client.get( + reverse("browse-chants", args=[source.id]), + {"volpiano_proofread": False}, + ) + self.assertIn(chant_std_proofread, response.context["chants"]) + self.assertIn(chant_ft_proofread, response.context["chants"]) + self.assertNotIn(chant_volpiano_proofread, response.context["chants"]) + def test_context_source(self): cantus_segment = make_fake_segment(id=4063) source = make_fake_source(segment=cantus_segment) diff --git a/django/cantusdb_project/main_app/views/source.py b/django/cantusdb_project/main_app/views/source.py index c2d79d0c5..ab26959e1 100644 --- a/django/cantusdb_project/main_app/views/source.py +++ b/django/cantusdb_project/main_app/views/source.py @@ -18,7 +18,11 @@ TemplateView, ) -from main_app.forms import SourceCreateForm, SourceEditForm +from main_app.forms import ( + SourceCreateForm, + SourceEditForm, + SourceBrowseChantsProofreadForm, +) from main_app.models import ( Century, Chant, @@ -34,6 +38,7 @@ user_can_edit_source, user_can_view_source, user_can_manage_source_editors, + user_can_proofread_source, ) from main_app.views.chant import ( get_feast_selector_options, @@ -54,6 +59,10 @@ class SourceBrowseChantsView(ListView): ``search_text``: Filters by text of Chant ``genre``: Filters by genre of Chant ``folio``: Filters by folio of Chant + ``manuscript_full_text_proofread``: Filters by chants that have their full text proofread + ``manuscript_full_text_std_proofread``: Filters by chants that have their standardized + spelling full text proofread + ``volpiano_proofread``: Filters by chants that have their volpiano proofread """ model = Chant @@ -84,8 +93,19 @@ def get_queryset(self): folio = self.request.GET.get("folio") search_text = self.request.GET.get("search_text") + # proofread fields filter + manuscript_full_text_proofread = self.request.GET.get( + "manuscript_full_text_proofread" + ) + manuscript_full_text_std_proofread = self.request.GET.get( + "manuscript_full_text_std_proofread" + ) + volpiano_proofread = self.request.GET.get("volpiano_proofread") + # get all chants in the specified source - chants = source.chant_set.select_related("feast", "service", "genre") + chants: QuerySet[Chant] = source.chant_set.select_related( + "feast", "service", "genre" + ) # filter the chants with optional search params if feast_id: chants = chants.filter(feast__id=feast_id) @@ -100,6 +120,18 @@ def get_queryset(self): | Q(incipit__icontains=search_text) | Q(manuscript_full_text__icontains=search_text) ) + # Apply proofreading filters if they are set + if manuscript_full_text_std_proofread: + chants = chants.filter( + manuscript_full_text_std_proofread=manuscript_full_text_std_proofread + ) + if manuscript_full_text_proofread: + chants = chants.filter( + manuscript_full_text_proofread=manuscript_full_text_proofread + ) + if volpiano_proofread: + chants = chants.filter(volpiano_proofread=volpiano_proofread) + return chants.order_by("folio", "c_sequence") def get_context_data(self, **kwargs): @@ -134,6 +166,7 @@ def get_context_data(self, **kwargs): user = self.request.user context["user_can_edit_chant"] = user_can_edit_chants_in_source(user, source) + context["user_can_proofread_source"] = user_can_proofread_source(user, source) chants_in_source: QuerySet[Chant] = source.chant_set if chants_in_source.count() == 0: @@ -165,6 +198,9 @@ def get_context_data(self, **kwargs): # the options for the feast selector on the right, same as the source detail page context["feasts_with_folios"] = get_feast_selector_options(source) + context["proofread_filter_form"] = SourceBrowseChantsProofreadForm( + self.request.GET or None + ) return context diff --git a/django/cantusdb_project/static/js/chant_list.js b/django/cantusdb_project/static/js/chant_list.js index 813cfb4e6..7b9cce564 100644 --- a/django/cantusdb_project/static/js/chant_list.js +++ b/django/cantusdb_project/static/js/chant_list.js @@ -6,6 +6,11 @@ window.addEventListener("load", function () { const genreFilter = document.getElementById("genreFilter"); const folioFilter = document.getElementById("folioFilter"); + // Proofreading filters (radio buttons) + const manuscriptFullTextStdProofread = document.querySelectorAll('input[name="manuscript_full_text_std_proofread"]'); + const manuscriptFullTextProofread = document.querySelectorAll('input[name="manuscript_full_text_proofread"]'); + const volpianoProofread = document.querySelectorAll('input[name="volpiano_proofread"]'); + // Make sure the select components keep their values across multiple GET requests // so the user can "drill down" on what they want const urlParams = new URLSearchParams(window.location.search); @@ -23,6 +28,18 @@ window.addEventListener("load", function () { folioFilter.value = urlParams.get("folio"); } + // Set the initial state of proofreading filters based on URL parameters + if (urlParams.has("manuscript_full_text_std_proofread")) { + document.querySelector(`input[name="manuscript_full_text_std_proofread"][value="${urlParams.get("manuscript_full_text_std_proofread")}"]`).checked = true; + } + if (urlParams.has("manuscript_full_text_proofread")) { + document.querySelector(`input[name="manuscript_full_text_proofread"][value="${urlParams.get("manuscript_full_text_proofread")}"]`).checked = true; + } + if (urlParams.has("volpiano_proofread")) { + document.querySelector(`input[name="volpiano_proofread"][value="${urlParams.get("volpiano_proofread")}"]`).checked = true; + } + + // Event listeners for the select fields and search input searchText.addEventListener("change", setSearch); sourceFilter.addEventListener("change", setSource); feastFilter.addEventListener("change", setFeastLeft); @@ -30,11 +47,23 @@ window.addEventListener("load", function () { genreFilter.addEventListener("change", setGenre); folioFilter.addEventListener("change", setFolio); + // Event listeners for the proofreading radio buttons + manuscriptFullTextStdProofread.forEach(radio => { + radio.addEventListener("change", setProofreadingFilter); + }); + manuscriptFullTextProofread.forEach(radio => { + radio.addEventListener("change", setProofreadingFilter); + }); + volpianoProofread.forEach(radio => { + radio.addEventListener("change", setProofreadingFilter); + }); + // functions for the auto-jump of various selectors and input fields on the page // the folio selector and folio-feast selector on the right half do source-wide filtering // the feast selector, genre selector, and text search on the left half do folio-wide filtering var url = new URL(window.location.href); + // Handle text search change function setSearch() { const searchTerm = searchText.value; url.searchParams.set('search_text', searchTerm); @@ -86,4 +115,25 @@ window.addEventListener("load", function () { url.searchParams.set('folio', folio); window.location.assign(url); } + + // Helper function to update URL parameters + function updateURLParam(name, value) { + if (value === "") { + url.searchParams.delete(name); + } else { + url.searchParams.set(name, value); + } + } + + // Handle proofreading filters (radio buttons) + function setProofreadingFilter() { + const stdProofread = document.querySelector('input[name="manuscript_full_text_std_proofread"]:checked')?.value; + const proofread = document.querySelector('input[name="manuscript_full_text_proofread"]:checked')?.value; + const volpianoProof = document.querySelector('input[name="volpiano_proofread"]:checked')?.value; + + updateURLParam('manuscript_full_text_std_proofread', stdProofread); + updateURLParam('manuscript_full_text_proofread', proofread); + updateURLParam('volpiano_proofread', volpianoProof); + window.location.assign(url); + } });