diff --git a/django/cantusdb_project/cantusdb/urls.py b/django/cantusdb_project/cantusdb/urls.py
index e39f736f6..0179b5e5d 100644
--- a/django/cantusdb_project/cantusdb/urls.py
+++ b/django/cantusdb_project/cantusdb/urls.py
@@ -13,6 +13,7 @@
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
+
from django.contrib import admin
from django.urls import path
from django.urls import include
diff --git a/django/cantusdb_project/main_app/models/base_model.py b/django/cantusdb_project/main_app/models/base_model.py
index dca07ac08..f64b44ff7 100644
--- a/django/cantusdb_project/main_app/models/base_model.py
+++ b/django/cantusdb_project/main_app/models/base_model.py
@@ -1,4 +1,5 @@
"""Defines a BaseModel to be extended by all other models"""
+
from typing import List
from django.db import models
from django.urls import reverse
diff --git a/django/cantusdb_project/main_app/tests/make_fakes.py b/django/cantusdb_project/main_app/tests/make_fakes.py
index 94ada55e4..47177eb57 100644
--- a/django/cantusdb_project/main_app/tests/make_fakes.py
+++ b/django/cantusdb_project/main_app/tests/make_fakes.py
@@ -1,4 +1,5 @@
"""Functions to make fake objects to be used for testing"""
+
import random
from faker import Faker
diff --git a/django/cantusdb_project/main_app/tests/test_views.py b/django/cantusdb_project/main_app/tests/test_views.py
index b4f7f3af8..c4402bd6f 100644
--- a/django/cantusdb_project/main_app/tests/test_views.py
+++ b/django/cantusdb_project/main_app/tests/test_views.py
@@ -2886,7 +2886,7 @@ def test_volpiano_signal(self):
"c_sequence": "1",
# liquescents, to be converted to lowercase
# vv v v v v v v
- "volpiano": "9abcdefg)A-B1C2D3E4F5G67?. yiz"
+ "volpiano": "9abcdefg)A-B1C2D3E4F5G67?. yiz",
# ^ ^ ^ ^ ^ ^ ^^^^^^^^
# clefs, accidentals, etc., to be deleted
},
@@ -2909,6 +2909,52 @@ def test_volpiano_signal(self):
self.assertEqual(chant_2.volpiano, "abacadaeafagahaja")
self.assertEqual(chant_2.volpiano_intervals, "1-12-23-34-45-56-67-78-8")
+ def test_initial_values(self):
+ # create a chant with a known folio, feast, office, c_sequence and image_link
+ source: Source = make_fake_source()
+ folio: str = "001r"
+ sequence: int = 1
+ feast: Feast = make_fake_feast()
+ office: Office = make_fake_office()
+ image_link: str = "https://www.youtube.com/watch?v=9bZkp7q19f0"
+ self.client.post(
+ reverse("chant-create", args=[source.id]),
+ {
+ "manuscript_full_text_std_spelling": "this is a bog standard manuscript textful spelling",
+ "folio": folio,
+ "c_sequence": str(sequence),
+ "feast": feast.id,
+ "office": office.id,
+ "image_link": image_link,
+ },
+ )
+
+ # when we request the Chant Create page, the same folio, feast, office and image_link should
+ # be preselected, and c_sequence should be incremented by 1.
+ response = self.client.get(
+ reverse("chant-create", args=[source.id]),
+ )
+
+ observed_initial_folio: int = response.context["form"].initial["folio"]
+ with self.subTest(subtest="test initial value of folio field"):
+ self.assertEqual(observed_initial_folio, folio)
+
+ observed_initial_feast: int = response.context["form"].initial["feast"]
+ with self.subTest(subtest="test initial value of feast feild"):
+ self.assertEqual(observed_initial_feast, feast.id)
+
+ observed_initial_office: int = response.context["form"].initial["office"]
+ with self.subTest(subtest="test initial value of office field"):
+ self.assertEqual(observed_initial_office, office.id)
+
+ observed_initial_sequence: int = response.context["form"].initial["c_sequence"]
+ with self.subTest(subtest="test initial value of c_sequence field"):
+ self.assertEqual(observed_initial_sequence, sequence + 1)
+
+ observed_initial_image: int = response.context["form"].initial["image_link"]
+ with self.subTest(subtest="test initial value of image_link field"):
+ self.assertEqual(observed_initial_image, image_link)
+
class ChantDeleteViewTest(TestCase):
@classmethod
@@ -3033,7 +3079,7 @@ def test_volpiano_signal(self):
"c_sequence": "1",
# liquescents, to be converted to lowercase
# vv v v v v v v
- "volpiano": "9abcdefg)A-B1C2D3E4F5G67?. yiz"
+ "volpiano": "9abcdefg)A-B1C2D3E4F5G67?. yiz",
# ^ ^ ^ ^ ^ ^ ^^^^^^^^
# clefs, accidentals, etc., to be deleted
},
@@ -4510,7 +4556,9 @@ def test_dd_column(self):
response = self.client.get(reverse("source-inventory", args=[source.id]))
html: str = str(response.content)
self.assertIn(diff_id, html)
- expected_html_substring: str = f''
+ expected_html_substring: str = (
+ f''
+ )
self.assertIn(expected_html_substring, html)
def test_redirect_with_source_parameter(self):
@@ -5180,9 +5228,16 @@ def test_url(self):
def test_content(self):
NUM_CHANTS = 5
- source = make_fake_source(published=True)
+ source_siglum = "SourceSiglum"
+ chant_siglum = "ChantSiglum" # OldCantus chants/sequences had a "siglum"
+ # field, which would sometimes get out of date when the chant's source's siglum
+ # was updated. We keep the chant siglum field around to ensure no data is
+ # inadvertently lost, but we need to ensure it is never displayed publicly.
+ source = make_fake_source(published=True, siglum=source_siglum)
for _ in range(NUM_CHANTS):
- make_fake_chant(source=source)
+ chant = make_fake_chant(source=source)
+ chant.siglum = chant_siglum
+ chant.save()
response = self.client.get(reverse("csv-export", args=[source.id]))
content = response.content.decode("utf-8")
split_content = list(csv.reader(content.splitlines(), delimiter=","))
@@ -5213,11 +5268,21 @@ def test_content(self):
"node_id",
]
for t in expected_column_titles:
- self.assertIn(t, header)
-
- self.assertEqual(len(rows), NUM_CHANTS)
- for row in rows:
- self.assertEqual(len(header), len(row))
+ with self.subTest(expected_column=t):
+ self.assertIn(t, header)
+ with self.subTest(subtest="ensure a row exists for each chant"):
+ self.assertEqual(len(rows), NUM_CHANTS)
+ with self.subTest(
+ subtest="ensure all rows have the same number of columns as the header"
+ ):
+ for row in rows:
+ self.assertEqual(len(header), len(row))
+ with self.subTest(
+ "ensure we only ever display chants' sources' sigla, and never the "
+ "value stored in chants' siglum fields"
+ ):
+ for row in rows:
+ self.assertEqual(row[0], source_siglum)
def test_published_vs_unpublished(self):
published_source = make_fake_source(published=True)
@@ -5243,12 +5308,18 @@ def test_csv_export_on_source_with_sequences(self):
split_content = list(csv.reader(content.splitlines(), delimiter=","))
header, rows = split_content[0], split_content[1:]
- self.assertEqual(len(rows), NUM_SEQUENCES)
- for row in rows:
- self.assertEqual(len(header), len(row))
- self.assertNotEqual(
- row[3], ""
- ) # ensure that the .s_sequence field is being written to the "sequence" column
+ with self.subTest(subtest="ensure a row exists for each sequence"):
+ self.assertEqual(len(rows), NUM_SEQUENCES)
+ with self.subTest(
+ subtest="ensure all rows have the same number of columns as the header"
+ ):
+ for row in rows:
+ self.assertEqual(len(header), len(row))
+ with self.subTest(
+ subtest="ensure .s_sequence field is being written to the 'sequence' column"
+ ):
+ for row in rows:
+ self.assertNotEqual(row[3], "")
class ChangePasswordViewTest(TestCase):
diff --git a/django/cantusdb_project/main_app/urls.py b/django/cantusdb_project/main_app/urls.py
index 7c1ed6039..7661db2f7 100644
--- a/django/cantusdb_project/main_app/urls.py
+++ b/django/cantusdb_project/main_app/urls.py
@@ -391,11 +391,6 @@
views.csv_export_redirect_from_old_path,
name="csv-export-old-path",
),
- path(
- "ajax/concordance/",
- views.ajax_concordance_list,
- name="ajax-concordance",
- ),
# content overview (for project managers)
path(
"content-overview/",
diff --git a/django/cantusdb_project/main_app/views/chant.py b/django/cantusdb_project/main_app/views/chant.py
index bd17143fc..4d7324ea3 100644
--- a/django/cantusdb_project/main_app/views/chant.py
+++ b/django/cantusdb_project/main_app/views/chant.py
@@ -805,6 +805,7 @@ def get_initial(self):
}
latest_folio = latest_chant.folio if latest_chant.folio else "001r"
latest_feast = latest_chant.feast.id if latest_chant.feast else ""
+ latest_office = latest_chant.office.id if latest_chant.office else ""
latest_seq = (
latest_chant.c_sequence if latest_chant.c_sequence is not None else 0
)
@@ -812,6 +813,7 @@ def get_initial(self):
return {
"folio": latest_folio,
"feast": latest_feast,
+ "office": latest_office,
"c_sequence": latest_seq + 1,
"image_link": latest_image,
}
diff --git a/django/cantusdb_project/main_app/views/views.py b/django/cantusdb_project/main_app/views/views.py
index 54a40fbcc..9768551ad 100644
--- a/django/cantusdb_project/main_app/views/views.py
+++ b/django/cantusdb_project/main_app/views/views.py
@@ -69,61 +69,6 @@ def items_count(request):
return render(request, "items_count.html", context)
-def ajax_concordance_list(request, cantus_id):
- """
- Function-based view responding to the AJAX call for concordance list on the chant detail page,
- accessed with ``chants/``, click on "Display concordances of this chant"
-
- Args:
- cantus_id (str): The Cantus ID of the requested concordances group
-
- Returns:
- JsonResponse: A response to the AJAX call, to be unpacked by the frontend js code
- """
- chants = Chant.objects.filter(cantus_id=cantus_id)
- seqs = Sequence.objects.filter(cantus_id=cantus_id)
-
- display_unpublished = request.user.is_authenticated
- if not display_unpublished:
- chants = chants.filter(source__published=True)
- seqs = seqs.filter(source__published=True)
-
- if seqs:
- chants = chants.union(seqs).order_by("siglum", "folio")
- else:
- chants = chants.order_by("siglum", "folio")
- # queryset(list of dictionaries)
- concordance_values = chants.values(
- "siglum",
- "folio",
- "incipit",
- "office__name",
- "genre__name",
- "position",
- "feast__name",
- "mode",
- "image_link",
- )
-
- concordances = list(concordance_values)
- for i, concordance in enumerate(concordances):
- # some chants do not have a source
- # for those chants, do not return source link
- if chants[i].source:
- concordance["source_link"] = chants[i].source.get_absolute_url()
- if chants[i].search_vector:
- concordance["chant_link"] = chants[i].get_absolute_url()
- else:
- concordance["chant_link"] = reverse("sequence-detail", args=[chants[i].id])
- concordance["db"] = "CD"
-
- concordance_count = len(concordances)
- return JsonResponse(
- {"concordances": concordances, "concordance_count": concordance_count},
- safe=True,
- )
-
-
def ajax_melody_list(request, cantus_id) -> JsonResponse:
"""
Function-based view responding to the AJAX call for melody list on the chant detail page,
@@ -235,6 +180,7 @@ def csv_export(request, source_id):
"node_id",
]
)
+ siglum = source.siglum
for entry in entries:
feast = entry.feast.name if entry.feast else ""
office = entry.office.name if entry.office else ""
@@ -243,7 +189,7 @@ def csv_export(request, source_id):
writer.writerow(
[
- entry.siglum,
+ siglum,
entry.marginalia,
entry.folio,
# if entry has a c_sequence, it's a Chant. If it doesn't, it's a Sequence, so write its s_sequence
diff --git a/django/cantusdb_project/requirements.txt b/django/cantusdb_project/requirements.txt
index 889947a2c..270ffceac 100644
--- a/django/cantusdb_project/requirements.txt
+++ b/django/cantusdb_project/requirements.txt
@@ -3,11 +3,11 @@ asgiref==3.6.0
astroid==2.4.2
attrs==19.3.0
backports.zoneinfo==0.2.1
-black==21.10b0
+black==24.3.0
certifi==2023.7.22
chardet==3.0.4
charset-normalizer==2.0.12
-click==7.1.2
+click==8.0.0
coverage==5.3.1
Django==4.2.11
django-autocomplete-light==3.9.4
@@ -36,7 +36,7 @@ sqlparse==0.4.4
text-unidecode==1.3
toml==0.10.1
typed-ast==1.4.1
-typing-extensions==3.10.0.0
+typing-extensions==4.0.1
ujson==5.9.0
urllib3==1.26.18
volpiano-display-utilities @ git+https://github.com/DDMAL/volpiano-display-utilities.git@v1.1.2
diff --git a/scripts/parse_link_checker_output.py b/scripts/parse_link_checker_output.py
index 172f84684..178d91109 100644
--- a/scripts/parse_link_checker_output.py
+++ b/scripts/parse_link_checker_output.py
@@ -1,4 +1,5 @@
"""Modules"""
+
import json
import sys
from pathlib import Path