diff --git a/app/Dockerfile b/app/Dockerfile index acf5aa00..635c922c 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 # Download and install python dependencies in a container -FROM python:3.12.3 as dependency-install-container +FROM python:3.12.3 AS dependency-install-container ARG DEVELOPMENT COPY ./poetry.lock ./pyproject.toml ./app/install-packages.sh /code/ WORKDIR /code diff --git a/app/public/cantusdata/helpers/expandr.py b/app/public/cantusdata/helpers/expandr.py index 64e21fd8..9546eb16 100644 --- a/app/public/cantusdata/helpers/expandr.py +++ b/app/public/cantusdata/helpers/expandr.py @@ -1,113 +1,144 @@ -from cantusdata.settings import BASE_DIR -from cantusdata.helpers.scrapers.genre import genres - -import csv -import urllib.request, urllib.error, urllib.parse -import re +import json import os +import requests + +from django.conf import settings -def expand_mode(mode_code): - input_list = mode_code.strip() +def expand_mode(mode_code: str) -> str: + """ + Translate non-numeric components of a CantusDB mode code into human-readable form. + + :param mode_code str: A CantusDB mode code + :return: A human-readable translation of the mode code + """ + mode_code_stripped = mode_code.strip() mode_output = [] - if "1" in input_list: - mode_output.append("1") - if "2" in input_list: - mode_output.append("2") - if "3" in input_list: - mode_output.append("3") - if "4" in input_list: - mode_output.append("4") - if "5" in input_list: - mode_output.append("5") - if "6" in input_list: - mode_output.append("6") - if "7" in input_list: - mode_output.append("7") - if "8" in input_list: - mode_output.append("8") - if "*" in input_list: - mode_output.append("No music") - if "r" in input_list: - mode_output.append("Formulaic") - if "?" in input_list: - mode_output.append("Uncertain") - if "S" in input_list: - mode_output.append("Responsory (special)") - if "T" in input_list: - mode_output.append("Chant in Transposition") + mode_nums = {"1", "2", "3", "4", "5", "6", "7", "8"} + for char in mode_code_stripped: + if char in mode_nums: + mode_output.append(char) + continue + match char: + case "*": + mode_output.append("No music") + case "r": + mode_output.append("Formulaic") + case "?": + mode_output.append("Uncertain") + case "S": + mode_output.append("Responsory (special)") + case "T": + mode_output.append("Chant in Transposition") outstring = " ".join(mode_output) return outstring -def expand_genre(genre_code): - if genre_code in genres: - description = genres[genre_code] +class GenreExpander: + """ + Loads the genre mapping from the CantusDB API and provides a method to retrieve + the full text genre description based on the given genre code. + """ + + cantus_db_api_endpoint = "https://cantusdatabase.org/genres" + request_headers = {"Accept": "application/json"} + + def __init__(self) -> None: + self.genre_data = self.load_genre_data() + + def load_genre_data(self) -> dict[str, str]: + """ + Loads the genre list from the CantusDB API and returns a dictionary mapping + genre codes to genre descriptions. + """ + response = requests.get( + self.cantus_db_api_endpoint, headers=self.request_headers, timeout=5 + ) + response.raise_for_status() + genre_map: dict[str, str] = { + x["name"]: x["description"] for x in response.json()["genres"] + } + return genre_map + + def expand_genre(self, genre_code: str) -> str: + """ + Gets the genre description based on the genre code. + """ + if not genre_code in self.genre_data: + return genre_code + + description = self.genre_data[genre_code] # some extra stuff in parentheses is showing up paren = description.find("(") return description[: paren - 1] if paren != -1 else description - # If nothing was found, return the original - return genre_code - -def expand_differentia(differentia_code): +def expand_differentia(differentia_code: str) -> str: """ In most cases, the differentia remains unmodified - :param differentia_code: - :return: + :param differentia_code: The differentia. + :return str: "No differentia" if no differentia is present, otherwise the differentia. """ return "No differentia" if "*" in differentia_code else differentia_code -def expand_office(office_code): - return { - "V": "First Vespers", - "C": "Compline", - "M": "Matins", - "L": "Lauds", - "P": "Prime", - "T": "Terce", - "S": "Sext", - "N": "None", - "V2": "Second Vespers", - "MI": "Mass", - "MI1": "First Mass", - "MI2": "Second Mass", - "MI3": "Third Mass", - "D": "Day Hours", - "R": "Memorial", - "E": "Antiphons for the Magnificat or Benedictus", - "H": "Antiphons based on texts from the Historia", - "CA": "Chapter", - "X": "Supplementary", - }.get(office_code, "Error") - - -class PositionExpander(object): - position_data_base = None - - def __init__(self): - self.csv_file = csv.DictReader( - open(os.path.join(BASE_DIR, "data_dumps", "position_names.csv")) - ) - self.position_data_base = dict() - for row in self.csv_file: - office_code = self.remove_double_dash(row["Office"]).strip() - genre_code = self.remove_double_dash(row["Genre"]).strip() - position_code = ( - self.remove_double_dash(row["Position"]) - .strip() - .lstrip("0") - .rstrip("._ ") - ) - text = self.remove_double_dash(row["Text Phrase"]).strip() - - # We are creating a 3-dimensional dictionary for fast lookup of names - self.add_text(office_code, genre_code, position_code, text) - - def get_text(self, office_code, genre_code, position_code): +OFFICE_CODES = { + "V": "First Vespers", + "C": "Compline", + "M": "Matins", + "L": "Lauds", + "P": "Prime", + "T": "Terce", + "S": "Sext", + "N": "None", + "V2": "Second Vespers", + "MI": "Mass", + "MI1": "First Mass", + "MI2": "Second Mass", + "MI3": "Third Mass", + "D": "Day Hours", + "R": "Memorial", + "E": "Antiphons for the Magnificat or Benedictus", + "H": "Antiphons based on texts from the Historia", + "CA": "Chapter", + "X": "Supplementary", +} + + +def expand_office(office_code: str) -> str: + """ + Returns the full name of the office based on the given office code. + + :param office_code: The office code. + :return: The full name of the office. + """ + return OFFICE_CODES.get(office_code, "Error") + + +class PositionExpander: + """ + Loads the position mapping data from a JSON file and provides a method to retrieve + the full text position description based on the given office, genre, and position code. + """ + + def __init__(self) -> None: + with open( + os.path.join( + settings.BASE_DIR, "cantusdata", "helpers", "position_mapping.json" + ), + "r", + encoding="utf-8", + ) as f: + self.position_data_base: dict[str, dict[str, dict[str, str]]] = json.load(f) + + def expand_position( + self, office_code: str, genre_code: str, position_code: str + ) -> str: + """ + Retrieves the full text position description based on the given office, genre, + and position code. + """ try: return self.position_data_base[office_code.strip()][genre_code.strip()][ position_code.strip().lstrip("0").rstrip("._ ") @@ -115,38 +146,3 @@ def get_text(self, office_code, genre_code, position_code): except KeyError: # If it's not in the dictionary then we just use an empty string return "" - - def add_text(self, office, genre, position, text): - """ - Add a record to self.position_data_base, which is a 3d dictionary. - Raises KeyError if a dictionary position is already taken. - """ - if office in self.position_data_base: - if genre in self.position_data_base[office]: - if position in self.position_data_base[office][genre]: - raise KeyError( - "Position record {0} {1} {2} already set to {3}!".format( - office, - genre, - position, - self.position_data_base[office][genre][position], - ) - ) - else: - # Position doesn't exist, so we create it - self.position_data_base[office][genre].update({position: text}) - else: - # Genre doesn't exist, so we create it and position - self.position_data_base[office].update({genre: {position: text}}) - else: - # Office doesn't exist, so we create office, genre, and position - self.position_data_base.update({office: {genre: {position: text}}}) - - def remove_double_dash(self, text): - """ - Turns double dashes into empty strings - """ - if text.strip() == "--": - return "" - else: - return text diff --git a/app/public/cantusdata/helpers/position_mapping.json b/app/public/cantusdata/helpers/position_mapping.json new file mode 100644 index 00000000..81f72884 --- /dev/null +++ b/app/public/cantusdata/helpers/position_mapping.json @@ -0,0 +1,335 @@ +{ + "C": { + "R": { + "": "" + }, + "V": { + "1": "Verse 1", + "2": "Verse 2" + }, + "W": { + "": "" + } + }, + "D": { + "A": { + "1": "1st Antiphon", + "2": "2nd Antiphon", + "3": "3rd Antiphon", + "4": "4th Antiphon", + "": "" + }, + "R": { + "1": "1st Responsory", + "2": "2nd Responsory", + "3": "3rd Responsory", + "4": "4th Responsory" + }, + "V": { + "1": "Verse 1" + } + }, + "E": { + "A": { + "1": "1st Antiphon for the Magnificat or Benedictus", + "2": "2nd Antiphon for the Magnificat or Benedictus", + "3": "3rd Antiphon for the Magnificat or Benedictus", + "4": "4th Antiphon for the Magnificat or Benedictus", + "5": "5th Antiphon for the Magnificat or Benedictus", + "6": "6th Antiphon for the Magnificat or Benedictus", + "7": "7th Antiphon for the Magnificat or Benedictus", + "8": "8th Antiphon for the Magnificat or Benedictus", + "9": "9th Antiphon for the Magnificat or Benedictus", + "10": "10th Antiphon for the Magnificat or Benedictus", + "11": "11th Antiphon for the Magnificat or Benedictus", + "12": "12th Antiphon for the Magnificat or Benedictus", + "13": "13th Antiphon for the Magnificat or Benedictus", + "14": "14th Antiphon for the Magnificat or Benedictus", + "15": "15th Antiphon for the Magnificat or Benedictus", + "": "" + }, + "AV": { + "1": "Antiphon Verse 1", + "2": "Antiphon Verse 2", + "3": "Antiphon Verse 3" + } + }, + "L": { + "A": { + "1": "Antiphon for the 1st Psalm", + "2": "Antiphon for the 2nd Psalm", + "3": "Antiphon for the 3rd Psalm", + "4": "Antiphon for the 4th Psalm", + "5": "Antiphon for the 5th Psalm", + "6": "Antiphon for the 6th Psalm", + "7": "Antiphon for the 7th Psalm", + "": "", + "1B": "1st Antiphon for the Benedictus", + "2B": "2nd Antiphon for the Benedictus", + "B": "Antiphon for the Benedictus", + "R": "Antiphon sung as a memorial" + }, + "R": { + "": "" + }, + "V": { + "1": "Verse 1", + "2": "Verse 2" + }, + "W": { + "": "" + } + }, + "M": { + "A": { + "1.1": "Antiphon for Nocturn 1, Psalm 1", + "1.2": "Antiphon for Nocturn 1, Psalm 2", + "1.3": "Antiphon for Nocturn 1, Psalm 3", + "1.4": "Antiphon for Nocturn 1, Psalm 4", + "1.5": "Antiphon for Nocturn 1, Psalm 5", + "1.6": "Antiphon for Nocturn 1, Psalm 6", + "2.1": "Antiphon for Nocturn 2, Psalm 1", + "2.2": "Antiphon for Nocturn 2, Psalm 2", + "2.3": "Antiphon for Nocturn 2, Psalm 3", + "2.4": "Antiphon for Nocturn 2, Psalm 4", + "2.5": "Antiphon for Nocturn 2, Psalm 5", + "2.6": "Antiphon for Nocturn 2, Psalm 6", + "3": "Antiphon for all Psalms of Nocturn 3", + "3.1": "Antiphon for Nocturn 3, Psalm 1", + "3.2": "Antiphon for Nocturn 3, Psalm 2", + "3.3": "Antiphon for Nocturn 3, Psalm 3" + }, + "I": { + "": "" + }, + "M": { + "": "" + }, + "R": { + "1": "1 for all Lessons of Nocturn 1", + "1.1": "Responsory for Nocturn 1, Lesson 1", + "1.2": "Responsory for Nocturn 1, Lesson 2", + "1.3": "Responsory for Nocturn 1, Lesson 3", + "1.4": "Responsory for Nocturn 1, Lesson 4", + "2": "2 for all Lessons of Nocturn 2", + "2.1": "Responsory for Nocturn 2, Lesson 1", + "2.2": "Responsory for Nocturn 2, Lesson 2", + "2.3": "Responsory for Nocturn 2, Lesson 3", + "2.4": "Responsory for Nocturn 2, Lesson 4", + "3": "3 for all Lessons of Nocturn 3", + "3.1": "Responsory for Nocturn 3, Lesson 1", + "3.2": "Responsory for Nocturn 3, Lesson 2", + "3.3": "Responsory for Nocturn 3, Lesson 3", + "3.4": "Responsory for Nocturn 3, Lesson 4", + "3.5": "Responsory for Nocturn 3, Lesson 5", + "4": "4 for all Lessons of Nocturn 4", + "5": "5 for all Lessons of Nocturn 5", + "6": "6 for all Lessons of Nocturn 6", + "7": "7 for all Lessons of Nocturn 7", + "8": "8 for all Lessons of Nocturn 8", + "9": "9 for all Lessons of Nocturn 9", + "10": "10 for all Lessons of Nocturn 10", + "11": "11 for all Lessons of Nocturn 11", + "12": "12 for all Lessons of Nocturn 12", + "13": "13 for all Lessons of Nocturn 13", + "14": "14 for all Lessons of Nocturn 14", + "15": "15 for all Lessons of Nocturn 15", + "16": "16 for all Lessons of Nocturn 16", + "17": "17 for all Lessons of Nocturn 17", + "18": "18 for all Lessons of Nocturn 18", + "": "" + }, + "V": { + "1": "Verse 1", + "2": "Verse 2", + "3": "Verse 3" + }, + "W": { + "1": "Antiphon for all Psalms of Nocturn 1", + "2": "Antiphon for all Psalms of Nocturn 2", + "3": "Antiphon for all Psalms of Nocturn 3" + } + }, + "N": { + "A": { + "": "" + }, + "R": { + "": "" + }, + "V": { + "1": "Verse 1" + }, + "W": { + "": "" + } + }, + "P": { + "A": { + "": "" + }, + "R": { + "": "" + }, + "V": { + "1": "Verse 1" + }, + "W": { + "": "" + } + }, + "S": { + "A": { + "": "" + }, + "R": { + "": "" + }, + "V": { + "1": "Verse 1" + }, + "W": { + "": "" + } + }, + "T": { + "A": { + "": "" + }, + "R": { + "": "" + }, + "V": { + "1": "Verse 1" + }, + "W": { + "": "" + } + }, + "V": { + "A": { + "1": "Antiphon for the 1st Psalm", + "2": "Antiphon for the 2nd Psalm", + "3": "Antiphon for the 3rd Psalm", + "4": "Antiphon for the 4th Psalm", + "5": "Antiphon for the 5th Psalm", + "6": "Antiphon for the 6th Psalm", + "7": "Antiphon for the 7th Psalm", + "8": "Antiphon for the 8th Psalm", + "9": "Antiphon for the 9th Psalm", + "10": "Antiphon for the 10th Psalm", + "11": "Antiphon for the 11th Psalm", + "12": "Antiphon for the 12th Psalm", + "13": "Antiphon for the 13th Psalm", + "14": "Antiphon for the 14th Psalm", + "15": "Antiphon for the 15th Psalm", + "16": "Antiphon for the 16th Psalm", + "": "", + "10M": "10th Antiphon for the Magnificat", + "11M": "11th Antiphon for the Magnificat", + "12M": "12th Antiphon for the Magnificat", + "1M": "1st Antiphon for the Magnificat", + "2M": "2nd Antiphon for the Magnificat", + "3M": "3rd Antiphon for the Magnificat", + "4M": "4th Antiphon for the Magnificat", + "5M": "5th Antiphon for the Magnificat", + "6M": "6th Antiphon for the Magnificat", + "7M": "7th Antiphon for the Magnificat", + "8M": "8th Antiphon for the Magnificat", + "9M": "9th Antiphon for the Magnificat", + "M": "Antiphon for the Magnificat", + "R": "Antiphon sung as a memorial" + }, + "R": { + "": "" + }, + "V": { + "1": "Verse 1", + "2": "Verse 2" + } + }, + "V2": { + "A": { + "1": "Antiphon for the 1st Psalm", + "2": "Antiphon for the 2nd Psalm", + "3": "Antiphon for the 3rd Psalm", + "4": "Antiphon for the 4th Psalm", + "5": "Antiphon for the 5th Psalm", + "6": "Antiphon for the 6th Psalm", + "7": "Antiphon for the 7th Psalm", + "8": "Antiphon for the 8th Psalm", + "9": "Antiphon for the 9th Psalm", + "10": "Antiphon for the 10th Psalm", + "": "", + "1M": "1st Antiphon for the Magnificat", + "2M": "2nd Antiphon for the Magnificat", + "3M": "3rd Antiphon for the Magnificat", + "4M": "4th Antiphon for the Magnificat", + "M": "Antiphon for the Magnificat", + "R": "Antiphon sung as a memorial" + }, + "AV": { + "1": "Antiphon for the 1st Psalm", + "2": "Antiphon for the 2nd Psalm", + "3": "Antiphon for the 3rd Psalm", + "4": "Antiphon for the 4th Psalm" + }, + "R": { + "": "" + }, + "V": { + "1": "Verse 1" + }, + "W": { + "": "" + } + }, + "X": { + "A": { + "1": "1st Antiphon", + "2": "2nd Antiphon", + "3": "3rd Antiphon", + "4": "4th Antiphon", + "5": "5th Antiphon", + "6": "6th Antiphon", + "7": "7th Antiphon", + "8": "8th Antiphon", + "9": "9th Antiphon", + "10": "10th Antiphon", + "11": "11th Antiphon", + "12": "12th Antiphon", + "13": "13th Antiphon", + "14": "14th Antiphon", + "": "", + "P": "Antiphon for All Psalms" + }, + "AV": { + "1": "1st Antiphon Verse" + }, + "M": { + "1": "1st Miscellaneous Chant", + "2": "2nd Miscellaneous Chant", + "3": "3rd Miscellaneous Chant", + "4": "4th Miscellaneous Chant", + "5": "5th Miscellaneous Chant", + "6": "6th Miscellaneous Chant", + "": "" + }, + "R": { + "1": "1st Responsory", + "2": "2nd Responsory", + "3": "3rd Responsory", + "4": "4th Responsory", + "5": "5th Responsory", + "": "", + "P": "Responsory for All Psalms" + }, + "V": { + "1": "Verse 1", + "2": "Verse 2" + }, + "W": { + "": "" + } + } +} \ No newline at end of file diff --git a/app/public/cantusdata/helpers/scrapers/genre.py b/app/public/cantusdata/helpers/scrapers/genre.py deleted file mode 100644 index 58ef2926..00000000 --- a/app/public/cantusdata/helpers/scrapers/genre.py +++ /dev/null @@ -1,66 +0,0 @@ -from html.parser import HTMLParser -import urllib.request -import sys - - -class GenreScraper(HTMLParser): - """Get the genres and description from the Cantus Database. - - Notes - ----- - If you know a better way of doing this, please replace this code. - It is not ideal but I had no success using the Cantus Web API (napulen). - """ - - def __init__(self): - super(GenreScraper, self).__init__() - self.flush() - - def flush(self): - self.is_field_name = False - self.is_field_description = False - self.row_cell_count = 0 - self.key = "" - self.value = "" - self.dictionary = {} - - def retrieve_genres(self): - ret = self.dictionary.copy() - self.flush() - return ret - - def handle_starttag(self, tag, attrs): - if tag == "tr": - self.row_cell_count = 0 - if tag == "td": - if self.row_cell_count == 0: - self.is_field_name = True - elif self.row_cell_count == 1: - self.is_field_description = True - - def handle_endtag(self, tag): - if tag == "td": - if self.is_field_name: - self.is_field_name = False - if self.is_field_description: - self.is_field_description = False - if self.key and self.value: - self.dictionary[self.key.strip("[]")] = self.value - self.key = "" - self.value = "" - self.row_cell_count += 1 - - def handle_data(self, data): - if self.is_field_name: - if not data.isspace(): - self.key = data - if self.is_field_description: - if not data.isspace(): - self.value = data.strip() - - -genre_url = "https://cantusdatabase.org/genres" -contents = urllib.request.urlopen(genre_url).read().decode("utf-8") -parser = GenreScraper() -parser.feed(contents) -genres = parser.retrieve_genres() diff --git a/app/public/cantusdata/test/core/helpers/test_expandr.py b/app/public/cantusdata/test/core/helpers/test_expandr.py index 1ffcaeaf..4051d668 100644 --- a/app/public/cantusdata/test/core/helpers/test_expandr.py +++ b/app/public/cantusdata/test/core/helpers/test_expandr.py @@ -1,12 +1,18 @@ -from django.test import TestCase -from cantusdata.helpers import expandr from itertools import combinations +from django.test import TestCase +from cantusdata.helpers.expandr import ( + expand_mode, + expand_differentia, + expand_office, + GenreExpander, + PositionExpander, +) class ExpandrFunctionsTestCase(TestCase): - def test_expand_mode(self): + def test_expand_mode(self) -> None: # Number and symbol ordering is important - numbers = [1, 2, 3, 4, 5, 6, 7, 8] + numbers = ["1", "2", "3", "4", "5", "6", "7", "8"] symbol_keys = ["*", "r", "?", "S", "T"] symbols = { "*": "No music", @@ -33,79 +39,65 @@ def test_expand_mode(self): for c in list(map(str, combination)): combination_string += c + " " # Test it - self.assertEqual(expandr.expand_mode(combination_string), expected_output) + self.assertEqual(expand_mode(combination_string), expected_output) - def test_expand_genre(self): - self.assertEqual(expandr.expand_genre("A"), "Antiphon") - self.assertEqual(expandr.expand_genre("AV"), "Antiphon verse") - self.assertEqual(expandr.expand_genre("R"), "Responsory") - self.assertEqual(expandr.expand_genre("V"), "Responsory verse") - self.assertEqual(expandr.expand_genre("W"), "Versicle") - self.assertEqual(expandr.expand_genre("H"), "Hymn") - self.assertEqual(expandr.expand_genre("I"), "Invitatory antiphon") - self.assertEqual(expandr.expand_genre("Pr"), "Prefatio") - self.assertEqual(expandr.expand_genre("IP"), "Invitatory psalm") - self.assertEqual(expandr.expand_genre("M"), '"Miscellaneous"') - self.assertEqual(expandr.expand_genre("G"), "Mass chant") + def test_expand_genre(self) -> None: + genre_expander = GenreExpander() + self.assertEqual(genre_expander.expand_genre("A"), "Antiphon") + self.assertEqual(genre_expander.expand_genre("AV"), "Antiphon verse") + self.assertEqual(genre_expander.expand_genre("R"), "Responsory") + self.assertEqual(genre_expander.expand_genre("V"), "Responsory verse") + self.assertEqual(genre_expander.expand_genre("W"), "Versicle") + self.assertEqual(genre_expander.expand_genre("H"), "Hymn") + self.assertEqual(genre_expander.expand_genre("I"), "Invitatory antiphon") + self.assertEqual(genre_expander.expand_genre("Pr"), "Prefatio") + self.assertEqual(genre_expander.expand_genre("IP"), "Invitatory psalm") + self.assertEqual(genre_expander.expand_genre("[M]"), '"Miscellaneous"') + self.assertEqual(genre_expander.expand_genre("[G]"), "Mass chant") self.assertEqual( - expandr.expand_genre("?"), "Unknown, ambiguous, unidentifiable, illegible" + genre_expander.expand_genre("[?]"), + "Unknown, ambiguous, unidentifiable, illegible", ) - self.assertEqual(expandr.expand_genre("Z"), "Z") + self.assertEqual(genre_expander.expand_genre("Z"), "Z") - def test_expand_differentia(self): - self.assertEqual(expandr.expand_differentia("*"), "No differentia") + def test_expand_differentia(self) -> None: + self.assertEqual(expand_differentia("*"), "No differentia") # Test that whitespace is stripped - self.assertEqual(expandr.expand_differentia(" * "), "No differentia") + self.assertEqual(expand_differentia(" * "), "No differentia") # Test all other cases - self.assertEqual(expandr.expand_differentia("Normal string"), "Normal string") + self.assertEqual(expand_differentia("Normal string"), "Normal string") - def test_expand_office(self): - self.assertEqual(expandr.expand_office("V"), "First Vespers") - self.assertEqual(expandr.expand_office("C"), "Compline") - self.assertEqual(expandr.expand_office("M"), "Matins") - self.assertEqual(expandr.expand_office("L"), "Lauds") - self.assertEqual(expandr.expand_office("P"), "Prime") - self.assertEqual(expandr.expand_office("T"), "Terce") - self.assertEqual(expandr.expand_office("S"), "Sext") - self.assertEqual(expandr.expand_office("N"), "None") - self.assertEqual(expandr.expand_office("V2"), "Second Vespers") - self.assertEqual(expandr.expand_office("D"), "Day Hours") - self.assertEqual(expandr.expand_office("R"), "Memorial") + def test_expand_office(self) -> None: + self.assertEqual(expand_office("V"), "First Vespers") + self.assertEqual(expand_office("C"), "Compline") + self.assertEqual(expand_office("M"), "Matins") + self.assertEqual(expand_office("L"), "Lauds") + self.assertEqual(expand_office("P"), "Prime") + self.assertEqual(expand_office("T"), "Terce") + self.assertEqual(expand_office("S"), "Sext") + self.assertEqual(expand_office("N"), "None") + self.assertEqual(expand_office("V2"), "Second Vespers") + self.assertEqual(expand_office("D"), "Day Hours") + self.assertEqual(expand_office("R"), "Memorial") self.assertEqual( - expandr.expand_office("E"), "Antiphons for the Magnificat or Benedictus" + expand_office("E"), "Antiphons for the Magnificat or Benedictus" ) self.assertEqual( - expandr.expand_office("H"), "Antiphons based on texts from the Historia" + expand_office("H"), "Antiphons based on texts from the Historia" ) - self.assertEqual(expandr.expand_office("CA"), "Chapter") - self.assertEqual(expandr.expand_office("X"), "Supplementary") - self.assertEqual(expandr.expand_office("125125"), "Error") + self.assertEqual(expand_office("CA"), "Chapter") + self.assertEqual(expand_office("X"), "Supplementary") + self.assertEqual(expand_office("125125"), "Error") class PositionExpanderTestCase(TestCase): - position_expander = None + def setUp(self) -> None: + self.position_expander = PositionExpander() - def setUp(self): - self.position_expander = expandr.PositionExpander() - - def test_get_text(self): - output = self.position_expander.get_text("M", "A", "3. ") + def test_get_text(self) -> None: + output = self.position_expander.expand_position("M", "A", "3. ") self.assertEqual("Antiphon for all Psalms of Nocturn 3", output) - def test_get_nonexistant_text(self): - output = self.position_expander.get_text("Z", "Z", "Z") + def test_get_nonexistant_text(self) -> None: + output = self.position_expander.expand_position("Z", "Z", "Z") self.assertEqual("", output) - - def test_add_text(self): - new_text = "This is some text!" - self.assertEqual("", self.position_expander.get_text("Z", "X", "Y")) - self.position_expander.add_text("Z", "X", "Y", "This is the new text.") - self.assertEqual( - "This is the new text.", self.position_expander.get_text("Z", "X", "Y") - ) - # We should get an exception if we try to add text to the same place - with self.assertRaises(KeyError): - self.position_expander.add_text("Z", "X", "Y", "This should not work.") - - def tearDown(self): - self.position_expander = None diff --git a/app/public/cantusdata/test/core/management/commands/test_import_all_data.py b/app/public/cantusdata/test/core/management/commands/test_import_all_data.py deleted file mode 100644 index 417176d9..00000000 --- a/app/public/cantusdata/test/core/management/commands/test_import_all_data.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.core.management import call_command -from django.test import TestCase -from cantusdata.models import Manuscript, Chant - - -class ImportAllDataTestCase(TestCase): - def setUp(self): - pass - - def test_import_all_data(self): - call_command("import_data", "manuscripts") - - def tearDown(self): - Chant.objects.all().delete() - Manuscript.objects.all().delete() diff --git a/app/public/data_dumps/position_names.csv b/app/public/data_dumps/position_names.csv deleted file mode 100644 index 443505e4..00000000 --- a/app/public/data_dumps/position_names.csv +++ /dev/null @@ -1,214 +0,0 @@ -Office,Genre,Position,Text Phrase -C,R,--,-- -C,V,1,Verse 1 -C,V,2,Verse 2 -C,W,--,-- -D,A,1,1st Antiphon -D,A,2,2nd Antiphon -D,A,3,3rd Antiphon -D,A,4,4th Antiphon -D,A,--,-- -D,R,1,1st Responsory -D,R,2,2nd Responsory -D,R,3,3rd Responsory -D,R,4,4th Responsory -D,V,1,Verse 1 -E,A,1,1st Antiphon for the Magnificat or Benidictus -E,A,2,2nd Antiphon for the Magnificat or Benidictus -E,A,3,3rd Antiphon for the Magnificat or Benidictus -E,A,4,4th Antiphon for the Magnificat or Benidictus -E,A,5,5th Antiphon for the Magnificat or Benidictus -E,A,6,6th Antiphon for the Magnificat or Benidictus -E,A,7,7th Antiphon for the Magnificat or Benidictus -E,A,8,8th Antiphon for the Magnificat or Benidictus -E,A,9,9th Antiphon for the Magnificat or Benidictus -E,A,10,10th Antiphon for the Magnificat or Benidictus -E,A,11,11th Antiphon for the Magnificat or Benidictus -E,A,12,12th Antiphon for the Magnificat or Benidictus -E,A,13,13th Antiphon for the Magnificat or Benidictus -E,A,14,14th Antiphon for the Magnificat or Benidictus -E,A,15,15th Antiphon for the Magnificat or Benidictus -E,A,--,-- -E,AV,1,Antiphon Verse 1 -E,AV,2,Antiphon Verse 2 -E,AV,3,Antiphon Verse 3 -L,A,1,Antiphon for the 1st Psalm -L,A,2,Antiphon for the 2nd Psalm -L,A,3,Antiphon for the 3rd Psalm -L,A,4,Antiphon for the 4th Psalm -L,A,5,Antiphon for the 5th Psalm -L,A,6,Antiphon for the 6th Psalm -L,A,7,Antiphon for the 7th Psalm -L,A,--,-- -L,A,1B,1st Antiphon for the Benedictus -L,A,2B,2nd Antiphon for the Benedictus -L,A,B,Antiphon for the Benedictus -L,A,R,Antiphon sung as a memorial -L,R,--,-- -L,V,1,Verse 1 -L,V,2,Verse 2 -L,W,--,-- -M,A,1.1,"Antiphon for Nocturn 1, Psalm 1 " -M,A,1.2,"Antiphon for Nocturn 1, Psalm 2 " -M,A,1.3,"Antiphon for Nocturn 1, Psalm 3 " -M,A,1.4,"Antiphon for Nocturn 1, Psalm 4 " -M,A,1.5,"Antiphon for Nocturn 1, Psalm 5 " -M,A,1.6,"Antiphon for Nocturn 1, Psalm 6 " -M,A,2.1,"Antiphon for Nocturn 2, Psalm 1 " -M,A,2.2,"Antiphon for Nocturn 2, Psalm 2 " -M,A,2.3,"Antiphon for Nocturn 2, Psalm 3 " -M,A,2.4,"Antiphon for Nocturn 2, Psalm 4 " -M,A,2.5,"Antiphon for Nocturn 2, Psalm 5 " -M,A,2.6,"Antiphon for Nocturn 2, Psalm 6 " -M,A,3,Antiphon for all Psalms of Nocturn 3 -M,A,3.1,"Antiphon for Nocturn 3, Psalm 1 " -M,A,3.2,"Antiphon for Nocturn 3, Psalm 2 " -M,A,3.3,"Antiphon for Nocturn 3, Psalm 3 " -M,I,--,-- -M,M,--,-- -M,R,1,1 for all Lessons of Nocturn 1 -M,R,1.1,"Responsory for Nocturn 1, Lesson 1 " -M,R,1.2,"Responsory for Nocturn 1, Lesson 2 " -M,R,1.3,"Responsory for Nocturn 1, Lesson 3 " -M,R,1.4,"Responsory for Nocturn 1, Lesson 4 " -M,R,2,2 for all Lessons of Nocturn 2 -M,R,2.1,"Responsory for Nocturn 2, Lesson 1 " -M,R,2.2,"Responsory for Nocturn 2, Lesson 2 " -M,R,2.3,"Responsory for Nocturn 2, Lesson 3 " -M,R,2.4,"Responsory for Nocturn 2, Lesson 4 " -M,R,3,3 for all Lessons of Nocturn 3 -M,R,3.1,"Responsory for Nocturn 3, Lesson 1 " -M,R,3.2,"Responsory for Nocturn 3, Lesson 2 " -M,R,3.3,"Responsory for Nocturn 3, Lesson 3 " -M,R,3.4,"Responsory for Nocturn 3, Lesson 4 " -M,R,3.5,"Responsory for Nocturn 3, Lesson 5 " -M,R,4,4 for all Lessons of Nocturn 4 -M,R,5,5 for all Lessons of Nocturn 5 -M,R,6,6 for all Lessons of Nocturn 6 -M,R,7,7 for all Lessons of Nocturn 7 -M,R,8,8 for all Lessons of Nocturn 8 -M,R,9,9 for all Lessons of Nocturn 9 -M,R,10,10 for all Lessons of Nocturn 10 -M,R,11,11 for all Lessons of Nocturn 11 -M,R,12,12 for all Lessons of Nocturn 12 -M,R,13,13 for all Lessons of Nocturn 13 -M,R,14,14 for all Lessons of Nocturn 14 -M,R,15,15 for all Lessons of Nocturn 15 -M,R,16,16 for all Lessons of Nocturn 16 -M,R,17,17 for all Lessons of Nocturn 17 -M,R,18,18 for all Lessons of Nocturn 18 -M,R,--,-- -M,V,1,Verse 1 -M,V,2,Verse 2 -M,V,3,Verse 3 -M,W,1,Antiphon for all Psalms of Nocturn 1 -M,W,2,Antiphon for all Psalms of Nocturn 2 -M,W,3,Antiphon for all Psalms of Nocturn 3 -N,A,--,-- -N,R,--,-- -N,V,1,Verse 1 -N,W,--,-- -P,A,--,-- -P,R,--,-- -P,V,1,Verse 1 -P,W,--,-- -S,A,--,-- -S,R,--,-- -S,V,1,Verse 1 -S,W,--,-- -T,A,--,-- -T,R,--,-- -T,V,1,Verse 1 -T,W,--,-- -V,A,1,Antiphon for the 1st Psalm -V,A,2,Antiphon for the 2nd Psalm -V,A,3,Antiphon for the 3rd Psalm -V,A,4,Antiphon for the 4th Psalm -V,A,5,Antiphon for the 5th Psalm -V,A,6,Antiphon for the 6th Psalm -V,A,7,Antiphon for the 7th Psalm -V,A,8,Antiphon for the 8th Psalm -V,A,9,Antiphon for the 9th Psalm -V,A,10,Antiphon for the 10th Psalm -V,A,11,Antiphon for the 11th Psalm -V,A,12,Antiphon for the 12th Psalm -V,A,13,Antiphon for the 13th Psalm -V,A,14,Antiphon for the 14th Psalm -V,A,15,Antiphon for the 15th Psalm -V,A,16,Antiphon for the 16th Psalm -V,A,--,-- -V,A,10M,10th Antiphon for the Magnificat -V,A,11M,11th Antiphon for the Magnificat -V,A,12M,12th Antiphon for the Magnificat -V,A,1M,1st Antiphon for the Magnificat -V,A,2M,2nd Antiphon for the Magnificat -V,A,3M,3rd Antiphon for the Magnificat -V,A,4M,4th Antiphon for the Magnificat -V,A,5M,5th Antiphon for the Magnificat -V,A,6M,6th Antiphon for the Magnificat -V,A,7M,7th Antiphon for the Magnificat -V,A,8M,8th Antiphon for the Magnificat -V,A,9M,9th Antiphon for the Magnificat -V,A,M,Antiphon for the Magnificat -V,A,R,Antiphon sung as a memorial -V,R,--,-- -V,V,1,Verse 1 -V,V,2,Verse 2 -V2,A,1,Antiphon for the 1st Psalm -V2,A,2,Antiphon for the 2nd Psalm -V2,A,3,Antiphon for the 3rd Psalm -V2,A,4,Antiphon for the 4th Psalm -V2,A,5,Antiphon for the 5th Psalm -V2,A,6,Antiphon for the 6th Psalm -V2,A,7,Antiphon for the 7th Psalm -V2,A,8,Antiphon for the 8th Psalm -V2,A,9,Antiphon for the 9th Psalm -V2,A,10,Antiphon for the 10th Psalm -V2,A,--,-- -V2,A,1M,1st Antiphon for the Magnificat -V2,A,2M,2nd Antiphon for the Magnificat -V2,A,3M,3rd Antiphon for the Magnificat -V2,A,4M,4th Antiphon for the Magnificat -V2,A,M,Antiphon for the Magnificat -V2,A,R,Antiphon sung as a memorial -V2,AV,1,Antiphon for the 1st Psalm -V2,AV,2,Antiphon for the 2nd Psalm -V2,AV,3,Antiphon for the 3rd Psalm -V2,AV,4,Antiphon for the 4th Psalm -V2,R,--,-- -V2,V,1,Verse 1 -V2,W,--,-- -X,A,1,1st Antiphon -X,A,2,2nd Antiphon -X,A,3,3rd Antiphon -X,A,4,4th Antiphon -X,A,5,5th Antiphon -X,A,6,6th Antiphon -X,A,7,7th Antiphon -X,A,8,8th Antiphon -X,A,9,9th Antiphon -X,A,10,10th Antiphon -X,A,11,11th Antiphon -X,A,12,12th Antiphon -X,A,13,13th Antiphon -X,A,14,14th Antiphon -X,A,--,-- -X,A,P,Antiphon for All Psalms -X,AV,1,1st Antiphon Verse -X,M,1,1st Miscellaneous Chant -X,M,2,2nd Miscellaneous Chant -X,M,3,3rd Miscellaneous Chant -X,M,4,4th Miscellaneous Chant -X,M,5,5th Miscellaneous Chant -X,M,6,6th Miscellaneous Chant -X,M,--,-- -X,R,1,1st Responsory -X,R,2,2nd Responsory -X,R,3,3rd Responsory -X,R,4,4th Responsory -X,R,5,5th Responsory -X,R,--,-- -X,R,P,Responsory for All Psalms -X,V,1,Verse 1 -X,V,2,Verse 2 -X,W,--,-- \ No newline at end of file