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

Re-implement cantus index input tool #1538

Merged
merged 15 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 35 additions & 1 deletion django/cantusdb_project/cantusindex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -160,13 +161,46 @@ 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):
return None
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/<text to search>
"""

# 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]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@

@admin.register(InstitutionIdentifier)
class InstitutionIdentifierAdmin(BaseModelAdmin):
list_display = ('identifier', 'identifier_type')
list_display = ("identifier", "identifier_type")
raw_id_fields = ("institution",)
6 changes: 5 additions & 1 deletion django/cantusdb_project/main_app/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)
85 changes: 55 additions & 30 deletions django/cantusdb_project/main_app/templates/chant_create.html
Original file line number Diff line number Diff line change
Expand Up @@ -295,38 +295,63 @@ <h5><a id="source" href="{% url 'source-detail' source.id %}">{{ source.siglum }
</div>
</div>

{% if previous_chant %}
<div class="card w-100">
<div class="card-body">
<small>
<b>Suggestions based on previous chant:</b><br>
<div>
{{ previous_chant.folio }} {{ previous_chant.c_sequence}} <a href="{% url 'chant-detail' previous_chant.id %}" target="_blank">{{ previous_chant.incipit }}</a><br>
{% if previous_chant.cantus_id %}
(Cantus ID: <b><a href="https://cantusindex.org/id/{{ previous_chant.cantus_id }}" target="_blank">{{ previous_chant.cantus_id }}</a></b>)
{% if previous_chant %}
<div class="card w-100 mb-3">
<div class="card-body">
<small>
<b>Suggestions based on previous chant:</b><br>
<div>
{{ previous_chant.folio }} {{ previous_chant.c_sequence}} <a href="{% url 'chant-detail' previous_chant.id %}" target="_blank">{{ previous_chant.incipit }}</a><br>
{% if previous_chant.cantus_id %}
(Cantus ID: <b><a href="https://cantusindex.org/id/{{ previous_chant.cantus_id }}" target="_blank">{{ previous_chant.cantus_id }}</a></b>)
{% endif %}
</div>
{% if suggested_chants %}
{% for suggestion in suggested_chants %}
<input
type="button"
style="width: 80px"
value="{{ suggestion.cantus_id }}"
title="{{ suggestion.cantus_id }}"
onclick='autoFillSuggestedChant(
"{{ suggestion.genre_name }}",
{{ suggestion.genre_id | default_if_none:"null" }},
"{{ suggestion.cantus_id }}",
"{{ suggestion.fulltext }}"
)'
>
<strong>{{ suggestion.genre_name }}</strong> - <span title="{{ suggestion.fulltext }}">{{ suggestion.incipit }}</span> (<strong>{{ suggestion.occurrences }}x</strong>)<br>
{% endfor %}
{% else %}
Sorry! No suggestions found. Please use the search form below.<br>
{% endif %}
</small>
</div>
{% if suggested_chants %}
{% for suggestion in suggested_chants %}
<input
type="button"
style="width: 80px"
value="{{ suggestion.cantus_id }}"
title="{{ suggestion.cantus_id }}"
onclick='autoFillSuggestedChant(
"{{ suggestion.genre_name }}",
{{ suggestion.genre_id | default_if_none:"null" }},
"{{ suggestion.cantus_id }}",
"{{ suggestion.fulltext }}"
)'
>
<strong>{{ suggestion.genre_name }}</strong> - <span title="{{ suggestion.fulltext }}">{{ suggestion.incipit }}</span> (<strong>{{ suggestion.occurrences }}x</strong>)<br>
{% endfor %}
{% else %}
Sorry! No suggestions found.<br>
{% endif %}
</small>
</div>
{% endif %}
<div class="card w-100">
<div class="card-header">
<h5>Input Tool</h5>
</div>

<div class=" card-body" style="font-size: 15px">
Enter any text from chant:
<input type="text" id="incipitsearch" />
<button class="btn btn-danger btn-sm" onclick="openCiSearchWin()">Search ID</button>
<script>
function openCiSearchWin() {
search_term = document.getElementById("incipitsearch").value;
if (search_term.length < 3) {
search_term = window.prompt("Enter at least 3 characters of incipit", "");
}
address = "/ci-search/" + search_term;
var ci = window.open(address, "ci", "height=400px, width=1200px, top=0, left=50px, status=no, toolbar=no, location=no, scrollbars=yes, resizable=yes");
}
</script>
</div>

</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}
73 changes: 73 additions & 0 deletions django/cantusdb_project/main_app/templates/ci_search.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<!DOCTYPE html>
{% load static %}
<script type="text/javascript" src="{% static 'admin/js/vendor/jquery/jquery.js' %}"></script>

<html>

<head>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
</head>

<body>

<p>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
<a href="https://cantusindex.org/">Cantus Index</a> or contact the <a
href="mailto:[email protected]">administrator</a>
</p>
<table class="table table-responsive table-sm small table-bordered table-striped table-hover" style="display: table;">
{# if we don't include 'style="display: table;"', the table is very narrow when viewed in certain browsers (e.g. Firefox) #}
<thead class="thead-dark">
<tr>
<th width="50">Select</th>
<th width="80">Cantus ID</th>
<th width="80">Genre</th>
<th width="100">Fulltext</th>
</tr>
</thead>

<tbody>
{% for cantus_id,genre,full_text in results %}
<tr id="{{ cantus_id }}">
<td><input type="button" value="OK" onclick="autoFill()" /></td>
<td>{{ cantus_id }}</td>
<td>{{ genre }}</td>
<td>{{ full_text }}</td>
</tr>
{% endfor %}
</tbody>
</table>

<script>
function autoFill() {
var rowId = event.target.parentNode.parentNode.id; //this gives id of whose button was clicked
var cantus_id = document.getElementById(rowId).cells[1].innerText;
var genre = document.getElementById(rowId).cells[2].innerText;
var full_text = document.getElementById(rowId).cells[3].innerText;

// get the option value corresponding to the genre name
var genres = {{ genres|safe }}; // genres contains "id" and "name" of all Genre objects in the database
var genreObj = genres.find(item => item.name === genre);
var genreID = genreObj ? genreObj.id : null;

opener.document.getElementById('id_cantus_id').value = cantus_id;
// Since we're using a django-autocomplete-light widget for the Genre selector,
// we need to follow a special process in selecting a value from the widget:
// Set the value, creating a new option if necessary
if (genreID) {
if (opener.$('#id_genre').find("option[value='" + genreID + "']").length) {
opener.$('#id_genre').val(genreID).trigger('change');
} else {
// Create a new DOM Option and pre-select it by default
var newOption = new Option(genre, genreID, true, true);
opener.$('#id_genre').append(newOption).trigger('change');
}
}
opener.document.getElementById('id_manuscript_full_text_std_spelling').value = full_text;
close()
}
</script>
</body>
</html>
4 changes: 3 additions & 1 deletion django/cantusdb_project/main_app/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'<a href="https://differentiaedatabase.ca/differentia/{diff_id}" target="_blank">'
expected_html_substring: str = (
f'<a href="https://differentiaedatabase.ca/differentia/{diff_id}" target="_blank">'
)
self.assertIn(expected_html_substring, html)

def test_redirect_with_source_parameter(self):
Expand Down
6 changes: 6 additions & 0 deletions django/cantusdb_project/main_app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
ChantEditSyllabificationView,
ChantSearchView,
ChantSearchMSView,
CISearchView,
MelodySearchView,
SourceEditChantsView,
)
Expand Down Expand Up @@ -368,6 +369,11 @@
ChantSearchMSView.as_view(),
name="chant-search-ms",
),
path(
"ci-search/<str:search_term>",
CISearchView.as_view(),
name="ci-search",
),
path(
"search/",
views.redirect_search,
Expand Down
43 changes: 42 additions & 1 deletion django/cantusdb_project/main_app/views/chant.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading