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 12 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
26 changes: 24 additions & 2 deletions django/cantusdb_project/main_app/templates/chant_create.html
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ <h5><a id="source" href="{% url 'source-detail' source.id %}">{{ source.siglum }
</div>

{% if previous_chant %}
<div class="card w-100">
<div class="card w-100 mb-3">
<div class="card-body">
<small>
<b>Suggestions based on previous chant:</b><br>
Expand All @@ -319,12 +319,34 @@ <h5><a id="source" href="{% url 'source-detail' source.id %}">{{ source.siglum }
<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>
Sorry! No suggestions found. Please use the search form below.<br>
{% endif %}
</small>
</div>
</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>
</div>
</div>
Expand Down
72 changes: 72 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,72 @@
<!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. 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>
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 @@ -369,6 +370,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
40 changes: 39 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,40 @@ 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[dict]] = get_ci_text_search(search_term)
lucasmarchd01 marked this conversation as resolved.
Show resolved Hide resolved

cantus_id = []
genre = []
full_text = []

if text_search_results:
for result in text_search_results:
cantus_id.append(result["cid"])
genre.append(result["genre"])
full_text.append(result["fulltext"])

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