Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge develop into staging, 13 Aug 2024 #1595

Merged
merged 65 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
8aac174
management: add atomic transation to populate diff_id fields
lucasmarchd01 Jun 5, 2024
ae31659
management: include iterator for chant queryset
lucasmarchd01 Jun 5, 2024
364a483
build(deps): bump urllib3 from 2.2.1 to 2.2.2
dependabot[bot] Jun 17, 2024
fdfeacb
build(deps): bump django from 4.2.11 to 4.2.14
dependabot[bot] Jul 10, 2024
13f8240
docs(acknowledgements): add logos for legacy organizations
lucasmarchd01 Jul 18, 2024
d2c2ed7
refactor(views): separate autocomplete views from views.views
dchiller Jul 16, 2024
0600305
refactor(views): move change_password view to views.auth
dchiller Jul 16, 2024
f677e76
refactor(views): move redirect views to views.redirect
dchiller Jul 16, 2024
ecd4f2a
refactor(views): create site_stats views
dchiller Jul 23, 2024
6847a84
refactor(views): move contact flatpage to views.contact
dchiller Jul 23, 2024
2094ec3
refactor(views): move api views to views.api
dchiller Jul 23, 2024
6ad61db
refactor(urls): change urls.py import strategy
dchiller Jul 23, 2024
ad35080
refactor(views): fix conflicts in views.api and views.redirects
dchiller Jul 23, 2024
46619d6
feat(chant detail): Display polyphony and function fields when existing
dchiller Jul 23, 2024
caffa0d
feat(chant detail): display Benedicamus Domino fields where available
dchiller Jul 24, 2024
dbd2975
refactor(chant detail): remove duplicate query by get_object
dchiller Jul 24, 2024
f709567
Merge pull request #1573 from dchiller/chant-detail-display-fields
dchiller Jul 24, 2024
7e905f1
docs(acknowledgements): invert CUA logo text
lucasmarchd01 Jul 24, 2024
8289abb
Merge pull request #1574 from lucasmarchd01/issue-1406
lucasmarchd01 Jul 24, 2024
c121369
refactor(permissions): move project_manager_check to permissions.py
dchiller Jul 24, 2024
c0202c4
refactor(chant detail): fix typo in differentiae database
lucasmarchd01 Jul 25, 2024
eac997b
style(black): black formatting changes
lucasmarchd01 Jul 25, 2024
77e26b9
docs(differentia): fix type in comment
lucasmarchd01 Jul 25, 2024
6995beb
Merge pull request #1575 from lucasmarchd01/issue-1371
lucasmarchd01 Jul 25, 2024
c447fb1
Merge pull request #1563 from dchiller/split-views-files
dchiller Jul 25, 2024
227c08b
feat(chant search): add indexing notes search functionality
lucasmarchd01 Jul 29, 2024
967f92c
feat(chant search): add indexing notes search functionality
lucasmarchd01 Jul 29, 2024
0d43fa6
Merge branch 'issue-843' of https://github.com/lucasmarchd01/CantusDB…
lucasmarchd01 Jul 29, 2024
f91934e
test(chant search): add tests for indexing notes search
lucasmarchd01 Jul 29, 2024
cbed5fe
feat(source model): add temporary segment field to source
dchiller Jul 29, 2024
b61c198
Merge pull request #1559 from DDMAL/dependabot/pip/django-4.2.14
dchiller Jul 29, 2024
65c83c2
Merge pull request #1543 from DDMAL/dependabot/pip/urllib3-2.2.2
dchiller Jul 29, 2024
da114cb
fix(docker-compose): Remove obsolete version line
dchiller Jul 29, 2024
02e275b
feat(browse sources): Add country and source columns to table
dchiller Jul 29, 2024
55e5903
Merge pull request #1578 from dchiller/i1569-browse-sources-table
dchiller Jul 30, 2024
0cf9be0
Merge pull request #1576 from lucasmarchd01/issue-843
lucasmarchd01 Jul 30, 2024
12f5202
fix(source forms): Show name without ID for segment field on source f…
dchiller Jul 30, 2024
449dd97
refactor(source edit view): remove duplicate object query
dchiller Jul 30, 2024
540cb3e
Merge pull request #1577 from dchiller/i1549-segment-field
dchiller Jul 30, 2024
671deea
build(deps): add django-extensions and werkzeug to dev dependencies
dchiller Aug 5, 2024
0f3257b
feat(dev server): user runserver_plus in docker-compose-development
dchiller Aug 5, 2024
fa4e8bf
build(deps): add types-requests dev dependency
dchiller Aug 5, 2024
de20954
feat(chant create): use fixed nextchants API for suggested chants fea…
dchiller Aug 6, 2024
b3f2fbd
feat(text functions): update test_functions for new suggested chants …
dchiller Aug 6, 2024
e7cdd79
fix(test_views): change expected number of suggested chants in Chant …
dchiller Aug 6, 2024
f0c1e4d
fix(gh action): add docker-compose-test-runner for github action djan…
dchiller Aug 6, 2024
e7b98cc
Merge branch 'develop' into issue-1368-3
lucasmarchd01 Aug 6, 2024
390cb57
fix(management): ensure Differentia is created if not found
lucasmarchd01 Aug 6, 2024
6d62fe9
refactor(services): update URLs, models, codebase, and admin pages to…
lucasmarchd01 Aug 7, 2024
f0e5ce6
Merge pull request #1580 from lucasmarchd01/issue-1368-3
lucasmarchd01 Aug 7, 2024
3e8f70c
Merge pull request #1579 from dchiller/optimize-suggested-chants
dchiller Aug 7, 2024
443f8ca
New: Add source key filter to chants
ahankinson Aug 8, 2024
4e1064e
New: Add views for institutions
ahankinson Aug 8, 2024
1889021
fix(redirects): add permanent=True to redirects
lucasmarchd01 Aug 8, 2024
9349505
Merge pull request #1582 from lucasmarchd01/issue-1581
lucasmarchd01 Aug 8, 2024
65f9d4f
Merge pull request #1584 from DDMAL/add-source-key-filter
dchiller Aug 8, 2024
40391a9
Merge branch 'develop' into add-institution-views
ahankinson Aug 9, 2024
fb455c6
Merge pull request #1586 from DDMAL/add-institution-views
dchiller Aug 9, 2024
09c5749
feat(templates): add sortable_header template tag
dchiller Aug 9, 2024
c36febf
feat(source list): add sorting by country and source columns
dchiller Aug 9, 2024
a206538
refactor(source list view): minor source list view refactoring
dchiller Aug 9, 2024
ee12c96
Merge pull request #1592 from dchiller/i1217-source-list-sortable-col…
dchiller Aug 13, 2024
0c687f5
fix(migrations): add missing migration file
lucasmarchd01 Aug 13, 2024
2803e64
style(black): black formatting changes
lucasmarchd01 Aug 13, 2024
5ae61a9
Merge pull request #1596 from lucasmarchd01/add-migration-28
lucasmarchd01 Aug 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/django_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ jobs:
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
- run: docker compose -f docker-compose-test-runner.yml build
- run: docker compose -f docker-compose-test-runner.yml up -d
- run: docker compose -f docker-compose-test-runner.yml exec -T django python manage.py test main_app.tests
1 change: 1 addition & 0 deletions django/cantusdb_project/cantusdb/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,5 +208,6 @@

