diff --git a/the_works/static/the_works.js b/the_works/static/the_works.js index 7c7fdf3..44796b5 100644 --- a/the_works/static/the_works.js +++ b/the_works/static/the_works.js @@ -1,3 +1,6 @@ +/* + * DATATABLES FUNCTIONS + */ // initializes the DataTable fumctionality for the given table function initDataTable(table_id) { // add # to id if not there already @@ -33,6 +36,10 @@ function initCreateButton(table_id, title, href=null) { document.getElementById(`${table_id.slice(0, 1) == "#" ? table_id.slice(1) : table_id}_wrapper`).firstElementChild.firstElementChild.appendChild(button); } + +/* + * MODAL FUNCTIONS + */ // adds event handlers that raise a modal function initModal(modal_id, input_ids, headings, form_actions) { // if input_ids is not an array, make it a single-element array @@ -41,7 +48,7 @@ function initModal(modal_id, input_ids, headings, form_actions) { document.getElementById("create-button").addEventListener("click", () => showDialog(modal_id, input_ids, headings[0], form_actions[0], new Array(input_ids.length).fill(""), 0)); // add event listeners to "update" elements for (const el of document.querySelectorAll('.action-update')) { - el.addEventListener("click", (event) => showDialog(modal_id, input_ids, headings[1], form_actions[1], input_ids.map((input) => event.currentTarget.dataset[input.slice(5).toLowerCase()]), event.currentTarget.dataset.id)); + el.addEventListener("click", event => showDialog(modal_id, input_ids, headings[1], form_actions[1], input_ids.map(input => event.currentTarget.dataset[input.slice(5).toLowerCase()]), event.currentTarget.dataset.id)); } } @@ -61,31 +68,10 @@ function showDialog(modal_id, input_ids, heading, form_action, input_values, url document.getElementById(modal_id).showModal(); } -// validates a date to be of format YYYY, MM-YYYY, DD-MM-YYYY, or empty -function validate_date(tag_id="form_Erscheinungstag", monat_id="form_Erscheinungsmonat", jahr_id="form_Erscheinungsjahr") { - let t = document.getElementById(tag_id); - let m = document.getElementById(monat_id); - let j = document.getElementById(jahr_id); - t.setCustomValidity(""); - t.setAttribute("aria-invalid", "false"); - m.setCustomValidity(""); - m.setAttribute("aria-invalid", "false"); - console.log("This is function validate_date(): tag/monat/jahr is " + t.value + "/" + m.value + "/" + j.value); //DEBUG - if ( t.value != "" ) { - if ( j.value == "" || m.value == "" ) { - t.setCustomValidity("wenn der Tag angegeben ist, müssen Monat und Jahr ebenfalls angegeben sein"); - t.setAttribute("aria-invalid", "true"); - t.reportValidity(); - } - } else if ( m.value != "" ) { - if ( j.value == "") { - m.setCustomValidity("wenn der Monat angegeben ist, muss das Jahr ebenfalls angegeben sein"); - m.setAttribute("aria-invalid", "true"); - m.reportValidity(); - } - } -} +/* + * MULTISELECT FUNCTIONS + */ // initialize a dropdown select component function initDropdownSelect(dropdown) { // empty search field @@ -93,13 +79,13 @@ function initDropdownSelect(dropdown) { search.value = ""; // add event listener to search field let input_name = dropdown.querySelector("input[type='checkbox']").getAttribute("name"); - search.addEventListener("input", (event) => { + search.addEventListener("input", event => { filterDropdownChoices(event.target, input_name); }); // add event listener to checkboxes let summary = dropdown.querySelector("summary"); - document.querySelectorAll(`[name='${input_name}']`).forEach( (el) => { - el.addEventListener("change", (event) => { + document.querySelectorAll(`[name='${input_name}']`).forEach( el => { + el.addEventListener("change", event => { updateDropdownSelected(event.target, summary); }); // add badges for genre that were already selected @@ -109,7 +95,7 @@ function initDropdownSelect(dropdown) { }); } -// event handler for genre search field; called whenever the search field's input value is changed +// event handler for dropdown search field; called whenever the search field's input value is changed function filterDropdownChoices(that, checkbox_name) { // show everything when search field is empty if ( ! that.value || that.value == "" ) { @@ -167,10 +153,10 @@ function removeDropdownBadge(event) { function initAllDropdownSelects() { // initialize each dropdown individually let dropdowns = document.querySelectorAll("form details.dropdown"); - dropdowns.forEach((d) => initDropdownSelect(d)); + dropdowns.forEach(d => initDropdownSelect(d)); // now it's time to set the z-indices // get the_works style sheet - let sheet = [...document.styleSheets].filter((s) => s.ownerNode.getAttribute("href").includes("the_works"))[0]; + let sheet = [...document.styleSheets].filter(s => s.ownerNode.getAttribute("href").includes("the_works"))[0]; const initialZ = 20; // set z-index on dropdowns in reverse order for ( let i = 0; i < dropdowns.length; i++ ) { @@ -185,6 +171,117 @@ function initAllDropdownSelects() { } } // add event handler to summary elements preventing default behavior (toggling the details element) if the click actually targeted a badge - document.querySelectorAll("form details.dropdown summary").forEach((summary) => summary.addEventListener("click", (event) => summaryClick(event, summary), true)); + document.querySelectorAll("form details.dropdown summary").forEach(summary => summary.addEventListener("click", event => summaryClick(event, summary), true)); +} + + +/* + * SEARCH-ALL FUNCTIONS + */ +function search_all(s, url) { +console.log(`JavaScript function search_all was called with search string ${s} and url ${url}`); + // remove previous results + document.getElementById("results").innerHTML = ""; + if ( s == "" ) { return; } + + // fetch search results + let fetch_url = new URL(url); + fetch_url.search = new URLSearchParams({query: s, case: document.getElementById("match_case").checked ? "match" : "no"}); +console.log(`fetch_url is ${fetch_url}`); + + fetch(fetch_url) + .then(response => response.json()) + .then(data => { + // tell user if there are no results + if ( Object.keys(data).length === 0 ) { + let h3 = document.createElement("h3"); + h3.innerText = "Die Suchanfrage ergab keine Treffer."; + document.getElementById("results").appendChild(h3); + return; + } + + // iterate over search results + for ( const db_table of Object.keys(data) ) { + let columns = Object.keys(data[db_table][0]); + + // build container, heading, table + let container = document.createElement("details"); + container.classList.add("result-container"); + container.setAttribute("name", "result-accordion"); + let heading = document.createElement("summary"); + container.appendChild(heading); + let table = document.createElement("table"); + + // build table head + let thead = document.createElement("thead"); + let tr, th, td; + tr = document.createElement("tr"); + for ( const column of columns) { + if ( column == "ID" ) { continue; } + th = document.createElement("th"); + th.innerText = column; + tr.appendChild(th); + } + thead.appendChild(tr); + table.appendChild(thead); + + // build table body + let tbody = document.createElement("tbody"); + for ( const row of data[db_table] ) { + tr = document.createElement("tr"); + for ( const column of columns ) { + // add column "ID" as data attribute, all other columns as regular table columns + if ( column == "ID" ) { + tr.setAttribute(`data-${db_table}-id`, row[column].toString()); + continue; + } + td = document.createElement("td"); + td.innerHTML = row[column] ? row[column].toString().replace(s, `${s}`) : ""; + tr.appendChild(td); + } + tbody.appendChild(tr); + } + table.appendChild(tbody); + heading.innerHTML = `

${db_table} (${tbody.childNodes.length} Treffer)

`; + + // add container element to DOM + container.append(table); + document.getElementById("results").appendChild(container); + + // open
but only if it's the first element + if ( document.getElementById("results").children[0] == container ) { + container.setAttribute("open", "open"); + } + document.getElementById("results").insertBefore(document.createElement("hr"), container); + } + }) + } + +/* + * MISC FUNCTIONS + */ +// validates a date to be of format YYYY, MM-YYYY, DD-MM-YYYY, or empty +function validate_date(tag_id="form_Erscheinungstag", monat_id="form_Erscheinungsmonat", jahr_id="form_Erscheinungsjahr") { + let t = document.getElementById(tag_id); + let m = document.getElementById(monat_id); + let j = document.getElementById(jahr_id); + t.setCustomValidity(""); + t.setAttribute("aria-invalid", "false"); + m.setCustomValidity(""); + m.setAttribute("aria-invalid", "false"); + console.log("This is function validate_date(): tag/monat/jahr is " + t.value + "/" + m.value + "/" + j.value); //DEBUG + if ( t.value != "" ) { + if ( j.value == "" || m.value == "" ) { + t.setCustomValidity("wenn der Tag angegeben ist, müssen Monat und Jahr ebenfalls angegeben sein"); + t.setAttribute("aria-invalid", "true"); + t.reportValidity(); + } + } else if ( m.value != "" ) { + if ( j.value == "") { + m.setCustomValidity("wenn der Monat angegeben ist, muss das Jahr ebenfalls angegeben sein"); + m.setAttribute("aria-invalid", "true"); + m.reportValidity(); + } + } } diff --git a/the_works/templates/views/home.html b/the_works/templates/views/home.html index 933d72e..cc5a4f3 100644 --- a/the_works/templates/views/home.html +++ b/the_works/templates/views/home.html @@ -1,9 +1,36 @@ {% extends 'base.html' %} -{% block title %}Home{% endblock title %} +{% block title %}Alles durchsuchen{% endblock title %} -{% block heading %}Home{% endblock heading %} +{% block head %} + +{% endblock head %} + +{% block heading %}Alles durchsuchen{% endblock heading %} {% block content %} -

Alle Texte

-{% endblock content %} \ No newline at end of file + +{% include "_icons.svg" %} +
+ + +
+ +
+
+{% endblock content %} + +{% block script %} + + +{% endblock script %} \ No newline at end of file diff --git a/the_works/views/home.py b/the_works/views/home.py index e402795..13365e4 100644 --- a/the_works/views/home.py +++ b/the_works/views/home.py @@ -1,8 +1,55 @@ -from flask import Blueprint, render_template +from flask import Blueprint, render_template, request, jsonify +from sqlalchemy import select +from sqlalchemy.sql.sqltypes import TEXT +from the_works.database import db +import the_works.models +import inspect bp = Blueprint("home", __name__) +# prepare list of DB table classes to be searched by search_all() +tables = [] +for name, obj in inspect.getmembers(the_works.models): + if "_" not in name and inspect.isclass(obj): + tables.append(obj) +#print(tables) #DEBUG + @bp.route("/") -def home(): +def startpage(): return render_template("views/home.html") +@bp.route("/search") +def search_all(): + print(f"home.search_all(): request.url is {request.url}") #DEBUG + # return when query is empty + if not request.args.get("query"): + return jsonify({}) + + # get URL parameters + s = request.args.get("query") + matchCase = True if request.args.get("case").lower() == "match" else False + result = {} + + # loop over database tables + for table in tables: + text_columns = [column.key for column in table.__table__.columns if type(column.type) == TEXT] + hits = [] + # loop over table rows + for row in db.session.execute(select(table)): + # loop over each text column in row + for column in text_columns: + if row[0].__getattribute__(column) is None: + continue + if matchCase: + if s in row[0].__getattribute__(column): + hits.append(row[0]._asdict()) + break + else: + if s.lower() in row[0].__getattribute__(column).lower(): + hits.append(row[0]._asdict()) + break + if hits != []: + result[table.__table__.fullname] = hits + # return results + return jsonify(result) +