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 %}
+
+ {% 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);
+ }
});