if DEBUG:
INSTALLED_APPS.append("debug_toolbar")
INSTALLED_APPS.append("django_extensions")
# debug toolbar must be inserted as early in the middleware as possible
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
173 changes: 81 additions & 92 deletions django/cantusdb_project/cantusindex.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""

import json
from typing import Optional, Union, Callable
from typing import Optional, Union, Callable, TypedDict, Any

import requests
from requests.exceptions import SSLError, Timeout, HTTPError
Expand All @@ -14,122 +14,106 @@
CANTUS_INDEX_DOMAIN: str = "https://cantusindex.uwaterloo.ca"
OLD_CANTUS_INDEX_DOMAIN: str = "https://cantusindex.org"
DEFAULT_TIMEOUT: float = 2 # seconds
NUMBER_OF_SUGGESTED_CHANTS: int = 3 # this number can't be too large,
# since for each suggested chant, we make a request to Cantus Index.
# We haven't yet parallelized this process, so setting this number
# too high will cause the Chant Create page to take a very long time
# to load. If/when we parallelize this process, we want to limit
# the size of the burst of requests sent to CantusIndex.
NUMBER_OF_SUGGESTED_CHANTS: int = 5 # default number of suggested chants to return
# with the get_suggested_chants function


class SuggestedChant(TypedDict):
"""
Dictionary containing information required for
the suggested chants feature on the Chant Create form.
"""

cantus_id: str
occurrences: int
fulltext: Optional[str]
genre_name: Optional[str]
genre_id: Optional[int]


