diff --git a/config/nginx/conf.d/cantusdb.conf.development b/config/nginx/conf.d/cantusdb.conf.development index 034e62f7c..51e2f12ce 100644 --- a/config/nginx/conf.d/cantusdb.conf.development +++ b/config/nginx/conf.d/cantusdb.conf.development @@ -1,41 +1,9 @@ # This file is configured for deployment of the CantusDB project in local development. # When building the project locally, replace the contents of cantusdb.conf with the # contents of this file. - -# server { -# listen 80; -# -# server_tokens off; -# -# location ^~ /.well-known/acme-challenge/ { -# root /var/www/lego; -# } -# -# location / { -# return 301 https://$host$request_uri; -# } -# } - -# server { -# # Redirect all https traffic for mass.cantusdatabase.org to cantusdatabase.org -# listen 443 ssl; -# -# server_name mass.cantusdatabase.org; -# -# ssl_certificate /etc/nginx/ssl/live/certificates/cantusdatabase.org.crt; -# ssl_certificate_key /etc/nginx/ssl/live/certificates/cantusdatabase.org.key; -# -# location / { -# return 301 https://cantusdatabase.org$request_uri; -# } -# } server { - # listen 443 default_server http2 ssl; listen 80; - - # ssl_certificate /etc/nginx/ssl/live/certificates/cantusdatabase.org.crt; - # ssl_certificate_key /etc/nginx/ssl/live/certificates/cantusdatabase.org.key; location / { proxy_pass http://django:8000; diff --git a/config/nginx/conf.d/cantusdb.conf.production b/config/nginx/conf.d/cantusdb.conf.production index b109ffedf..ec1107ede 100644 --- a/config/nginx/conf.d/cantusdb.conf.production +++ b/config/nginx/conf.d/cantusdb.conf.production @@ -31,10 +31,10 @@ server { } server { - listen 443 default_server http2 ssl; + listen 443 http2 ssl; server_name cantusdatabase.org; - + ssl_certificate /etc/nginx/ssl/live/certificates/cantusdatabase.org.crt; ssl_certificate_key /etc/nginx/ssl/live/certificates/cantusdatabase.org.key; @@ -56,7 +56,7 @@ server { alias /resources/api_cache/concordances.json; expires modified +24h; } - + location = /style.css { root /; } @@ -79,3 +79,18 @@ server { root /; } } + +# Default server to block requests to IP address and non-valid hostnames +server { + access_log off; + server_name _; + + listen 80 default_server; + listen 443 default_server; + ssl_reject_handshake on; + + # Close connection immediately for requests that don't match a defined + # server name + return 444; +} + diff --git a/config/nginx/conf.d/cantusdb.conf.staging b/config/nginx/conf.d/cantusdb.conf.staging index cf3e59a13..7377880f0 100644 --- a/config/nginx/conf.d/cantusdb.conf.staging +++ b/config/nginx/conf.d/cantusdb.conf.staging @@ -21,11 +21,8 @@ server { # The Staging server's subdomain "staging-alias" is analogous to Production's "mass" subdomain.) listen 443 ssl; - # server_name mass.cantusdatabase.org; server_name staging-alias.cantusdatabase.org; - # ssl_certificate /etc/nginx/ssl/live/certificates/cantusdatabase.org.crt; - # ssl_certificate_key /etc/nginx/ssl/live/certificates/cantusdatabase.org.key; ssl_certificate /etc/nginx/ssl/live/certificates/staging.cantusdatabase.org.crt; ssl_certificate_key /etc/nginx/ssl/live/certificates/staging.cantusdatabase.org.key; @@ -35,12 +32,10 @@ server { } server { - listen 443 default_server http2 ssl; + listen 443 http2 ssl; server_name staging.cantusdatabase.org; - - # ssl_certificate /etc/nginx/ssl/live/certificates/cantusdatabase.org.crt; - # ssl_certificate_key /etc/nginx/ssl/live/certificates/cantusdatabase.org.key; + ssl_certificate /etc/nginx/ssl/live/certificates/staging.cantusdatabase.org.crt; ssl_certificate_key /etc/nginx/ssl/live/certificates/staging.cantusdatabase.org.key; @@ -62,7 +57,7 @@ server { alias /resources/api_cache/concordances.json; expires modified +24h; } - + location = /style.css { root /; } @@ -85,3 +80,17 @@ server { root /; } } + +# Default server to block requests to IP address and non-valid hostnames +server { + access_log off; + server_name _; + + listen 80 default_server; + listen 443 default_server; + ssl_reject_handshake on; + + # Close connection immediately for requests that don't match a defined + # server name + return 444; +} diff --git a/django/cantusdb_project/main_app/admin.py b/django/cantusdb_project/main_app/admin.py index 45cff5f78..e23b07503 100644 --- a/django/cantusdb_project/main_app/admin.py +++ b/django/cantusdb_project/main_app/admin.py @@ -21,6 +21,8 @@ READ_ONLY = ( "created_by", "last_updated_by", + "date_created", + "date_updated", ) @@ -61,10 +63,7 @@ def get_source_siglum(self, obj): "id", ) - readonly_fields = READ_ONLY + ( - "date_created", - "date_updated", - ) + readonly_fields = READ_ONLY + ("incipit",) list_filter = ( "genre", @@ -170,6 +169,7 @@ def get_source_siglum(self, obj): "source", "feast", ) + readonly_fields = READ_ONLY + ("incipit",) ordering = ("source__siglum",) form = AdminSequenceForm diff --git a/django/cantusdb_project/main_app/signals.py b/django/cantusdb_project/main_app/signals.py index d5def7b75..a9879a4cd 100644 --- a/django/cantusdb_project/main_app/signals.py +++ b/django/cantusdb_project/main_app/signals.py @@ -7,6 +7,8 @@ from django.db.models.signals import post_save, post_delete from django.dispatch import receiver +from typing import Optional + import re from main_app.models import Chant @@ -16,36 +18,38 @@ @receiver(post_save, sender=Chant) -def on_chant_save(instance, **kwargs): +def on_chant_save(instance, **kwargs) -> None: update_source_chant_count(instance) update_source_melody_count(instance) update_chant_search_vector(instance) + update_chant_incipit_field(instance) update_volpiano_fields(instance) @receiver(post_delete, sender=Chant) -def on_chant_delete(instance, **kwargs): +def on_chant_delete(instance, **kwargs) -> None: update_source_chant_count(instance) update_source_melody_count(instance) @receiver(post_save, sender=Sequence) -def on_sequence_save(instance, **kwargs): +def on_sequence_save(instance, **kwargs) -> None: update_source_chant_count(instance) + update_sequence_incipit_field(instance) @receiver(post_delete, sender=Sequence) -def on_sequence_delete(instance, **kwargs): +def on_sequence_delete(instance, **kwargs) -> None: update_source_chant_count(instance) @receiver(post_save, sender=Feast) -def on_feast_save(instance, **kwargs): +def on_feast_save(instance, **kwargs) -> None: update_prefix_field(instance) -def update_chant_search_vector(instance): +def update_chant_search_vector(instance) -> None: """When saving an instance of Chant, update its search vector field. Called in on_chant_save() @@ -63,7 +67,7 @@ def update_chant_search_vector(instance): ) -def update_source_chant_count(instance): +def update_source_chant_count(instance) -> None: """When saving or deleting a Chant or Sequence, update its Source's number_of_chants field Called in on_chant_save(), on_chant_delete(), on_sequence_save() and on_sequence_delete() @@ -79,7 +83,7 @@ def update_source_chant_count(instance): source.save() -def update_source_melody_count(instance): +def update_source_melody_count(instance) -> None: """When saving or deleting a Chant, update its Source's number_of_melodies field Called in on_chant_save() and on_chant_delete() @@ -99,88 +103,12 @@ def update_source_melody_count(instance): source.save() -def update_volpiano_fields(instance): +def update_volpiano_fields(instance) -> None: """When saving a Chant, make sure the chant's volpiano_notes and volpiano_intervals are up-to-date Called in on_chant_save() """ - def generate_volpiano_notes(volpiano): - """ - Populate the ``volpiano_notes`` field of the ``Chant`` model - - This field is used for melody search - - Args: - volpiano (str): The content of ``chant.volpiano`` - - Returns: - str: Volpiano str with non-note chars and duplicate consecutive notes removed - """ - # unwanted_chars are non-note chars, including the clefs, barlines, and accidentals etc. - # the `searchMelody.js` on old cantus makes no reference to the b-flat accidentals ("y", "i", "z") - # so put them in unwanted chars for now - unwanted_chars = [ - "-", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "?", - ".", - " ", - "y", - "i", - "z", - ] - # convert all charactors to lower-case, upper-case letters stand for liquescent of the same pitch - volpiano_lower = volpiano.lower() - # `)` stands for the lowest `g` note liquescent in volpiano, its 'lower case' is `9` - volpiano_notes = volpiano_lower.replace(")", "9") - # remove none-note charactors - for unwanted_char in unwanted_chars: - volpiano_notes = volpiano_notes.replace(unwanted_char, "") - # remove duplicate consecutive chars - volpiano_notes = re.sub(r"(.)\1+", r"\1", volpiano_notes) - return volpiano_notes - - def generate_volpiano_intervals(volpiano_notes): - """ - Populate the ``volpiano_intervals`` field of the ``Chant`` model - - This field is used for melody search when searching for transpositions - - Args: - volpiano_notes (str): The content of ``chant.volpiano_notes``, - populated by the ``generate_volpiano_notes`` function - - Returns: - str: A str of digits, recording the intervals between adjacent notes - """ - # replace '9' (the note G) with the char corresponding to (ASCII(a) - 1), because 'a' denotes the note A - volpiano_notes = volpiano_notes.replace("9", chr(ord("a") - 1)) - # we model the interval between notes using the difference between the ASCII codes of corresponding letters - # the letter for the note B is "j" (106), note A is "h" (104), the letter "i" (105) is skipped - # move all notes above A down by one letter - volpiano_notes = list(volpiano_notes) - for j, note in enumerate(volpiano_notes): - if ord(note) >= 106: - volpiano_notes[j] = chr(ord(note) - 1) - - # `intervals` records the difference between two adjacent notes. - # Note that intervals are encoded by counting the number of scale - # steps between adjacent notes: an ascending second is thus encoded - # as "1"; a descending third is encoded "-2", and so on. - intervals = [] - for j in range(1, len(volpiano_notes)): - intervals.append(ord(volpiano_notes[j]) - ord(volpiano_notes[j - 1])) - # convert `intervals` to str - volpiano_intervals = "".join([str(interval) for interval in intervals]) - return volpiano_intervals - if instance.volpiano is None: return @@ -193,7 +121,84 @@ def generate_volpiano_intervals(volpiano_notes): ) -def update_prefix_field(instance): +def generate_volpiano_notes(volpiano) -> str: + """ + Populate the ``volpiano_notes`` field of the ``Chant`` model + + This field is used for melody search + + Args: + volpiano (str): The content of ``chant.volpiano`` + + Returns: + str: Volpiano str with non-note chars and duplicate consecutive notes removed + """ + # unwanted_chars are non-note chars, including the clefs, barlines, and accidentals etc. + # the `searchMelody.js` on old cantus makes no reference to the b-flat accidentals ("y", "i", "z") + # so put them in unwanted chars for now + unwanted_chars: list[str] = [ + "-", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "?", + ".", + " ", + "y", + "i", + "z", + ] + # convert all charactors to lower-case, upper-case letters stand for liquescent of the same pitch + volpiano_lower: str = volpiano.lower() + # `)` stands for the lowest `g` note liquescent in volpiano, its 'lower case' is `9` + volpiano_notes: str = volpiano_lower.replace(")", "9") + # remove none-note charactors + for unwanted_char in unwanted_chars: + volpiano_notes = volpiano_notes.replace(unwanted_char, "") + # remove duplicate consecutive chars + volpiano_notes = re.sub(r"(.)\1+", r"\1", volpiano_notes) + return volpiano_notes + + +def generate_volpiano_intervals(volpiano_notes) -> str: + """ + Populate the ``volpiano_intervals`` field of the ``Chant`` model + + This field is used for melody search when searching for transpositions + + Args: + volpiano_notes (str): The content of ``chant.volpiano_notes``, + populated by the ``generate_volpiano_notes`` function + + Returns: + str: A str of digits, recording the intervals between adjacent notes + """ + # replace '9' (the note G) with the char corresponding to (ASCII(a) - 1), because 'a' denotes the note A + volpiano_notes: str = volpiano_notes.replace("9", chr(ord("a") - 1)) + # we model the interval between notes using the difference between the ASCII codes of corresponding letters + # the letter for the note B is "j" (106), note A is "h" (104), the letter "i" (105) is skipped + # move all notes above A down by one letter + notes_list: list = list(volpiano_notes) + for j, note in enumerate(notes_list): + if ord(note) >= 106: + notes_list[j] = chr(ord(note) - 1) + + # `intervals` records the difference between two adjacent notes. + # Note that intervals are encoded by counting the number of scale + # steps between adjacent notes: an ascending second is thus encoded + # as "1"; a descending third is encoded "-2", and so on. + intervals: list[int] = [] + for j in range(1, len(notes_list)): + intervals.append(ord(notes_list[j]) - ord(notes_list[j - 1])) + volpiano_intervals: str = "".join([str(interval) for interval in intervals]) + return volpiano_intervals + + +def update_prefix_field(instance) -> None: pk = instance.pk if instance.feast_code: @@ -201,3 +206,53 @@ def update_prefix_field(instance): instance.__class__.objects.filter(pk=pk).update(prefix=prefix) else: # feast_code is None, "" instance.__class__.objects.filter(pk=pk).update(prefix="") + + +def update_chant_incipit_field(chant: Chant) -> None: + """Update the incipit field of the specified Chant to be the first + several words of the chant's standardized-spelling fulltext + + Args: + chant (Chant): The chant from the database whose `incipit` field + is to be updated + """ + fulltext: Optional[str] = chant.manuscript_full_text_std_spelling + if fulltext: # many chants in the database have only an incipit - + # we should only update the incipit if the chant has a fulltext, + # just in case a chant manages to get saved without a fulltext somehow + new_incipit: str = generate_incipit(fulltext) + Chant.objects.filter(id=chant.id).update(incipit=new_incipit) + + +def update_sequence_incipit_field(sequence: Sequence) -> None: + """Update the incipit field of the specified Sequence to be the first + several words of the sequence's standardized-spelling fulltext + + Args: + sequence (Sequence): The sequence from the database whose `incipit` + field is to be updated + """ + title: Optional[str] = sequence.title + if title: # As of late Feb 2024, no sequences in the database have + # fulltext, but every sequence has a title, and the value stored in + # the title field is an incipit. + incipit: str = title + Sequence.objects.filter(id=sequence.id).update(incipit=incipit) + + +def generate_incipit(fulltext: str) -> str: + """Given the fulltext of a chant or sequence, generate an incipit + consisting of the first 5 words of the fulltext. + + Args: + fulltext (str): the full text of a chant or sequence + + Returns: + str: an incipit - the first five words of the fulltext + """ + INCIPIT_LENGTH: int = 5 # number of words to include in the new incipit + + fulltext_words: list[str] = fulltext.split(" ") + incipit_words: list[str] = fulltext_words[:INCIPIT_LENGTH] + incipit: str = " ".join(incipit_words) + return incipit diff --git a/django/cantusdb_project/main_app/templates/400.html b/django/cantusdb_project/main_app/templates/400.html index ac1d64bf8..3ee52993d 100644 --- a/django/cantusdb_project/main_app/templates/400.html +++ b/django/cantusdb_project/main_app/templates/400.html @@ -25,10 +25,11 @@ {% block content %}
Your request couldn't be understood by the server.
+ We couldn't find a page at this address. +
Title / Incipit / Name | -Type | -Creation Date | -Creator | -Last Updated Date | -Last Updated By | -Operations | -
---|
- {{ object.title|truncatechars:30 }} - | - {% elif object.incipit %} -- {{ object.incipit|truncatechars:30 }} - | - {% elif object.name %} +Title / Incipit / Name | +Type | +Creation Date | +Creator | +Last Updated Date | +Last Updated By | +Operations | +||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
+ {{ object.title|truncatechars:30 }} + | + {% elif object.incipit %} ++ {{ object.incipit|truncatechars:30 }} + | + {% elif object.name %} ++ {% if object|classname == "Notation" or object|classname == "Segment" or object|classname == "RismSiglum" %} + {{ object.name|truncatechars:30 }} + {% else %} + {{ object.name|truncatechars:30 }} + {% endif %} + | + {% elif object.full_name %} ++ {{ object.full_name }} + | + {% else %} ++ {{ object|classname }} Object + | + {% endif %} +{{ object|classname }} | +{{ object.date_created|date:'Y-m-d H:i' }} | - {% if object|classname == "Notation" or object|classname == "Segment" or object|classname == "RismSiglum" %} - {{ object.name|truncatechars:30 }} + {% if object.created_by is None %} + {{ object.created_by }} {% else %} - {{ object.name|truncatechars:30 }} + + {{ object.created_by }} + {% endif %} | - {% elif object.full_name %} +{{ object.date_updated|date:'Y-m-d H:i' }} | - {{ object.full_name }} + {% if object.last_updated_by is None %} + {{ object.last_updated_by }} + {% else %} + + {{ object.last_updated_by }} + + {% endif %} | - {% else %}- {{ object|classname }} Object + {% with class=object|classname %} + Edit + | + Delete + {% endwith %} | - {% endif %} -{{ object|classname }} | -{{ object.date_created|date:'Y-m-d H:i' }} | -- {% if object.created_by is None %} - {{ object.created_by }} - {% else %} - - {{ object.created_by }} - - {% endif %} - | -{{ object.date_updated|date:'Y-m-d H:i' }} | -- {% if object.last_updated_by is None %} - {{ object.last_updated_by }} - {% else %} - - {{ object.last_updated_by }} - - {% endif %} - | -- {% with class=object|classname %} - Edit - | - Delete - {% endwith %} - | -
Your password has been set. You may go ahead and log in now.
diff --git a/django/cantusdb_project/main_app/templates/registration/reset_password_confirm.html b/django/cantusdb_project/main_app/templates/registration/reset_password_confirm.html index a68933226..f46d59567 100644 --- a/django/cantusdb_project/main_app/templates/registration/reset_password_confirm.html +++ b/django/cantusdb_project/main_app/templates/registration/reset_password_confirm.html @@ -4,7 +4,7 @@Please enter your new password twice so we can verify you typed it in correctly.