diff --git a/django/cantusdb_project/cantusindex.py b/django/cantusdb_project/cantusindex.py index b00ef8ee3..3bd7c6284 100644 --- a/django/cantusdb_project/cantusindex.py +++ b/django/cantusdb_project/cantusindex.py @@ -7,6 +7,7 @@ from typing import Optional, Union, Callable from main_app.models import Genre import json +from requests.exceptions import SSLError, Timeout, HTTPError CANTUS_INDEX_DOMAIN: str = "https://cantusindex.uwaterloo.ca" DEFAULT_TIMEOUT: float = 2 # seconds @@ -132,7 +133,7 @@ def get_suggested_fulltext(cantus_id: str) -> Optional[str]: return suggested_fulltext -def get_merged_cantus_ids() -> Optional[list]: +def get_merged_cantus_ids() -> Optional[list[Optional[dict]]]: """Retrieve merged Cantus IDs from the Cantus Index API (/json-merged-chants) This function sends a request to the Cantus Index API endpoint for merged chants @@ -160,6 +161,8 @@ def get_merged_cantus_ids() -> Optional[list]: response.encoding = "utf-8-sig" raw_text: str = response.text text_without_bom: str = raw_text.encode().decode("utf-8-sig") + if not text_without_bom: + return None merge_events: list = json.loads(text_without_bom) if not isinstance(merge_events, list): @@ -167,6 +170,37 @@ def get_merged_cantus_ids() -> Optional[list]: return merge_events +def get_ci_text_search(search_term: str) -> Optional[list[Optional[dict]]]: + """Fetch data from Cantus Index for a given search term. + To do a text search on CI, we use 'https://cantusindex.org/json-text/ + """ + + # We have to use the old CI domain since this API is still not available on + # cantusindex.uwaterloo.ca. Once it's available, we can use get_json_from_ci_api + # json: Union[dict, list, None] = get_json_from_ci_api(uri) + uri: str = f"https://cantusindex.org/json-text/{search_term}" + try: + response: requests.Response = requests.get( + uri, + timeout=DEFAULT_TIMEOUT, + ) + except (SSLError, Timeout, HTTPError): + return None + if not response.status_code == 200: + return None + response.encoding = "utf-8-sig" + raw_text: str = response.text + text_without_bom: str = raw_text.encode().decode("utf-8-sig") + if not text_without_bom: + return None + text_search_results: list = json.loads(text_without_bom) + # if cantus index returns an empty table + if not text_search_results or not isinstance(text_search_results, list): + return None + + return text_search_results + + def get_json_from_ci_api( path: str, timeout: float = DEFAULT_TIMEOUT ) -> Union[dict, list, None]: diff --git a/django/cantusdb_project/main_app/admin/institution_identifier.py b/django/cantusdb_project/main_app/admin/institution_identifier.py index 6eb1288c9..220832999 100644 --- a/django/cantusdb_project/main_app/admin/institution_identifier.py +++ b/django/cantusdb_project/main_app/admin/institution_identifier.py @@ -6,5 +6,5 @@ @admin.register(InstitutionIdentifier) class InstitutionIdentifierAdmin(BaseModelAdmin): - list_display = ('identifier', 'identifier_type') + list_display = ("identifier", "identifier_type") raw_id_fields = ("institution",) diff --git a/django/cantusdb_project/main_app/permissions.py b/django/cantusdb_project/main_app/permissions.py index dd23ac0b6..cc7ae78bc 100644 --- a/django/cantusdb_project/main_app/permissions.py +++ b/django/cantusdb_project/main_app/permissions.py @@ -162,4 +162,8 @@ def user_can_manage_source_editors(user: User) -> bool: Checks if the user has permission to change the editors assigned to a Source. Used in SourceDetailView. """ - return user.is_superuser or user.is_staff or user.groups.filter(name="project manager").exists() + return ( + user.is_superuser + or user.is_staff + or user.groups.filter(name="project manager").exists() + ) diff --git a/django/cantusdb_project/main_app/templates/chant_create.html b/django/cantusdb_project/main_app/templates/chant_create.html index 7a42880ad..95f335c97 100644 --- a/django/cantusdb_project/main_app/templates/chant_create.html +++ b/django/cantusdb_project/main_app/templates/chant_create.html @@ -295,38 +295,63 @@
{{ source.siglum } - {% if previous_chant %} -
-
- - Suggestions based on previous chant:
-
- {{ previous_chant.folio }} {{ previous_chant.c_sequence}} {{ previous_chant.incipit }}
- {% if previous_chant.cantus_id %} - (Cantus ID: {{ previous_chant.cantus_id }}) + {% if previous_chant %} +
+
+ + Suggestions based on previous chant:
+
+ {{ previous_chant.folio }} {{ previous_chant.c_sequence}} {{ previous_chant.incipit }}
+ {% if previous_chant.cantus_id %} + (Cantus ID: {{ previous_chant.cantus_id }}) + {% endif %} +
+ {% if suggested_chants %} + {% for suggestion in suggested_chants %} + + {{ suggestion.genre_name }} - {{ suggestion.incipit }} ({{ suggestion.occurrences }}x)
+ {% endfor %} + {% else %} + Sorry! No suggestions found. Please use the search form below.
{% endif %} +
- {% if suggested_chants %} - {% for suggestion in suggested_chants %} - - {{ suggestion.genre_name }} - {{ suggestion.incipit }} ({{ suggestion.occurrences }}x)
- {% endfor %} - {% else %} - Sorry! No suggestions found.
- {% endif %} -
+
+ {% endif %} +
+
+
Input Tool
+
+ +
+ Enter any text from chant: + + + +
+
- {% endif %} +
+
{% endblock %} diff --git a/django/cantusdb_project/main_app/templates/ci_search.html b/django/cantusdb_project/main_app/templates/ci_search.html new file mode 100644 index 000000000..1dc1d6c47 --- /dev/null +++ b/django/cantusdb_project/main_app/templates/ci_search.html @@ -0,0 +1,73 @@ + +{% load static %} + + + + + + + + + + + +

Select the chant by clicking "OK" at the left. Please note that the search results are limited to the first 50 chants. + If your chant is not included here, please add it into + Cantus Index or contact the administrator +

+ + {# if we don't include 'style="display: table;"', the table is very narrow when viewed in certain browsers (e.g. Firefox) #} + + + + + + + + + + + {% for cantus_id,genre,full_text in results %} + + + + + + + {% endfor %} + +
SelectCantus IDGenreFulltext
{{ cantus_id }}{{ genre }}{{ full_text }}
+ + + + \ No newline at end of file diff --git a/django/cantusdb_project/main_app/tests/test_views.py b/django/cantusdb_project/main_app/tests/test_views.py index bf86bd1dc..aca6039ac 100644 --- a/django/cantusdb_project/main_app/tests/test_views.py +++ b/django/cantusdb_project/main_app/tests/test_views.py @@ -4963,7 +4963,9 @@ def test_dd_column(self): response = self.client.get(reverse("source-inventory", args=[source.id])) html: str = str(response.content) self.assertIn(diff_id, html) - expected_html_substring: str = f'' + expected_html_substring: str = ( + f'' + ) self.assertIn(expected_html_substring, html) def test_redirect_with_source_parameter(self): diff --git a/django/cantusdb_project/main_app/urls.py b/django/cantusdb_project/main_app/urls.py index 05a446f43..0df25cd21 100644 --- a/django/cantusdb_project/main_app/urls.py +++ b/django/cantusdb_project/main_app/urls.py @@ -18,6 +18,7 @@ ChantEditSyllabificationView, ChantSearchView, ChantSearchMSView, + CISearchView, MelodySearchView, SourceEditChantsView, ) @@ -368,6 +369,11 @@ ChantSearchMSView.as_view(), name="chant-search-ms", ), + path( + "ci-search/", + CISearchView.as_view(), + name="ci-search", + ), path( "search/", views.redirect_search, diff --git a/django/cantusdb_project/main_app/views/chant.py b/django/cantusdb_project/main_app/views/chant.py index 4b16a9489..7ceee6250 100644 --- a/django/cantusdb_project/main_app/views/chant.py +++ b/django/cantusdb_project/main_app/views/chant.py @@ -47,7 +47,11 @@ user_can_view_chant, ) -from cantusindex import get_suggested_chants, get_suggested_fulltext +from cantusindex import ( + get_suggested_chants, + get_suggested_fulltext, + get_ci_text_search, +) CHANT_SEARCH_TEMPLATE_VALUES: tuple[str, ...] = ( # for views that use chant_search.html, this allows them to @@ -869,6 +873,43 @@ def get_success_url(self): return reverse("source-edit-chants", args=[self.object.source.id]) +class CISearchView(TemplateView): + """Search in CI and write results in get_context_data + Shown on the chant create page as the "Input Tool" + """ + + template_name = "ci_search.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["genres"] = list( + Genre.objects.all().order_by("name").values("id", "name") + ) + search_term: str = kwargs["search_term"] + search_term: str = search_term.replace(" ", "+") # for multiple keywords + + text_search_results: Optional[list[Optional[dict]]] = get_ci_text_search( + search_term + ) + + cantus_id = [] + genre = [] + full_text = [] + + if text_search_results: + for result in text_search_results: + if result: + cantus_id.append(result.get("cid", None)) + genre.append(result.get("genre", None)) + full_text.append(result.get("fulltext", None)) + + if len(cantus_id) == 0: + context["results"] = [["No results", "No results", "No results"]] + else: + context["results"] = zip(cantus_id, genre, full_text) + return context + + class SourceEditChantsView(LoginRequiredMixin, UserPassesTestMixin, UpdateView): template_name = "chant_edit.html" model = Chant