def get_suggested_chants(
cantus_id: str, number_of_suggestions: int = NUMBER_OF_SUGGESTED_CHANTS
) -> Optional[list[dict]]:
) -> Optional[list[SuggestedChant]]:
"""
Given a Cantus ID, query Cantus Index's /nextchants API for a list of
Cantus IDs that follow the given Cantus ID in existing manuscripts.
Sort the list by the number of occurrences of each Cantus ID, and return
a list of dictionaries containing information about the suggested Cantus IDs
with the highest number of occurrences.

Args:
cantus_id (str): a Cantus ID
number_of_suggestions (int): the number of suggested Cantus IDs to return

Returns:
Optional[list[dict]]: A list of dictionaries, each containing information
about a suggested Cantus ID:
- "cantus_id": the suggested Cantus ID
- "occurrences": the number of times the suggested Cantus ID follows
the given Cantus ID in existing manuscripts
- "fulltext": the full text of the suggested Cantus ID
- "genre_name": the genre of the suggested Cantus ID
- "genre_id": the ID of the genre of the suggested Cantus ID
If no suggestions are available, returns None.
"""
endpoint_path: str = f"/json-nextchants/{cantus_id}"
all_suggestions: Union[list, dict, None] = get_json_from_ci_api(endpoint_path)
all_suggestions = get_json_from_ci_api(endpoint_path)

if not isinstance(all_suggestions, list):
# get_json_from_ci_api timed out
# or CI returned a response with no suggestions.
if all_suggestions is None:
return None

# when Cantus ID doesn't exist within CI, CI's api returns a 200 response with `['Cantus ID is not valid']`
# when Cantus ID doesn't exist within CI, CI's api returns a
# 200 response with `['Cantus ID is not valid']`
first_suggestion = all_suggestions[0]
if not isinstance(first_suggestion, dict):
return None

