diff --git a/django/cantusdb_project/cantusindex.py b/django/cantusdb_project/cantusindex.py index 7980b2cc6..e456d6211 100644 --- a/django/cantusdb_project/cantusindex.py +++ b/django/cantusdb_project/cantusindex.py @@ -3,12 +3,14 @@ Cantus Index's (CI's) various APIs. """ -import requests -from typing import Optional, Union, Callable -from main_app.models import Genre import json +from typing import Optional, Union, Callable + +import requests from requests.exceptions import SSLError, Timeout, HTTPError +from main_app.models import Genre + CANTUS_INDEX_DOMAIN: str = "https://cantusindex.uwaterloo.ca" OLD_CANTUS_INDEX_DOMAIN: str = "https://cantusindex.org" DEFAULT_TIMEOUT: float = 2 # seconds diff --git a/django/cantusdb_project/main_app/admin/filters.py b/django/cantusdb_project/main_app/admin/filters.py new file mode 100644 index 000000000..d4f0f0ade --- /dev/null +++ b/django/cantusdb_project/main_app/admin/filters.py @@ -0,0 +1,17 @@ +from django.contrib.admin import SimpleListFilter + + +class InputFilter(SimpleListFilter): + template = "admin/input_filter.html" + + def lookups(self, request, model_admin): + return (), + + def choices(self, changelist): + all_choice = next(super().choices(changelist)) + all_choice["query_parts"] = ( + (key, value) + for key, value in changelist.get_filters_params().items() + if key != self.parameter_name + ) + yield all_choice diff --git a/django/cantusdb_project/main_app/admin/institution.py b/django/cantusdb_project/main_app/admin/institution.py index 71d9c9d48..3938732fc 100644 --- a/django/cantusdb_project/main_app/admin/institution.py +++ b/django/cantusdb_project/main_app/admin/institution.py @@ -1,7 +1,21 @@ from django.contrib import admin +from django.urls import reverse +from django.utils.safestring import mark_safe from main_app.admin.base_admin import BaseModelAdmin -from main_app.models import Institution, InstitutionIdentifier +from main_app.models import Institution, InstitutionIdentifier, Source + + +class InstitutionSourceInline(admin.TabularInline): + model = Source + extra = 0 + fields = ("link_id_field", "shelfmark", "published") + readonly_fields = ("link_id_field", "published", "shelfmark") + can_delete = False + + def link_id_field(self, obj): + change_url = reverse('admin:main_app_source_change', args=(obj.pk,)) + return mark_safe(f'{obj.pk}') class InstitutionIdentifierInline(admin.TabularInline): @@ -12,10 +26,16 @@ class InstitutionIdentifierInline(admin.TabularInline): @admin.register(Institution) class InstitutionAdmin(BaseModelAdmin): - list_display = ("name", "siglum", "get_city_region", "country") + list_display = ("name", "siglum", "get_city_region", "country", "is_private_collector") search_fields = ("name", "siglum", "city", "region", "alternate_names") - list_filter = ("city",) - inlines = (InstitutionIdentifierInline,) + list_filter = ("is_private_collector", "city") + inlines = (InstitutionIdentifierInline, InstitutionSourceInline) + fieldsets = [ + (None, {"fields": ("name", "city", "region", "country", "alternate_names", + "former_sigla", "private_notes")}), + ("Private Collector", {"fields": ["is_private_collector"]}), + ("Holding Institution", {"fields": ["siglum"]}) + ] def get_city_region(self, obj) -> str: city: str = obj.city if obj.city else "[No city]" diff --git a/django/cantusdb_project/main_app/admin/source.py b/django/cantusdb_project/main_app/admin/source.py index d047d518d..4e667952e 100644 --- a/django/cantusdb_project/main_app/admin/source.py +++ b/django/cantusdb_project/main_app/admin/source.py @@ -1,19 +1,34 @@ from django.contrib import admin from main_app.admin.base_admin import BaseModelAdmin, EXCLUDE, READ_ONLY +from main_app.admin.filters import InputFilter from main_app.forms import AdminSourceForm from main_app.models import Source +class SourceKeyFilter(InputFilter): + parameter_name = "holding_institution__siglum" + title = "Institution Siglum" + + def queryset(self, request, queryset): + if self.value(): + return queryset.filter(holding_institution__siglum__icontains=self.value()) + + @admin.register(Source) class SourceAdmin(BaseModelAdmin): exclude = EXCLUDE + ("source_status",) + raw_id_fields = ("holding_institution",) # These search fields are also available on the user-source inline relationship in the user admin page search_fields = ( "siglum", "title", + "shelfmark", + "holding_institution__siglum", + "holding_institution__name", "id", + "provenance_notes" ) readonly_fields = READ_ONLY + ( "number_of_chants", @@ -36,19 +51,27 @@ class SourceAdmin(BaseModelAdmin): ) list_display = ( - "title", - "siglum", + "shelfmark", + # "title", + "holding_institution", + # "siglum", "id", ) list_filter = ( + SourceKeyFilter, "full_source", "segment", "source_status", "published", "century", + "holding_institution__is_private_collector", ) ordering = ("siglum",) form = AdminSourceForm + + def get_queryset(self, request): + queryset = super().get_queryset(request) + return queryset.select_related("holding_institution") \ No newline at end of file diff --git a/django/cantusdb_project/main_app/forms.py b/django/cantusdb_project/main_app/forms.py index 3c366d35f..14c09383f 100644 --- a/django/cantusdb_project/main_app/forms.py +++ b/django/cantusdb_project/main_app/forms.py @@ -172,8 +172,10 @@ class SourceCreateForm(forms.ModelForm): class Meta: model = Source fields = [ - "title", - "siglum", + # "title", + # "siglum", + "holding_institution", + "shelfmark", "provenance", "provenance_notes", "full_source", @@ -196,8 +198,10 @@ class Meta: "indexing_notes", ] widgets = { - "title": TextInputWidget(), - "siglum": TextInputWidget(), + # "title": TextInputWidget(), + # "siglum": TextInputWidget(), + "holding_institution": autocomplete.ModelSelect2(url="holding-autocomplete"), + "shelfmark": TextInputWidget(), "provenance": autocomplete.ModelSelect2(url="provenance-autocomplete"), "provenance_notes": TextInputWidget(), "date": TextInputWidget(), @@ -355,8 +359,10 @@ class SourceEditForm(forms.ModelForm): class Meta: model = Source fields = [ - "title", - "siglum", + # "title", + # "siglum", + "holding_institution", + "shelfmark", "provenance", "provenance_notes", "full_source", @@ -380,8 +386,8 @@ class Meta: "other_editors", ] widgets = { - "title": TextInputWidget(), - "siglum": TextInputWidget(), + "holding_institution": autocomplete.ModelSelect2(url="holding-autocomplete"), + "shelfmark": TextInputWidget(), "provenance": autocomplete.ModelSelect2(url="provenance-autocomplete"), "provenance_notes": TextInputWidget(), "date": TextInputWidget(), diff --git a/django/cantusdb_project/main_app/management/commands/migrate_institution_source_records.py b/django/cantusdb_project/main_app/management/commands/migrate_institution_source_records.py new file mode 100644 index 000000000..7d20e68be --- /dev/null +++ b/django/cantusdb_project/main_app/management/commands/migrate_institution_source_records.py @@ -0,0 +1,279 @@ +from collections import defaultdict + +import requests +from django.core.exceptions import ValidationError +from django.core.management import BaseCommand + +from main_app.identifiers import ExternalIdentifiers +from main_app.models import Source, Institution, InstitutionIdentifier + +sigla_to_skip = { + "N-N.miss.imp.1519", + "McGill, Fragment 21", + "D-WÜ/imp1583", + "Unknown", + "Test-Test IV", + "Test-Test VI", + "Test-test VII", + "D-P/imp1511", + "MA Impr. 1537", + "GOTTSCHALK", + "BEAUVAIS", + "D-A/imp:1498", +} + +private_collections = { + "US-SLpc", + "US-CinOHpc", + "US-AshORpc", + "US-CTpc", + "IRL-Dpc", + "US-NYpc", + "GB-Oxpc", + "US-Phpc", + "D-ROTTpc", + "D-Berpc", + "US-Nevpc", + "US-IssWApc", + "US-RiCTpc", + "US-RiCTpc,", + "US-OakCApc", + "US-CApc", + "ZA-Newpc", + "CDN-MtlQCpc", + "CDN-MasQCpc", + "CDN-HalNSpc", + "CDN-WatONpc", + "CDN-LonONpc", + "CDN-VicBCpc", + "US-BosMApc", + "US-RiCTpc", + "US-Unpc", + "US-SalNHpc", + "F-Villpc", + "GB-Brpc", + "CDN-NVanBCpc", + "CDN-SYpc", + "NL-EINpc", + "BR-PApc" +} + +siglum_to_country = { + "A": "Austria", + "AUS": "Australia", + "B": "Belgium", + "BR": "Brazil", + "CDN": "Canada", + "CH": "Switzerland", + "CZ": "Czechia", + "D": "Germany", + "DK": "Denmark", + "E": "Spain", + "EC": "Ecuador", + "F": "France", + "FIN": "Finland", + "GB": "United Kingdom", + "GR": "Greece", + "H": "Hungary", + "HR": "Croatia", + "I": "Italy", + "IRL": "Ireland", + "NL": "Netherlands", + "NZ": "New Zealand", + "P": "Portugal", + "PL": "Poland", + "RO": "Romania", + "SI": "Slovenia", + "SK": "Slovakia", + "SA": "South Africa", + "ZA": "South Africa", + "T": "Taiwan", + "TR": "Turkey", + "US": "United States", + "V": "Vatican City", + "XX": "Unknown", +} + +prints = { + "MA Impr. 1537", + "N-N.miss.imp.1519", + "D-A/imp:1498", + "D-P/imp1511", + "D-WÜ/imp1583" +} + + +class Command(BaseCommand): + help = "Creates institution records based on the entries in the Sources model" + + def add_arguments(self, parser): + parser.add_argument("-e", "--errors", action="store_true") + parser.add_argument("-l", "--lookup", action="store_true") + parser.add_argument("-d", "--dry-run", action="store_true") + parser.add_argument("-m", "--empty", action="store_true") + + def handle(self, *args, **options): + if options["empty"]: + print(self.style.WARNING("Deleting records...")) + Source.objects.all().update(holding_institution=None) + Institution.objects.all().delete() + InstitutionIdentifier.objects.all().delete() + + insts_name = defaultdict(set) + insts_ids = defaultdict(set) + insts_city = defaultdict(set) + insts_rism = {} + bad_sigla = set() + source_shelfmarks = {} + + for source in Source.objects.all().order_by("siglum"): + source_name = source.title + source_siglum = source.siglum + + try: + city, institution_name, shelfmark = source_name.split(",", 2) + source_shelfmarks[source.id] = shelfmark.strip() + except ValueError: + print( + self.style.WARNING( + f"{source.id:^11}| Could not extract institution name for {source_name}" + ) + ) + city = "[Unknown]" + institution_name = source_name + source_shelfmarks[source.id] = source_siglum.strip() + shelfmark = source_siglum.strip() + + try: + siglum, _ = source_siglum.split(" ", 1) + except ValueError: + print( + self.style.WARNING( + f"{source.id:^11}| Could not extract siglum for {source_siglum}" + ) + ) + bad_sigla.add(source_siglum) + siglum = source_siglum + + insts_name[siglum].add(institution_name.strip()) + insts_city[siglum].add(city.strip()) + insts_ids[siglum].add(source.id) + + if options["lookup"] and (siglum not in bad_sigla or siglum not in private_collections or siglum not in insts_rism): + req = requests.get( + f"https://rism.online/sigla/{siglum}", + allow_redirects=True, + headers={"Accept": "application/ld+json"}, + ) + if req.status_code != 200: + print( + self.style.WARNING( + f"{source.id:^11}| Could not fetch siglum {siglum}" + ) + ) + bad_sigla.add(siglum) + else: + resp = req.json() + inst_ident = resp.get("id", "") + rism_id = "/".join(inst_ident.split("/")[-2:]) + insts_rism[siglum] = rism_id + + print( + self.style.SUCCESS( + f"{source.id:^11}|{city:^31}|{institution_name:^101}|{siglum:^11}|{shelfmark}" + ) + ) + + if options["lookup"]: + print("Bad Sigla: ") + for sig in bad_sigla: + names = list(insts_name[sig]) + print(sig, ",", names if len(names) > 0 else "No name") + + print("Here are the institutions that I will create:") + print("siglum,city,country,name,alt_names") + + print_inst = Institution.objects.create( + name="Print (Multiple Copies)", + siglum="XX-NN", + city=None + ) + + for sig, names in insts_name.items(): + print("Sig: ", sig) + inst_id = insts_ids[sig] + + if sig not in prints: + if sig == "MA Impr. 1537": + print("WHAHAHAHATTT?T??T?TTT?T") + + inst_city = insts_city[sig] + main_city = list(inst_city)[0] if len(inst_city) > 0 else "" + main_name = list(names)[0] if len(names) > 0 else "" + alt_names = "; ".join(list(names)[1:]) + alt_names_fmt = f'"{alt_names}"' if alt_names else "" + + try: + inst_country = siglum_to_country[sig.split("-")[0]] + inst_sig = sig + except KeyError: + print(self.style.WARNING(f"Unknown country for siglum {sig}.")) + inst_country = None + # Setting siglum to None will make it XX-NN + inst_sig = None + + print(f"{inst_sig},{main_city},{inst_country},{main_name},{alt_names_fmt}") + + if options["dry_run"]: + continue + + iobj = { + "city": main_city if main_city != "[Unknown]" else None, + "country": inst_country, + "name": main_name, + "alternate_names": "\n".join(list(names)[1:]), + } + + if inst_sig in private_collections: + iobj["is_private_collector"] = True + elif inst_sig is not None: + iobj["siglum"] = inst_sig + else: + print(self.style.WARNING(f"Could not create {inst_id}. Setting siglum to XX-NN")) + iobj["siglum"] = "XX-NN" + + try: + holding_institution = Institution.objects.create(**iobj) + except ValidationError: + print( + self.style.WARNING(f"Could not create {sig} {main_name}. Setting institution to None") + ) + holding_institution = None + + if holding_institution: + print("Created", holding_institution) + else: + holding_institution = print_inst + + if rismid := insts_rism.get(sig): + instid = InstitutionIdentifier.objects.create( + identifier=rismid, + identifier_type=ExternalIdentifiers.RISM, + institution=holding_institution, + ) + instid.save() + + for source_id in list(inst_id): + shelfmark = source_shelfmarks.get(source_id) + + s = Source.objects.get(id=source_id) + if not shelfmark: + shelfmark = s.siglum + + print(s) + s.holding_institution = holding_institution + s.shelfmark = shelfmark.strip() + s.save() + print(self.style.SUCCESS( + f"Saved update to Source {s.id}" + )) diff --git a/django/cantusdb_project/main_app/management/commands/migrate_records.py b/django/cantusdb_project/main_app/management/commands/migrate_records.py new file mode 100644 index 000000000..9d6248295 --- /dev/null +++ b/django/cantusdb_project/main_app/management/commands/migrate_records.py @@ -0,0 +1,236 @@ +from typing import Optional + +import requests +from django.core.management import BaseCommand + +from main_app.identifiers import ExternalIdentifiers +from main_app.models import Source, Institution, InstitutionIdentifier + +private_collections = { + "US-SLpc", + "US-CinOHpc", + "US-AshORpc", + "US-CTpc", + "IRL-Dpc", + "US-NYpc", + "GB-Oxpc", + "US-Phpc", + "D-ROTTpc", + "D-Berpc", + "US-Nevpc", + "US-IssWApc", + "US-RiCTpc", + "US-RiCTpc,", + "US-OakCApc", + "US-CApc", + "ZA-Newpc", + "CDN-MtlQCpc", + "CDN-MasQCpc", + "CDN-HalNSpc", + "CDN-WatONpc", + "CDN-LonONpc", + "CDN-VicBCpc", + "US-BosMApc", + "US-RiCTpc", + "US-Unpc", + "US-SalNHpc", + "F-Villpc", + "GB-Brpc", + "CDN-NVanBCpc", + "CDN-SYpc", + "NL-EINpc", + "BR-PApc" +} + +siglum_to_country = { + "A": "Austria", + "AUS": "Australia", + "B": "Belgium", + "BR": "Brazil", + "CDN": "Canada", + "CH": "Switzerland", + "CZ": "Czechia", + "D": "Germany", + "DK": "Denmark", + "E": "Spain", + "EC": "Ecuador", + "F": "France", + "FIN": "Finland", + "GB": "United Kingdom", + "GR": "Greece", + "H": "Hungary", + "HR": "Croatia", + "I": "Italy", + "IRL": "Ireland", + "NL": "Netherlands", + "NZ": "New Zealand", + "P": "Portugal", + "PL": "Poland", + "RO": "Romania", + "SI": "Slovenia", + "SK": "Slovakia", + "SA": "South Africa", + "ZA": "South Africa", + "T": "Taiwan", + "TR": "Turkey", + "US": "United States", + "V": "Vatican City", + "XX": "Unknown", +} + +prints = { + "MA Impr. 1537", + "N-N.miss.imp.1519", + "D-A/imp:1498", + "D-P/imp1511", + "D-WÜ/imp1583" +} + + +class Command(BaseCommand): + def add_arguments(self, parser): + parser.add_argument("-e", "--empty", action="store_true") + + def handle(self, *args, **options): + if options["empty"]: + print( + self.style.WARNING("Deleting records...") + ) + Source.objects.all().update(holding_institution=None) + Institution.objects.all().delete() + InstitutionIdentifier.objects.all().delete() + + print_inst = Institution.objects.create( + name="Print (Multiple Copies)", + siglum="XX-NN", + city=None + ) + + # Store a siglum: id + created_institutions = {} + # Track the sources with a bad siglum so that we don't try and look it up and fail. + bad_siglum = set() + + for source in Source.objects.all().order_by("siglum"): + print( + self.style.SUCCESS(f"Processing {source.id}") + ) + source_name = source.title + source_siglum = source.siglum + + try: + city, institution_name, shelfmark = source_name.split(",", 2) + except ValueError: + print( + self.style.WARNING( + f"{source.id:^11}| Could not extract institution name for {source_name}" + ) + ) + city = "[Unknown]" + institution_name = source_name + shelfmark = source_siglum + + try: + siglum, _ = source_siglum.split(" ", 1) + except ValueError: + print( + self.style.WARNING( + f"{source.id:^11}| Could not extract siglum for {source_siglum}" + ) + ) + siglum = "XX-NN" + bad_siglum.add(source.id) + + try: + country = siglum_to_country[siglum.split("-")[0]] + except KeyError: + print( + self.style.WARNING( + f"{source.id:^11}| Could not extract country for {source_siglum}" + ) + ) + country = "[Unknown Country]" + bad_siglum.add(source.id) + + if source_siglum in prints: + print( + self.style.SUCCESS( + f"Adding {source_siglum} to the printed records institution" + ) + ) + institution = print_inst + elif siglum in created_institutions: + print( + self.style.SUCCESS( + f"Re-using the pre-created institution for {siglum}" + ) + ) + + institution = created_institutions[siglum] + if institution_name != institution.name: + institution.alternate_names = f"{institution.alternate_names}\n{institution_name}" + institution.save() + elif siglum not in created_institutions: + print( + self.style.SUCCESS( + f"Creating institution record for {siglum}" + ) + ) + + iobj = { + "city": city.strip() if city else None, + "country": country, + "name": institution_name.strip(), + } + + if siglum in private_collections: + iobj["is_private_collector"] = True + else: + iobj["siglum"] = siglum + + institution = Institution.objects.create( + **iobj + ) + + if source.id not in bad_siglum and siglum not in private_collections: + rism_id = get_rism_id(siglum) + if rism_id: + print( + self.style.SUCCESS( + f"Adding {rism_id} to the identifiers for {siglum}" + ) + ) + + instid = InstitutionIdentifier.objects.create( + identifier=rism_id, + identifier_type=ExternalIdentifiers.RISM, + institution=institution, + ) + instid.save() + + created_institutions[siglum] = institution + + else: + print(self.style.ERROR( + f"Could not determine the holding institution for {source}" + )) + continue + + source.holding_institution = institution + source.shelfmark = shelfmark.strip() + source.save() + + +def get_rism_id(siglum) -> Optional[str]: + req = requests.get( + f"https://rism.online/sigla/{siglum}", + allow_redirects=True, + headers={"Accept": "application/ld+json"}, + ) + if req.status_code != 200: + return None + else: + resp = req.json() + inst_ident = resp.get("id", "") + rism_id = "/".join(inst_ident.split("/")[-2:]) + return rism_id \ No newline at end of file diff --git a/django/cantusdb_project/main_app/management/commands/reassign_feasts.py b/django/cantusdb_project/main_app/management/commands/reassign_feasts.py new file mode 100644 index 000000000..5ad542595 --- /dev/null +++ b/django/cantusdb_project/main_app/management/commands/reassign_feasts.py @@ -0,0 +1,58 @@ +from django.core.management.base import BaseCommand +from main_app.models import Feast, Chant, Sequence + + +FEAST_MAPPING = { + 2456: 4474, + 2094: 4475, +} + + +class Command(BaseCommand): + help = "Reassign feasts and update chants accordingly" + + def handle(self, *args, **options): + + for old_feast_id, new_feast_id in FEAST_MAPPING.items(): + try: + old_feast = Feast.objects.get(id=old_feast_id) + new_feast = Feast.objects.get(id=new_feast_id) + except Feast.DoesNotExist as e: + self.stderr.write(self.style.ERROR(f"Feast not found: {e}")) + continue + + # Transfer data (if necessary) + new_feast.name = new_feast.name or old_feast.name + new_feast.description = new_feast.description or old_feast.description + new_feast.feast_code = new_feast.feast_code or old_feast.feast_code + new_feast.notes = new_feast.notes or old_feast.notes + new_feast.month = new_feast.month or old_feast.month + new_feast.day = new_feast.day or old_feast.day + + # Calling save method will update 'prefix' field + new_feast.save() + + # Reassign chants + chants_updated = Chant.objects.filter(feast=old_feast).update( + feast=new_feast + ) + self.stdout.write( + self.style.SUCCESS( + f"Reassigned {chants_updated} chants from feast {old_feast_id} to {new_feast_id}" + ) + ) + + # Reassign sequences + sequences_updated = Sequence.objects.filter(feast=old_feast).update( + feast=new_feast + ) + self.stdout.write( + self.style.SUCCESS( + f"Reassigned {sequences_updated} sequences from feast {old_feast_id} to {new_feast_id}" + ) + ) + + old_feast.delete() + self.stdout.write(self.style.SUCCESS(f"Deleted old feast {old_feast_id}")) + + self.stdout.write(self.style.SUCCESS("Feast reassignment complete.")) diff --git a/django/cantusdb_project/main_app/migrations/0020_institution_is_private_collector_and_more.py b/django/cantusdb_project/main_app/migrations/0020_institution_is_private_collector_and_more.py new file mode 100644 index 000000000..c0bf8af80 --- /dev/null +++ b/django/cantusdb_project/main_app/migrations/0020_institution_is_private_collector_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.1.6 on 2024-06-14 13:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("main_app", "0019_remove_source_rism_siglum_delete_rismsiglum"), + ] + + operations = [ + migrations.AddField( + model_name="institution", + name="is_private_collector", + field=models.BooleanField( + default=False, help_text="Mark this institution as private collector." + ), + ), + migrations.AddField( + model_name="institution", + name="private_notes", + field=models.TextField( + blank=True, + help_text="Notes about this institution that are not publicly visible.", + null=True, + ), + ), + ] diff --git a/django/cantusdb_project/main_app/migrations/0021_source_shelfmark.py b/django/cantusdb_project/main_app/migrations/0021_source_shelfmark.py new file mode 100644 index 000000000..9815e733b --- /dev/null +++ b/django/cantusdb_project/main_app/migrations/0021_source_shelfmark.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.6 on 2024-06-14 13:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("main_app", "0020_institution_is_private_collector_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="source", + name="shelfmark", + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/django/cantusdb_project/main_app/migrations/0022_alter_source_siglum_alter_source_title.py b/django/cantusdb_project/main_app/migrations/0022_alter_source_siglum_alter_source_title.py new file mode 100644 index 000000000..6bae4039d --- /dev/null +++ b/django/cantusdb_project/main_app/migrations/0022_alter_source_siglum_alter_source_title.py @@ -0,0 +1,32 @@ +# Generated by Django 4.1.6 on 2024-06-18 12:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("main_app", "0021_source_shelfmark"), + ] + + operations = [ + migrations.AlterField( + model_name="source", + name="siglum", + field=models.CharField( + blank=True, + help_text="RISM-style siglum + Shelf-mark (e.g. GB-Ob 202).", + max_length=63, + null=True, + ), + ), + migrations.AlterField( + model_name="source", + name="title", + field=models.CharField( + blank=True, + help_text="Full Source Identification (City, Archive, Shelf-mark)", + max_length=255, + null=True, + ), + ), + ] diff --git a/django/cantusdb_project/main_app/migrations/0023_alter_institution_siglum_and_more.py b/django/cantusdb_project/main_app/migrations/0023_alter_institution_siglum_and_more.py new file mode 100644 index 000000000..d365f1f97 --- /dev/null +++ b/django/cantusdb_project/main_app/migrations/0023_alter_institution_siglum_and_more.py @@ -0,0 +1,47 @@ +# Generated by Django 4.1.6 on 2024-06-20 15:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("main_app", "0022_alter_source_siglum_alter_source_title"), + ] + + operations = [ + migrations.AlterField( + model_name="institution", + name="siglum", + field=models.CharField( + blank=True, + help_text="Reserved for assigned RISM sigla", + max_length=32, + null=True, + verbose_name="RISM Siglum", + ), + ), + migrations.AddConstraint( + model_name="institution", + constraint=models.CheckConstraint( + check=models.Q( + ("is_private_collector", True), + ("siglum__isnull", False), + _negated=True, + ), + name="siglum_and_private_not_valid", + violation_error_message="Siglum and Private Collector cannot both be specified.", + ), + ), + migrations.AddConstraint( + model_name="institution", + constraint=models.CheckConstraint( + check=models.Q( + ("is_private_collector", True), + ("siglum__isnull", False), + _connector="OR", + ), + name="at_least_one_of_siglum_or_private_collector", + violation_error_message="At least one of Siglum or Private Collector must be specified.", + ), + ), + ] diff --git a/django/cantusdb_project/main_app/migrations/0024_merge_20240714_2153.py b/django/cantusdb_project/main_app/migrations/0024_merge_20240714_2153.py new file mode 100644 index 000000000..52c830bba --- /dev/null +++ b/django/cantusdb_project/main_app/migrations/0024_merge_20240714_2153.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.11 on 2024-07-14 21:53 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "main_app", + "0020_remove_chant_segment_remove_sequence_segment_project_and_more", + ), + ("main_app", "0023_alter_institution_siglum_and_more"), + ] + + operations = [] diff --git a/django/cantusdb_project/main_app/models/feast.py b/django/cantusdb_project/main_app/models/feast.py index 7caaf0041..b35f8cbcc 100644 --- a/django/cantusdb_project/main_app/models/feast.py +++ b/django/cantusdb_project/main_app/models/feast.py @@ -17,7 +17,6 @@ class Feast(BaseModel): blank=True, null=True, validators=[MinValueValidator(1), MaxValueValidator(31)] ) - # the `prefix` field can be automatically populated by running `python manage.py add_prefix` prefix = models.CharField(max_length=2, blank=True, null=True, editable=False) class Meta: diff --git a/django/cantusdb_project/main_app/models/institution.py b/django/cantusdb_project/main_app/models/institution.py index 02fd1f19e..fd7c4e61c 100644 --- a/django/cantusdb_project/main_app/models/institution.py +++ b/django/cantusdb_project/main_app/models/institution.py @@ -1,15 +1,41 @@ from django.db import models +from django.db.models import CheckConstraint, Q from main_app.models import BaseModel region_help_text = """Province / State / Canton / County. Used to disambiguate cities, e.g., "London (Ontario)".""" city_help_text = """City / Town / Village / Settlement""" +private_collector_help = """Mark this institution as private collector.""" class Institution(BaseModel): + class Meta: + constraints = [ + CheckConstraint( + check=~(Q(is_private_collector=True) & Q(siglum__isnull=False)), + name="siglum_and_private_not_valid", + violation_error_message="Siglum and Private Collector cannot both be specified." + ), + CheckConstraint( + check=(Q(is_private_collector=True) | Q(siglum__isnull=False)), + name="at_least_one_of_siglum_or_private_collector", + violation_error_message="At least one of Siglum or Private Collector must be specified." + ) + + ] name = models.CharField(max_length=255, default="s.n.") - siglum = models.CharField(max_length=32, default="XX-Nn") + siglum = models.CharField( + verbose_name="RISM Siglum", + max_length=32, + blank=True, + null=True, + help_text="Reserved for assigned RISM sigla", + ) + is_private_collector = models.BooleanField( + default=False, + help_text=private_collector_help, + ) city = models.CharField( max_length=64, blank=True, null=True, help_text=city_help_text ) @@ -23,7 +49,11 @@ class Institution(BaseModel): former_sigla = models.TextField( blank=True, null=True, help_text="Enter former sigla on separate lines." ) + private_notes = models.TextField( + blank=True, null=True, help_text="Notes about this institution that are not publicly visible." + ) def __str__(self) -> str: - sigl: str = f"({self.siglum})" if self.siglum else "" - return f"{self.name} {sigl}" + sigl: str = f" ({self.siglum})" if self.siglum else "" + city: str = f"{self.city}, " if self.city else "" + return f"{city}{self.name}{sigl}" diff --git a/django/cantusdb_project/main_app/models/source.py b/django/cantusdb_project/main_app/models/source.py index 30abb038f..0be3e23d9 100644 --- a/django/cantusdb_project/main_app/models/source.py +++ b/django/cantusdb_project/main_app/models/source.py @@ -27,14 +27,16 @@ class Source(BaseModel): title = models.CharField( max_length=255, + blank=True, + null=True, help_text="Full Source Identification (City, Archive, Shelf-mark)", ) # the siglum field as implemented on the old Cantus is composed of both the RISM siglum and the shelfmark # it is a human-readable ID for a source siglum = models.CharField( max_length=63, - null=False, - blank=False, + null=True, + blank=True, help_text="RISM-style siglum + Shelf-mark (e.g. GB-Ob 202).", ) holding_institution = models.ForeignKey( @@ -43,6 +45,11 @@ class Source(BaseModel): null=True, blank=True, ) + shelfmark = models.CharField( + max_length=255, + blank=True, + null=True, + ) provenance = models.ForeignKey( "Provenance", on_delete=models.PROTECT, @@ -131,8 +138,7 @@ class Source(BaseModel): number_of_melodies = models.IntegerField(blank=True, null=True) def __str__(self): - string = "[{s}] {t} ({i})".format(s=self.siglum, t=self.title, i=self.id) - return string + return self.heading def save(self, *args, **kwargs): # when creating a source, assign it to "CANTUS Database" segment by default @@ -140,3 +146,30 @@ def save(self, *args, **kwargs): cantus_db_segment = Segment.objects.get(name="CANTUS Database") self.segment = cantus_db_segment super().save(*args, **kwargs) + + @property + def heading(self) -> str: + title = [] + if holdinst := self.holding_institution: + city = f"{holdinst.city}," if holdinst.city else "" + title.append(city) + title.append(f"{holdinst.name},") + + tt = self.shelfmark if self.shelfmark else self.title + + title.append(tt) + + return " ".join(title) + + @property + def short_heading(self) -> str: + title = [] + if holdinst := self.holding_institution: + if holdinst.siglum and holdinst.siglum != "XX-NN": + title.append(f"{holdinst.siglum}") + elif holdinst.is_private_collector: + title.append("Private") + + tt = self.shelfmark if self.shelfmark else self.title + title.append(tt) + return " ".join(title) diff --git a/django/cantusdb_project/main_app/templates/400.html b/django/cantusdb_project/main_app/templates/400.html index 3ee52993d..2d12bcd82 100644 --- a/django/cantusdb_project/main_app/templates/400.html +++ b/django/cantusdb_project/main_app/templates/400.html @@ -1,6 +1,5 @@ {% load static %} -
@@ -8,7 +7,7 @@ - +