diff --git a/.github/workflows/django_tests.yml b/.github/workflows/django_tests.yml new file mode 100644 index 000000000..b175071e2 --- /dev/null +++ b/.github/workflows/django_tests.yml @@ -0,0 +1,29 @@ +name: django-tests +on: + pull_request: + types: [opened, synchronize] +jobs: + run-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: mkdir -p config/envs + - uses: SpicyPizza/create-envfile@v2.0 + with: + envkey_POSTGRES_DB: test_cantusdb + envkey_POSTGRES_USER: test_user + envkey_POSTGRES_HOST: postgres + envkey_POSTGRES_PORT: 5432 + envkey_PROJECT_ENVIRONMENT: PRODUCTION + envkey_CANTUSDB_STATIC_ROOT: /path/to/static + envkey_CANTUSDB_MEDIA_ROOT: /path/to/media + envkey_CANTUSDB_HOST: somehost + envkey_CANTUSDB_SECRET_KEY: "hereisakey1234" + envkey_POSTGRES_PASSWORD: woahagreatpasswordabc + envkey_AWS_EMAIL_HOST_USER: test_user + envkey_AWS_EMAIL_HOST_PASSWORD: test_password + directory: config/envs + file_name: dev_env + - run: docker compose -f docker-compose-development.yml build + - run: docker compose -f docker-compose-development.yml up -d + - run: docker compose -f docker-compose-development.yml exec -T django python manage.py test main_app.tests \ No newline at end of file diff --git a/config/nginx/conf.d/cantusdb.conf.development b/config/nginx/conf.d/cantusdb.conf.development index 1b8e0b654..972c91519 100644 --- a/config/nginx/conf.d/cantusdb.conf.development +++ b/config/nginx/conf.d/cantusdb.conf.development @@ -23,17 +23,9 @@ server { alias /resources/api_cache/concordances.json; expires modified +24h; } - - location = /style.css { - root /; - } - location = /background.jpg { - root /; - } - location = /CantusLogoSmall.gif { - root /; - } - location = /favicon.ico { + + error_page 500 /500.html; + location = /500.html { root /; } @@ -45,4 +37,4 @@ server { location = /504.html { root /; } -} \ No newline at end of file +} diff --git a/cron/management/manage.sh b/cron/management/manage.sh index 0419f28fa..0dabc8e94 100644 --- a/cron/management/manage.sh +++ b/cron/management/manage.sh @@ -7,4 +7,4 @@ DOCKER_COMPOSE_FILE=$1 # This is the path to the docker-compose file. COMMAND=$2 # This is the command to execute. -/usr/local/bin/docker compose -f $DOCKER_COMPOSE_FILE exec -T django python manage.py $COMMAND +/usr/bin/docker compose -f $DOCKER_COMPOSE_FILE exec -T django python manage.py $COMMAND diff --git a/django/cantusdb_project/cantusindex.py b/django/cantusdb_project/cantusindex.py index 210aa0505..b00ef8ee3 100644 --- a/django/cantusdb_project/cantusindex.py +++ b/django/cantusdb_project/cantusindex.py @@ -91,9 +91,12 @@ def get_suggested_chant( # mostly, in case of a timeout within get_json_from_ci_api return None - fulltext: str = json["info"]["field_full_text"] - incipit: str = " ".join(fulltext.split(" ")[:5]) - genre_name: str = json["info"]["field_genre"] + try: + fulltext: str = json["info"]["field_full_text"] + incipit: str = " ".join(fulltext.split(" ")[:5]) + genre_name: str = json["info"]["field_genre"] + except TypeError: + return None genre_id: Optional[int] = None try: genre_id = Genre.objects.get(name=genre_name).id diff --git a/django/cantusdb_project/main_app/admin.py b/django/cantusdb_project/main_app/admin.py deleted file mode 100644 index e23b07503..000000000 --- a/django/cantusdb_project/main_app/admin.py +++ /dev/null @@ -1,236 +0,0 @@ -from django.contrib import admin -from reversion.admin import VersionAdmin -from main_app.models import * -from main_app.forms import ( - AdminCenturyForm, - AdminChantForm, - AdminFeastForm, - AdminGenreForm, - AdminNotationForm, - AdminOfficeForm, - AdminProvenanceForm, - AdminRismSiglumForm, - AdminSegmentForm, - AdminSequenceForm, - AdminSourceForm, -) - -# these fields should not be editable by all classes -EXCLUDE = ("json_info",) - -READ_ONLY = ( - "created_by", - "last_updated_by", - "date_created", - "date_updated", -) - - -class BaseModelAdmin(VersionAdmin): - exclude = EXCLUDE - readonly_fields = READ_ONLY - - # if an object is created in the admin interface, assign the user to the created_by field - # else if an object is updated in the admin interface, assign the user to the last_updated_by field - def save_model(self, request, obj, form, change): - if change: - obj.last_updated_by = request.user - else: - obj.created_by = request.user - super().save_model(request, obj, form, change) - - -class CenturyAdmin(BaseModelAdmin): - search_fields = ("name",) - form = AdminCenturyForm - - -class ChantAdmin(BaseModelAdmin): - @admin.display(description="Source Siglum") - def get_source_siglum(self, obj): - if obj.source: - return obj.source.siglum - - list_display = ( - "incipit", - "get_source_siglum", - "genre", - ) - search_fields = ( - "title", - "incipit", - "cantus_id", - "id", - ) - - readonly_fields = READ_ONLY + ("incipit",) - - list_filter = ( - "genre", - "office", - ) - exclude = EXCLUDE + ( - "col1", - "col2", - "col3", - "next_chant", - "s_sequence", - "is_last_chant_in_feast", - "visible_status", - "date", - "volpiano_notes", - "volpiano_intervals", - "title", - "differentiae_database", - ) - form = AdminChantForm - raw_id_fields = ( - "source", - "feast", - ) - ordering = ("source__siglum",) - - -class DifferentiaAdmin(BaseModelAdmin): - search_fields = ( - "differentia_id", - "id", - ) - - -class FeastAdmin(BaseModelAdmin): - search_fields = ( - "name", - "feast_code", - ) - list_display = ( - "name", - "month", - "day", - "feast_code", - ) - form = AdminFeastForm - - -class GenreAdmin(BaseModelAdmin): - search_fields = ("name",) - form = AdminGenreForm - - -class NotationAdmin(BaseModelAdmin): - search_fields = ("name",) - form = AdminNotationForm - - -class OfficeAdmin(BaseModelAdmin): - search_fields = ("name",) - form = AdminOfficeForm - - -class ProvenanceAdmin(BaseModelAdmin): - search_fields = ("name",) - form = AdminProvenanceForm - - -class RismSiglumAdmin(BaseModelAdmin): - search_fields = ("name",) - form = AdminRismSiglumForm - - -class SegmentAdmin(BaseModelAdmin): - search_fields = ("name",) - form = AdminSegmentForm - - -class SequenceAdmin(BaseModelAdmin): - @admin.display(description="Source Siglum") - def get_source_siglum(self, obj): - if obj.source: - return obj.source.siglum - - search_fields = ( - "title", - "incipit", - "cantus_id", - "id", - ) - exclude = EXCLUDE + ( - "c_sequence", - "next_chant", - "is_last_chant_in_feast", - "visible_status", - ) - list_display = ("incipit", "get_source_siglum", "genre") - list_filter = ( - "genre", - "office", - ) - raw_id_fields = ( - "source", - "feast", - ) - readonly_fields = READ_ONLY + ("incipit",) - ordering = ("source__siglum",) - form = AdminSequenceForm - - -class SourceAdmin(BaseModelAdmin): - exclude = EXCLUDE + ("source_status",) - - # These search fields are also available on the user-source inline relationship in the user admin page - search_fields = ( - "siglum", - "title", - "id", - ) - readonly_fields = READ_ONLY + ( - "number_of_chants", - "number_of_melodies", - "date_created", - "date_updated", - ) - # from the Django docs: - # Adding a ManyToManyField to this list will instead use a nifty unobtrusive JavaScript “filter” interface - # that allows searching within the options. The unselected and selected options appear in two boxes side by side. - filter_horizontal = ( - "century", - "notation", - "current_editors", - "inventoried_by", - "full_text_entered_by", - "melodies_entered_by", - "proofreaders", - "other_editors", - ) - - list_display = ( - "title", - "siglum", - "id", - ) - - list_filter = ( - "full_source", - "segment", - "source_status", - "published", - "century", - ) - - ordering = ("siglum",) - - form = AdminSourceForm - - -admin.site.register(Century, CenturyAdmin) -admin.site.register(Chant, ChantAdmin) -admin.site.register(Differentia, DifferentiaAdmin) -admin.site.register(Feast, FeastAdmin) -admin.site.register(Genre, GenreAdmin) -admin.site.register(Notation, NotationAdmin) -admin.site.register(Office, OfficeAdmin) -admin.site.register(Provenance, ProvenanceAdmin) -admin.site.register(RismSiglum, RismSiglumAdmin) -admin.site.register(Segment, SegmentAdmin) -admin.site.register(Sequence, SequenceAdmin) -admin.site.register(Source, SourceAdmin) diff --git a/django/cantusdb_project/main_app/admin/__init__.py b/django/cantusdb_project/main_app/admin/__init__.py new file mode 100644 index 000000000..6839ad2d8 --- /dev/null +++ b/django/cantusdb_project/main_app/admin/__init__.py @@ -0,0 +1,14 @@ +from main_app.admin.century import CenturyAdmin +from main_app.admin.chant import ChantAdmin +from main_app.admin.differentia import DifferentiaAdmin +from main_app.admin.feast import FeastAdmin +from main_app.admin.genre import GenreAdmin +from main_app.admin.notation import NotationAdmin +from main_app.admin.office import OfficeAdmin +from main_app.admin.provenance import ProvenanceAdmin +from main_app.admin.rism_siglum import RismSiglumAdmin +from main_app.admin.segment import SegmentAdmin +from main_app.admin.sequence import SequenceAdmin +from main_app.admin.source import SourceAdmin +from main_app.admin.institution import InstitutionAdmin +from main_app.admin.institution_identifier import InstitutionIdentifierAdmin diff --git a/django/cantusdb_project/main_app/admin/base_admin.py b/django/cantusdb_project/main_app/admin/base_admin.py new file mode 100644 index 000000000..98963f68e --- /dev/null +++ b/django/cantusdb_project/main_app/admin/base_admin.py @@ -0,0 +1,27 @@ +from reversion.admin import VersionAdmin + + +# these fields should not be editable by all classes +EXCLUDE = ("json_info",) + + +READ_ONLY = ( + "created_by", + "last_updated_by", + "date_created", + "date_updated", +) + + +class BaseModelAdmin(VersionAdmin): + exclude = EXCLUDE + readonly_fields = READ_ONLY + + # if an object is created in the admin interface, assign the user to the created_by field + # else if an object is updated in the admin interface, assign the user to the last_updated_by field + def save_model(self, request, obj, form, change): + if change: + obj.last_updated_by = request.user + else: + obj.created_by = request.user + super().save_model(request, obj, form, change) diff --git a/django/cantusdb_project/main_app/admin/century.py b/django/cantusdb_project/main_app/admin/century.py new file mode 100644 index 000000000..31dc76c20 --- /dev/null +++ b/django/cantusdb_project/main_app/admin/century.py @@ -0,0 +1,11 @@ +from django.contrib import admin + +from main_app.admin.base_admin import BaseModelAdmin +from main_app.forms import AdminCenturyForm +from main_app.models import Century + + +@admin.register(Century) +class CenturyAdmin(BaseModelAdmin): + search_fields = ("name",) + form = AdminCenturyForm diff --git a/django/cantusdb_project/main_app/admin/chant.py b/django/cantusdb_project/main_app/admin/chant.py new file mode 100644 index 000000000..ce2c8b775 --- /dev/null +++ b/django/cantusdb_project/main_app/admin/chant.py @@ -0,0 +1,53 @@ +from django.contrib import admin + +from main_app.admin.base_admin import EXCLUDE, READ_ONLY, BaseModelAdmin +from main_app.forms import AdminChantForm +from main_app.models import Chant + + +@admin.register(Chant) +class ChantAdmin(BaseModelAdmin): + + @admin.display(description="Source Siglum") + def get_source_siglum(self, obj): + if obj.source: + return obj.source.siglum + + list_display = ( + "incipit", + "get_source_siglum", + "genre", + ) + search_fields = ( + "title", + "incipit", + "cantus_id", + "id", + ) + + readonly_fields = READ_ONLY + ("incipit",) + + list_filter = ( + "genre", + "office", + ) + exclude = EXCLUDE + ( + "col1", + "col2", + "col3", + "next_chant", + "s_sequence", + "is_last_chant_in_feast", + "visible_status", + "date", + "volpiano_notes", + "volpiano_intervals", + "title", + "differentiae_database", + ) + form = AdminChantForm + raw_id_fields = ( + "source", + "feast", + ) + ordering = ("source__siglum",) diff --git a/django/cantusdb_project/main_app/admin/differentia.py b/django/cantusdb_project/main_app/admin/differentia.py new file mode 100644 index 000000000..7d823d5ae --- /dev/null +++ b/django/cantusdb_project/main_app/admin/differentia.py @@ -0,0 +1,12 @@ +from django.contrib import admin + +from main_app.admin.base_admin import BaseModelAdmin +from main_app.models import Differentia + + +@admin.register(Differentia) +class DifferentiaAdmin(BaseModelAdmin): + search_fields = ( + "differentia_id", + "id", + ) diff --git a/django/cantusdb_project/main_app/admin/feast.py b/django/cantusdb_project/main_app/admin/feast.py new file mode 100644 index 000000000..81a070ef0 --- /dev/null +++ b/django/cantusdb_project/main_app/admin/feast.py @@ -0,0 +1,20 @@ +from django.contrib import admin + +from main_app.admin.base_admin import BaseModelAdmin +from main_app.forms import AdminFeastForm +from main_app.models import Feast + + +@admin.register(Feast) +class FeastAdmin(BaseModelAdmin): + search_fields = ( + "name", + "feast_code", + ) + list_display = ( + "name", + "month", + "day", + "feast_code", + ) + form = AdminFeastForm diff --git a/django/cantusdb_project/main_app/admin/genre.py b/django/cantusdb_project/main_app/admin/genre.py new file mode 100644 index 000000000..86f7ba84a --- /dev/null +++ b/django/cantusdb_project/main_app/admin/genre.py @@ -0,0 +1,11 @@ +from django.contrib import admin + +from main_app.admin.base_admin import BaseModelAdmin +from main_app.forms import AdminGenreForm +from main_app.models import Genre + + +@admin.register(Genre) +class GenreAdmin(BaseModelAdmin): + search_fields = ("name",) + form = AdminGenreForm diff --git a/django/cantusdb_project/main_app/admin/institution.py b/django/cantusdb_project/main_app/admin/institution.py new file mode 100644 index 000000000..71d9c9d48 --- /dev/null +++ b/django/cantusdb_project/main_app/admin/institution.py @@ -0,0 +1,25 @@ +from django.contrib import admin + +from main_app.admin.base_admin import BaseModelAdmin +from main_app.models import Institution, InstitutionIdentifier + + +class InstitutionIdentifierInline(admin.TabularInline): + model = InstitutionIdentifier + extra = 0 + exclude = ["created_by", "last_updated_by"] + + +@admin.register(Institution) +class InstitutionAdmin(BaseModelAdmin): + list_display = ("name", "siglum", "get_city_region", "country") + search_fields = ("name", "siglum", "city", "region", "alternate_names") + list_filter = ("city",) + inlines = (InstitutionIdentifierInline,) + + def get_city_region(self, obj) -> str: + city: str = obj.city if obj.city else "[No city]" + region: str = f"({obj.region})" if obj.region else "" + return f"{city} {region}" + + get_city_region.short_description = "City" diff --git a/django/cantusdb_project/main_app/admin/institution_identifier.py b/django/cantusdb_project/main_app/admin/institution_identifier.py new file mode 100644 index 000000000..6eb1288c9 --- /dev/null +++ b/django/cantusdb_project/main_app/admin/institution_identifier.py @@ -0,0 +1,10 @@ +from django.contrib import admin + +from main_app.admin.base_admin import BaseModelAdmin +from main_app.models import InstitutionIdentifier + + +@admin.register(InstitutionIdentifier) +class InstitutionIdentifierAdmin(BaseModelAdmin): + list_display = ('identifier', 'identifier_type') + raw_id_fields = ("institution",) diff --git a/django/cantusdb_project/main_app/admin/notation.py b/django/cantusdb_project/main_app/admin/notation.py new file mode 100644 index 000000000..c257b2d35 --- /dev/null +++ b/django/cantusdb_project/main_app/admin/notation.py @@ -0,0 +1,11 @@ +from django.contrib import admin + +from main_app.admin.base_admin import BaseModelAdmin +from main_app.forms import AdminNotationForm +from main_app.models import Notation + + +@admin.register(Notation) +class NotationAdmin(BaseModelAdmin): + search_fields = ("name",) + form = AdminNotationForm diff --git a/django/cantusdb_project/main_app/admin/office.py b/django/cantusdb_project/main_app/admin/office.py new file mode 100644 index 000000000..d31d56534 --- /dev/null +++ b/django/cantusdb_project/main_app/admin/office.py @@ -0,0 +1,11 @@ +from django.contrib import admin + +from main_app.admin.base_admin import BaseModelAdmin +from main_app.forms import AdminOfficeForm +from main_app.models import Office + + +@admin.register(Office) +class OfficeAdmin(BaseModelAdmin): + search_fields = ("name",) + form = AdminOfficeForm diff --git a/django/cantusdb_project/main_app/admin/provenance.py b/django/cantusdb_project/main_app/admin/provenance.py new file mode 100644 index 000000000..130b35eca --- /dev/null +++ b/django/cantusdb_project/main_app/admin/provenance.py @@ -0,0 +1,11 @@ +from django.contrib import admin + +from main_app.admin.base_admin import BaseModelAdmin +from main_app.forms import AdminProvenanceForm +from main_app.models import Provenance + + +@admin.register(Provenance) +class ProvenanceAdmin(BaseModelAdmin): + search_fields = ("name",) + form = AdminProvenanceForm diff --git a/django/cantusdb_project/main_app/admin/rism_siglum.py b/django/cantusdb_project/main_app/admin/rism_siglum.py new file mode 100644 index 000000000..36113bec8 --- /dev/null +++ b/django/cantusdb_project/main_app/admin/rism_siglum.py @@ -0,0 +1,11 @@ +from django.contrib import admin + +from main_app.admin.base_admin import BaseModelAdmin +from main_app.forms import AdminRismSiglumForm +from main_app.models import RismSiglum + + +@admin.register(RismSiglum) +class RismSiglumAdmin(BaseModelAdmin): + search_fields = ("name",) + form = AdminRismSiglumForm diff --git a/django/cantusdb_project/main_app/admin/segment.py b/django/cantusdb_project/main_app/admin/segment.py new file mode 100644 index 000000000..b8a16a6b0 --- /dev/null +++ b/django/cantusdb_project/main_app/admin/segment.py @@ -0,0 +1,11 @@ +from django.contrib import admin + +from main_app.admin.base_admin import BaseModelAdmin +from main_app.forms import AdminSegmentForm +from main_app.models import Segment + + +@admin.register(Segment) +class SegmentAdmin(BaseModelAdmin): + search_fields = ("name",) + form = AdminSegmentForm diff --git a/django/cantusdb_project/main_app/admin/sequence.py b/django/cantusdb_project/main_app/admin/sequence.py new file mode 100644 index 000000000..9c7868e4d --- /dev/null +++ b/django/cantusdb_project/main_app/admin/sequence.py @@ -0,0 +1,38 @@ +from django.contrib import admin + +from main_app.admin.base_admin import BaseModelAdmin, EXCLUDE, READ_ONLY +from main_app.forms import AdminSequenceForm +from main_app.models import Sequence + + +@admin.register(Sequence) +class SequenceAdmin(BaseModelAdmin): + @admin.display(description="Source Siglum") + def get_source_siglum(self, obj): + if obj.source: + return obj.source.siglum + + search_fields = ( + "title", + "incipit", + "cantus_id", + "id", + ) + exclude = EXCLUDE + ( + "c_sequence", + "next_chant", + "is_last_chant_in_feast", + "visible_status", + ) + list_display = ("incipit", "get_source_siglum", "genre") + list_filter = ( + "genre", + "office", + ) + raw_id_fields = ( + "source", + "feast", + ) + readonly_fields = READ_ONLY + ("incipit",) + ordering = ("source__siglum",) + form = AdminSequenceForm diff --git a/django/cantusdb_project/main_app/admin/source.py b/django/cantusdb_project/main_app/admin/source.py new file mode 100644 index 000000000..d047d518d --- /dev/null +++ b/django/cantusdb_project/main_app/admin/source.py @@ -0,0 +1,54 @@ +from django.contrib import admin + +from main_app.admin.base_admin import BaseModelAdmin, EXCLUDE, READ_ONLY +from main_app.forms import AdminSourceForm +from main_app.models import Source + + +@admin.register(Source) +class SourceAdmin(BaseModelAdmin): + exclude = EXCLUDE + ("source_status",) + + # These search fields are also available on the user-source inline relationship in the user admin page + search_fields = ( + "siglum", + "title", + "id", + ) + readonly_fields = READ_ONLY + ( + "number_of_chants", + "number_of_melodies", + "date_created", + "date_updated", + ) + # from the Django docs: + # Adding a ManyToManyField to this list will instead use a nifty unobtrusive JavaScript “filter” interface + # that allows searching within the options. The unselected and selected options appear in two boxes side by side. + filter_horizontal = ( + "century", + "notation", + "current_editors", + "inventoried_by", + "full_text_entered_by", + "melodies_entered_by", + "proofreaders", + "other_editors", + ) + + list_display = ( + "title", + "siglum", + "id", + ) + + list_filter = ( + "full_source", + "segment", + "source_status", + "published", + "century", + ) + + ordering = ("siglum",) + + form = AdminSourceForm diff --git a/django/cantusdb_project/main_app/forms.py b/django/cantusdb_project/main_app/forms.py index 100027609..41abb97b3 100644 --- a/django/cantusdb_project/main_app/forms.py +++ b/django/cantusdb_project/main_app/forms.py @@ -83,13 +83,14 @@ class Meta: "content_structure", "indexing_notes", "addendum", - "segment", - "liturgical_function", - "polyphony", - "cm_melody_id", - "incipit_of_refrain", - "later_addition", - "rubrics", + # See issue #1521: Temporarily commenting out segment-related functions on Chant + # "segment", + # "liturgical_function", + # "polyphony", + # "cm_melody_id", + # "incipit_of_refrain", + # "later_addition", + # "rubrics", ] # the widgets dictionary is ignored for a model field with a non-empty # choices attribute. In this case, you must override the form field to @@ -148,13 +149,14 @@ class Meta: "Mass Alleluias. Punctuation is omitted.", ) - segment = SelectWidgetNameModelChoiceField( - queryset=Segment.objects.all().order_by("id"), - required=True, - initial=Segment.objects.get(id=4063), # Default to the "Cantus" segment - help_text="Select the Database segment that the chant belongs to. " - "In most cases, this will be the CANTUS segment.", - ) + # See issue #1521: Temporarily commenting out segment-related functions on Chant + # segment = SelectWidgetNameModelChoiceField( + # queryset=Segment.objects.all().order_by("id"), + # required=True, + # initial=Segment.objects.get(id=4063), # Default to the "Cantus" segment + # help_text="Select the Database segment that the chant belongs to. " + # "In most cases, this will be the CANTUS segment.", + # ) # automatically computed fields # source and incipit are mandatory fields in model, @@ -281,13 +283,14 @@ class Meta: "manuscript_full_text_proofread", "volpiano_proofread", "proofread_by", - "segment", - "liturgical_function", - "polyphony", - "cm_melody_id", - "incipit_of_refrain", - "later_addition", - "rubrics", + # See issue #1521: Temporarily commenting out segment-related functions on Chant + # "segment", + # "liturgical_function", + # "polyphony", + # "cm_melody_id", + # "incipit_of_refrain", + # "later_addition", + # "rubrics", ] widgets = { # manuscript_full_text_std_spelling: defined below (required) @@ -317,12 +320,13 @@ class Meta: "proofread_by": autocomplete.ModelSelect2Multiple( url="proofread-by-autocomplete" ), - "polyphony": SelectWidget(), - "liturgical_function": SelectWidget(), - "cm_melody_id": TextInputWidget(), - "incipit_of_refrain": TextInputWidget(), - "later_addition": TextInputWidget(), - "rubrics": TextInputWidget(), + # See issue #1521: Temporarily commenting out segment-related functions on Chant + # "polyphony": SelectWidget(), + # "liturgical_function": SelectWidget(), + # "cm_melody_id": TextInputWidget(), + # "incipit_of_refrain": TextInputWidget(), + # "later_addition": TextInputWidget(), + # "rubrics": TextInputWidget(), } manuscript_full_text_std_spelling = forms.CharField( @@ -347,12 +351,13 @@ class Meta: help_text="Each folio starts with '1'.", ) - segment = SelectWidgetNameModelChoiceField( - queryset=Segment.objects.all().order_by("id"), - required=True, - help_text="Select the Database segment that the chant belongs to. " - "In most cases, this will be the CANTUS segment.", - ) + # See issue #1521: Temporarily commenting out segment-related functions on Chant + # segment = SelectWidgetNameModelChoiceField( + # queryset=Segment.objects.all().order_by("id"), + # required=True, + # help_text="Select the Database segment that the chant belongs to. " + # "In most cases, this will be the CANTUS segment.", + # ) class SourceEditForm(forms.ModelForm): diff --git a/django/cantusdb_project/main_app/identifiers.py b/django/cantusdb_project/main_app/identifiers.py new file mode 100644 index 000000000..4ab27e298 --- /dev/null +++ b/django/cantusdb_project/main_app/identifiers.py @@ -0,0 +1,26 @@ +class ExternalIdentifiers: + RISM = 1 + VIAF = 2 + WIKIDATA = 3 + GND = 4 + BNF = 5 + LC = 6 + + +IDENTIFIER_TYPES = ( + (ExternalIdentifiers.RISM, "RISM Online"), + (ExternalIdentifiers.VIAF, "VIAF"), + (ExternalIdentifiers.WIKIDATA, "Wikidata"), + (ExternalIdentifiers.GND, "GND (Gemeinsame Normdatei)"), + (ExternalIdentifiers.BNF, "Bibliothèque national de France"), + (ExternalIdentifiers.LC, "Library of Congress"), +) + +TYPE_PREFIX = { + ExternalIdentifiers.RISM: ("rism", "https://rism.online/"), + ExternalIdentifiers.VIAF: ("viaf", "https://viaf.org/viaf/"), + ExternalIdentifiers.WIKIDATA: ("wkp", "https://www.wikidata.org/wiki/"), + ExternalIdentifiers.GND: ("dnb", "https://d-nb.info/gnd/"), + ExternalIdentifiers.BNF: ("bnf", "https://catalogue.bnf.fr/ark:/12148/cb"), + ExternalIdentifiers.LC: ("lc", "https://id.loc.gov/authorities/"), +} diff --git a/django/cantusdb_project/main_app/migrations/0012_alter_source_date_alter_source_title.py b/django/cantusdb_project/main_app/migrations/0012_alter_source_date_alter_source_title.py new file mode 100644 index 000000000..7eae069bf --- /dev/null +++ b/django/cantusdb_project/main_app/migrations/0012_alter_source_date_alter_source_title.py @@ -0,0 +1,30 @@ +# Generated by Django 4.1.6 on 2024-06-06 12:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("main_app", "0011_chant_cm_melody_id_chant_incipit_of_refrain_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="source", + name="date", + field=models.CharField( + blank=True, + help_text='Date of the source (e.g. "1200s", "1300-1350", etc.)', + max_length=63, + null=True, + ), + ), + migrations.AlterField( + model_name="source", + name="title", + field=models.CharField( + help_text="Full Source Identification (City, Archive, Shelf-mark)", + max_length=255, + ), + ), + ] diff --git a/django/cantusdb_project/main_app/migrations/0013_institution.py b/django/cantusdb_project/main_app/migrations/0013_institution.py new file mode 100644 index 000000000..b206c5342 --- /dev/null +++ b/django/cantusdb_project/main_app/migrations/0013_institution.py @@ -0,0 +1,67 @@ +# Generated by Django 4.1.6 on 2024-06-06 12:06 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("main_app", "0012_alter_source_date_alter_source_title"), + ] + + operations = [ + migrations.CreateModel( + name="Institution", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "date_created", + models.DateTimeField( + auto_now_add=True, help_text="The date this entry was created" + ), + ), + ( + "date_updated", + models.DateTimeField( + auto_now=True, help_text="The date this entry was updated" + ), + ), + ("name", models.CharField(default="s.n.", max_length=255)), + ("siglum", models.CharField(default="XX-Nn", max_length=32)), + ("city", models.CharField(blank=True, max_length=64, null=True)), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_created_by_user", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "last_updated_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_last_updated_by_user", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/django/cantusdb_project/main_app/migrations/0014_institutionidentifier.py b/django/cantusdb_project/main_app/migrations/0014_institutionidentifier.py new file mode 100644 index 000000000..f544fd859 --- /dev/null +++ b/django/cantusdb_project/main_app/migrations/0014_institutionidentifier.py @@ -0,0 +1,92 @@ +# Generated by Django 4.1.6 on 2024-06-06 12:16 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("main_app", "0013_institution"), + ] + + operations = [ + migrations.CreateModel( + name="InstitutionIdentifier", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "date_created", + models.DateTimeField( + auto_now_add=True, help_text="The date this entry was created" + ), + ), + ( + "date_updated", + models.DateTimeField( + auto_now=True, help_text="The date this entry was updated" + ), + ), + ( + "identifier", + models.CharField( + help_text="Do not provide the full URL here; only the identifier.", + max_length=512, + ), + ), + ( + "identifier_type", + models.IntegerField( + choices=[ + (1, "RISM Online"), + (2, "VIAF"), + (3, "Wikidata"), + (4, "GND (Gemeinsame Normdatei)"), + (5, "Bibliothèque national de France"), + (6, "Library of Congress"), + ] + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_created_by_user", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "institution", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="identifiers", + to="main_app.institution", + ), + ), + ( + "last_updated_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_last_updated_by_user", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/django/cantusdb_project/main_app/migrations/0015_source_holding_institution.py b/django/cantusdb_project/main_app/migrations/0015_source_holding_institution.py new file mode 100644 index 000000000..2de78ae33 --- /dev/null +++ b/django/cantusdb_project/main_app/migrations/0015_source_holding_institution.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.11 on 2024-06-06 13:05 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("main_app", "0014_institutionidentifier"), + ] + + operations = [ + migrations.AddField( + model_name="source", + name="holding_institution", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="main_app.institution", + ), + ), + ] diff --git a/django/cantusdb_project/main_app/migrations/0016_institution_alternate_names_institution_region_squashed_0017_institution_country_alter_institution_city.py b/django/cantusdb_project/main_app/migrations/0016_institution_alternate_names_institution_region_squashed_0017_institution_country_alter_institution_city.py new file mode 100644 index 000000000..15e17ec59 --- /dev/null +++ b/django/cantusdb_project/main_app/migrations/0016_institution_alternate_names_institution_region_squashed_0017_institution_country_alter_institution_city.py @@ -0,0 +1,51 @@ +# Generated by Django 4.1.6 on 2024-06-11 09:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + replaces = [ + ("main_app", "0016_institution_alternate_names_institution_region"), + ("main_app", "0017_institution_country_alter_institution_city"), + ] + + dependencies = [ + ("main_app", "0015_source_holding_institution"), + ] + + operations = [ + migrations.AddField( + model_name="institution", + name="alternate_names", + field=models.TextField( + blank=True, + help_text="Enter alternate names on separate lines.", + null=True, + ), + ), + migrations.AddField( + model_name="institution", + name="region", + field=models.CharField( + blank=True, + help_text='Province / State / Canton / County. Used to disambiguate cities, e.g., "London (Ontario)".', + max_length=64, + null=True, + ), + ), + migrations.AddField( + model_name="institution", + name="country", + field=models.CharField(default="s.l.", max_length=64), + ), + migrations.AlterField( + model_name="institution", + name="city", + field=models.CharField( + blank=True, + help_text="City / Town / Village / Settlement", + max_length=64, + null=True, + ), + ), + ] diff --git a/django/cantusdb_project/main_app/migrations/0018_institution_former_sigla.py b/django/cantusdb_project/main_app/migrations/0018_institution_former_sigla.py new file mode 100644 index 000000000..cdd13afe9 --- /dev/null +++ b/django/cantusdb_project/main_app/migrations/0018_institution_former_sigla.py @@ -0,0 +1,22 @@ +# Generated by Django 4.1.6 on 2024-06-11 15:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "main_app", + "0016_institution_alternate_names_institution_region_squashed_0017_institution_country_alter_institution_city", + ), + ] + + operations = [ + migrations.AddField( + model_name="institution", + name="former_sigla", + field=models.TextField( + blank=True, help_text="Enter former sigla on separate lines.", null=True + ), + ), + ] diff --git a/django/cantusdb_project/main_app/models/__init__.py b/django/cantusdb_project/main_app/models/__init__.py index 67d135611..9959354e9 100644 --- a/django/cantusdb_project/main_app/models/__init__.py +++ b/django/cantusdb_project/main_app/models/__init__.py @@ -11,3 +11,5 @@ from main_app.models.sequence import Sequence from main_app.models.rism_siglum import RismSiglum from main_app.models.source import Source +from main_app.models.institution import Institution +from main_app.models.institution_identifier import InstitutionIdentifier diff --git a/django/cantusdb_project/main_app/models/institution.py b/django/cantusdb_project/main_app/models/institution.py new file mode 100644 index 000000000..02fd1f19e --- /dev/null +++ b/django/cantusdb_project/main_app/models/institution.py @@ -0,0 +1,29 @@ +from django.db import models + +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""" + + +class Institution(BaseModel): + name = models.CharField(max_length=255, default="s.n.") + siglum = models.CharField(max_length=32, default="XX-Nn") + city = models.CharField( + max_length=64, blank=True, null=True, help_text=city_help_text + ) + region = models.CharField( + max_length=64, blank=True, null=True, help_text=region_help_text + ) + country = models.CharField(max_length=64, default="s.l.") + alternate_names = models.TextField( + blank=True, null=True, help_text="Enter alternate names on separate lines." + ) + former_sigla = models.TextField( + blank=True, null=True, help_text="Enter former sigla on separate lines." + ) + + def __str__(self) -> str: + sigl: str = f"({self.siglum})" if self.siglum else "" + return f"{self.name} {sigl}" diff --git a/django/cantusdb_project/main_app/models/institution_identifier.py b/django/cantusdb_project/main_app/models/institution_identifier.py new file mode 100644 index 000000000..6e0c4df85 --- /dev/null +++ b/django/cantusdb_project/main_app/models/institution_identifier.py @@ -0,0 +1,33 @@ +from django.db import models + +from main_app.identifiers import IDENTIFIER_TYPES, TYPE_PREFIX +from main_app.models import BaseModel + + +class InstitutionIdentifier(BaseModel): + identifier = models.CharField( + max_length=512, + help_text="Do not provide the full URL here; only the identifier.", + ) + identifier_type = models.IntegerField(choices=IDENTIFIER_TYPES) + institution = models.ForeignKey( + "Institution", related_name="identifiers", on_delete=models.CASCADE + ) + + def __str__(self): + return f"{self.identifier_prefix}:{self.identifier}" + + @property + def identifier_label(self) -> str: + d: dict[int, str] = dict(IDENTIFIER_TYPES) + return d[self.identifier_type] + + @property + def identifier_prefix(self) -> str: + (pfx, _) = TYPE_PREFIX[self.identifier_type] + return pfx + + @property + def identifier_url(self) -> str: + (_, url) = TYPE_PREFIX[self.identifier_type] + return f"{url}{self.identifier}" diff --git a/django/cantusdb_project/main_app/models/source.py b/django/cantusdb_project/main_app/models/source.py index 6d0efe68b..e49363306 100644 --- a/django/cantusdb_project/main_app/models/source.py +++ b/django/cantusdb_project/main_app/models/source.py @@ -44,6 +44,12 @@ class Source(BaseModel): null=True, blank=True, ) + holding_institution = models.ForeignKey( + "Institution", + on_delete=models.PROTECT, + null=True, + blank=True, + ) provenance = models.ForeignKey( "Provenance", on_delete=models.PROTECT, diff --git a/django/cantusdb_project/main_app/templates/chant_create.html b/django/cantusdb_project/main_app/templates/chant_create.html index 39bb752cc..3e60159bc 100644 --- a/django/cantusdb_project/main_app/templates/chant_create.html +++ b/django/cantusdb_project/main_app/templates/chant_create.html @@ -84,7 +84,8 @@

Create Chant

{{ form.cantus_id }} -
+ +
@@ -140,10 +141,11 @@

Create Chant

{{ form.extra }}
-
+ +
@@ -179,8 +181,9 @@

Create Chant

+ -