sort_by_occurrences: Callable[[dict], int] = lambda suggestion: int(
sort_by_occurrences: Callable[[dict[Any, Any]], int] = lambda suggestion: int(
suggestion["count"]
)
sorted_suggestions: list = sorted(
sorted_suggestions: list[dict[Any, Any]] = sorted(
all_suggestions, key=sort_by_occurrences, reverse=True
)
trimmed_suggestions: list = sorted_suggestions[:number_of_suggestions]
trimmed_suggestions = sorted_suggestions[:number_of_suggestions]

suggested_chants: list[Optional[dict]] = []
suggested_chants: list[SuggestedChant] = []
for suggestion in trimmed_suggestions:
cantus_id: str = suggestion["cid"]
occurrences: int = int(suggestion["count"])
suggested_chants.append(get_suggested_chant(cantus_id, occurrences))

# filter out Cantus IDs where get_suggested_chant timed out
filtered_suggestions: list[dict] = [
sugg for sugg in suggested_chants if sugg is not None
]

return filtered_suggestions


def get_suggested_chant(
cantus_id: str, occurrences: int, timeout: float = DEFAULT_TIMEOUT
) -> Optional[dict]:
"""Given a Cantus ID and a number of occurrences, query one of Cantus Index's
APIs for information on that Cantus ID and return a dictionary
containing a full text, an incipit, the ID of that Cantus ID's genre, and
the number of occurrences for that Cantus ID

(Number of occurrences: this function is used on the Chant Create page,
to suggest Cantus IDs of chants that might follow a chant with the Cantus ID
of the most recently created chant within the current source. Number of occurrences
is provided by Cantus Index's /nextchants API, based on which chants follow which
other chants in existing manuscripts)

Args:
cantus_id (str): a Cantus ID
occurrences (int): the number of times chants with this Cantus ID follow chants
with the Cantus ID of the most recently created chant.

Returns:
Optional[dict]: A dictionary with the following keys:
- "cantus_id"
- "occurrences"
- "fulltext"
- "incipit"
- "genre_id"
...but if get_json_from_ci_api timed out, returns None instead
"""
endpoint_path: str = f"/json-cid/{cantus_id}"
json: Union[dict, list, None] = get_json_from_ci_api(endpoint_path, timeout=timeout)

if not isinstance(json, dict):
# mostly, in case of a timeout within get_json_from_ci_api
return None
sugg_cantus_id = suggestion["cid"]
occurences = int(suggestion["count"])
suggestion_info = suggestion.get("info")
if suggestion_info:
fulltext = suggestion_info.get("field_full_text")
genre_name = suggestion_info.get("field_genre")
else:
fulltext = None
genre_name = None
try:
genre_id = Genre.objects.get(name=genre_name).id
except Genre.DoesNotExist:
genre_id = None
suggested_chants.append(
{
"cantus_id": sugg_cantus_id,
"occurrences": occurences,
"fulltext": fulltext,
"genre_name": genre_name,
"genre_id": genre_id,
}
)

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
except Genre.DoesNotExist:
pass

clean_cantus_id = cantus_id.replace(".", "d").replace(":", "c")
# "d"ot "c"olon
return {
"cantus_id": cantus_id,
"occurrences": occurrences,
"fulltext": fulltext,
"incipit": incipit,
"genre_name": genre_name,
"genre_id": genre_id,
"clean_cantus_id": clean_cantus_id,
}
return suggested_chants


def get_suggested_fulltext(cantus_id: str) -> Optional[str]:
endpoint_path: str = f"/json-cid/{cantus_id}"
json: Union[dict, list, None] = get_json_from_ci_api(endpoint_path)
json_response: Union[dict, list, None] = get_json_from_ci_api(endpoint_path)

if not isinstance(json, dict):
if not isinstance(json_response, dict):
# mostly, in case of a timeout within get_json_from_ci_api
return None

try:
suggested_fulltext = json["info"]["field_full_text"]
suggested_fulltext = json_response["info"]["field_full_text"]
except KeyError:
return None

Expand Down Expand Up @@ -207,7 +191,7 @@ def get_ci_text_search(search_term: str) -> Optional[list[Optional[dict]]]:

def get_json_from_ci_api(
path: str, timeout: float = DEFAULT_TIMEOUT
) -> Union[dict, list, None]:
) -> Union[dict[Any, Any], list[Any], None]:
"""Given a path, send a request to Cantus Index at that path,
decode the response to remove its Byte Order Marker, parse it,
and return it as a dictionary or list.
Expand All @@ -221,7 +205,7 @@ def get_json_from_ci_api(
Union[dict, list, None]:
If the JSON returned from Cantus Index is a JSON object, returns a dict.
If the JSON returned is a JSON array, returns a list.
In case the request times out, returns None.
If the request times out, or other types are returned, returns None.
"""

if not path.startswith("/"):
Expand All @@ -243,4 +227,9 @@ def get_json_from_ci_api(
# there are no suggested chants
return None

return response.json()
parsed_response = response.json()

if not isinstance(parsed_response, (dict, list)):
return None

return parsed_response
2 changes: 1 addition & 1 deletion django/cantusdb_project/main_app/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
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.service import ServiceAdmin
from main_app.admin.provenance import ProvenanceAdmin
from main_app.admin.segment import SegmentAdmin
from main_app.admin.sequence import SequenceAdmin
Expand Down
16 changes: 14 additions & 2 deletions django/cantusdb_project/main_app/admin/chant.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
from django.contrib import admin

from main_app.admin.base_admin import EXCLUDE, READ_ONLY, BaseModelAdmin
from main_app.admin.filters import InputFilter
from main_app.forms import AdminChantForm
from main_app.models import Chant


class SourceKeyFilter(InputFilter):
parameter_name = "source_id"
title = "Source ID"

def queryset(self, request, queryset):
if self.value():
return queryset.filter(source_id=self.value())


@admin.register(Chant)
class ChantAdmin(BaseModelAdmin):

def get_queryset(self, request):
return (
super()
.get_queryset(request)
.select_related("source__holding_institution", "genre", "office")
.select_related("source__holding_institution", "genre", "service")
)

@admin.display(description="Source Siglum")
Expand All @@ -30,13 +40,15 @@ def get_source_siglum(self, obj):
"incipit",
"cantus_id",
"id",
"source__holding_institution__siglum",
)

readonly_fields = READ_ONLY + ("incipit",)

list_filter = (
SourceKeyFilter,
"genre",
"office",
"service",
)
exclude = EXCLUDE + (
"col1",
Expand Down
11 changes: 0 additions & 11 deletions django/cantusdb_project/main_app/admin/office.py

This file was deleted.

4 changes: 2 additions & 2 deletions django/cantusdb_project/main_app/admin/sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def get_queryset(self, request):
return (
super()
.get_queryset(request)
.select_related("source__holding_institution", "genre", "office")
.select_related("source__holding_institution", "genre", "service")
)

@admin.display(description="Source Siglum")
Expand All @@ -34,7 +34,7 @@ def get_source_siglum(self, obj):
list_display = ("incipit", "get_source_siglum", "genre")
list_filter = (
"genre",
"office",
"service",
)
raw_id_fields = (
"source",
Expand Down
11 changes: 11 additions & 0 deletions django/cantusdb_project/main_app/admin/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.contrib import admin

from main_app.admin.base_admin import BaseModelAdmin
from main_app.forms import AdminServiceForm
from main_app.models import Service


@admin.register(Service)
class ServiceAdmin(BaseModelAdmin):
search_fields = ("name",)
form = AdminServiceForm
14 changes: 9 additions & 5 deletions django/cantusdb_project/main_app/admin/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,15 @@ class SourceAdmin(BaseModelAdmin):
"id",
"provenance_notes",
)
readonly_fields = ("title", "siglum") + READ_ONLY + (
"number_of_chants",
"number_of_melodies",
"date_created",
"date_updated",
readonly_fields = (
("title", "siglum")
+ 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
Expand Down
Loading
Loading