Quellcode durchsuchen

Test feedback form.

scossu vor 6 Monaten
Ursprung
Commit
a6e774f4bc

+ 18 - 0
README.md

@@ -19,6 +19,24 @@ docker run -e TXL_FLASK_SECRET=changeme -p 8000:8000 scriptshifter:latest
 For running in development mode, add `-e FLASK_ENV=development` to the options.
 
 
+## Environment variables
+
+The following environment variables are available for modification:
+
+`TXL_EMAIL_FROM`: Email address sending the feedback form on behalf of users.
+
+`TXL_EMAIL_TO`: Recipients of the feedback form.
+
+`TXL_FLASK_SECRET`: Seed for web server security. Set to a random-generated
+string in a production environment.
+
+`TXL_LOGLEVEL`: Logging level. Use Python notation. The default is `WARN`.
+
+`TXL_SMTP_HOST`: SMTP host to send feedback messages through. Defaults to
+`localhost`.
+
+`TXL_SMTP_PORT`: Port of the SMTP server. Defaults to `1025`.
+
 ## Web UI
 
 `/` renders a simple HTML form to test the transliteration service.

+ 19 - 19
doc/config.md

@@ -14,19 +14,31 @@ The configuration file names are key to most operations in the software. They
 are all-lowercase and use underscores to separate words, e.g.
 `church_slavonic`. They have the `.yml` extension and are written in the
 [YAML](https://yaml.org/) configuration language. Hence, a transliteration
-request to the REST API endpoint `/trans/church_slavonic` uses the
-`church_slavonic.yml` configuration file.
-
-In order for a transliteration option to appear in the Web interface menu or
-in the `/languages` API endpoint, it must be added to the `index.yml` file.
-This file contains summary information about the available languages.
+request to the `/trans` REST API endpoint providing `church_slavonic` as the
+transliteration language, uses the `church_slavonic.yml` configuration file.
 
 Other files are present in the `data` directory that are not exposed to the end
 user via Web UI or REST API. These files may be incomplete transliteration
 tables that are used by other specific tables. An example is `_cyrillic.yml`,
 which is used by `belarusian.yml`, `bulgarian.yml`, etc., but is not meant to
 be used by itself. It is still accessible for transliteration however, for
-testing purposes. See below for more details about inhritance.
+testing purposes. See below for more details about inheritance.
+
+###  Index file
+
+In order for a transliteration option to appear in the Web interface menu or
+in the `/languages` API endpoint, it must be added to the `index.yml` file.
+This file contains summary information about the available languages.
+
+The index file is a map of key-value pairs, where the keys are the
+transliteration table key names as described previously, and the values are
+key-value pairs which can have arbitrary contents. These contents are displayed
+to the user in the `/languages` API endpoint.
+
+The only mandatory key for each key-value pair is `name`, which is the
+human-readable label that is displayed in the Web UI. Other keys, such as
+`description`, may be used to inform the user about the scope of a particular
+table.
 
 ## Inheritance
 
@@ -256,15 +268,3 @@ Type: list
 
 This is only a valid subsection of S2R. It removes double capitalization rules
 from the inherited list.
-
-##  Index file
-
-The index file is a map of key-value pairs, where the keys are the
-transliteration table key names as described previously, and the values are
-key-value pairs which can have arbitrary contents. These contents are displayed
-to the user in the `/languages` API endpoint.
-
-The only mandatory key for each key-value pair is `name`, which is the
-human-readable label that is displayed in the Web UI. Other keys, such as
-`description`, may be used to inform the user about the scope of a particular
-table.

+ 10 - 0
scriptshifter/__init__.py

@@ -5,6 +5,16 @@ from os import environ, path
 
 APP_ROOT = path.dirname(path.realpath(__file__))
 
+# SMTP server for sending email. For a dummy server that just echoes the
+# messages, run: `python -m smtpd -n -c DebuggingServer localhost:1025`
+SMTP_HOST = environ.get("TXL_SMTP_HOST", "localhost")
+try:
+    SMTP_PORT = int(environ.get("TXL_SMTP_PORT", "1025"))
+except ValueError:
+    raise SystemError("TXL_SMTP_PORT env var is not an integer.")
+EMAIL_FROM = environ["TXL_EMAIL_FROM"]
+EMAIL_TO = environ["TXL_EMAIL_TO"]
+
 logging.basicConfig(
         # filename=environ.get("TXL_LOGFILE", "/dev/stdout"),
         level=environ.get("TXL_LOGLEVEL", logging.INFO))

+ 46 - 1
scriptshifter/rest_api.py

@@ -2,11 +2,14 @@ import logging
 
 from base64 import b64encode
 from copy import deepcopy
-from json import loads
+from email.message import EmailMessage
+from json import loads, dumps
 from os import environ, urandom
+from smtplib import SMTP
 
 from flask import Flask, jsonify, render_template, request
 
+from scriptshifter import EMAIL_FROM, EMAIL_TO, SMTP_HOST, SMTP_PORT
 from scriptshifter.tables import list_tables, load_table
 from scriptshifter.trans import transliterate
 
@@ -91,3 +94,45 @@ def transliterate_req():
         return (str(e), 400)
 
     return {"output": out, "warnings": warnings}
+
+
+@app.route("/feedback", methods=["POST"])
+def feedback():
+    """
+    Allows users to provide feedback to improve a specific result.
+    """
+    lang = request.form["lang"]
+    src = request.form["src"]
+    t_dir = request.form.get("t_dir", "s2r")
+    result = request.form["result"]
+    expected = request.form["expected"]
+    options = request.form.get("options", {})
+    notes = request.form.get("notes")
+    contact = request.form.get("contact")
+
+    msg = EmailMessage()
+    msg["subject"] = "Scriptshifter feedback report"
+    msg["from"] = EMAIL_FROM
+    msg["to"] = EMAIL_TO
+    if contact:
+        msg["cc"] = contact
+    msg.set_content(f"""
+        *Scriptshifter feedback report from {contact or 'anonymous'}*\n\n
+        *Language:* {lang}\n
+        *Direction:* {
+                    'Roman to Script' if t_dir == 'r2s'
+                    else 'Script to Roman'}\n
+        *Source:* {src}\n
+        *Result:* {result}\n
+        *Expected result:* {expected}\n
+        *Applied options:* {dumps(options)}\n
+        *Notes:*\n
+        {notes}""")
+
+    # TODO This uses a test SMTP server:
+    # python -m smtpd -n -c DebuggingServer localhost:1025
+    smtp = SMTP(SMTP_HOST, SMTP_PORT)
+    smtp.send_message(msg)
+    smtp.quit()
+
+    return {"message": "Feedback message sent."}

BIN
scriptshifter/static/loading.gif


+ 151 - 0
scriptshifter/static/ss.js

@@ -0,0 +1,151 @@
+document.getElementById('lang').addEventListener('change',(event)=>{
+    let lang = document.getElementById("lang").value;
+
+    fetch('/options/' + lang)
+      .then(response=>response.json())
+        .then((data) => {
+            document.getElementById("options").replaceChildren();
+            data.forEach((opt)=>{
+                let fset = document.createElement("fieldset");
+                let label = document.createElement("label");
+                label.setAttribute("for", opt.id);
+                label.append(opt.label);
+
+                let input = document.createElement("input");
+                input.setAttribute("id", opt.id);
+                input.setAttribute("name", opt.id);
+                input.classList.add("option_i");
+                input.value = opt.default;
+
+                let descr = document.createElement("p");
+                descr.setAttribute("class", "input_descr");
+                descr.append(opt.description);
+
+                fset.append(label, descr, input);
+                document.getElementById("options").append(fset);
+            });
+        });
+
+    event.preventDefault();
+    return false;
+})
+document.getElementById('lang').dispatchEvent(new Event('change'));
+
+
+document.getElementById('transliterate').addEventListener('submit',(event)=>{
+
+    document.getElementById('feedback_cont').classList.add("hidden");
+    document.getElementById('loader_results').classList.remove("hidden");
+
+    const data = new URLSearchParams();
+
+    let t_dir = Array.from(document.getElementsByName("t_dir")).find(r => r.checked).value;
+
+    let capitalize = Array.from(document.getElementsByName("capitalize")).find(r => r.checked).value;
+
+
+    data.append('text',document.getElementById('text').value)
+    data.append('lang',document.getElementById('lang').value)
+    data.append('t_dir',t_dir)
+    data.append('capitalize',capitalize)
+
+    let options = {};
+    let option_inputs = document.getElementsByClassName("option_i");
+    for (i = 0; i < option_inputs.length; i++) {
+        let el = option_inputs[i];
+        options[el.getAttribute('id')] = el.value;
+    };
+    data.append('options', JSON.stringify(options));
+
+    fetch('/trans', {
+        method: 'post',
+        body: data,
+    })
+    .then(response=>response.json())
+    .then((results)=>{
+
+        document.getElementById('warnings-toggle').classList.add("hidden");
+        document.getElementById('loader_results').classList.add("hidden");
+
+        document.getElementById('results').innerText = results.output
+        document.getElementById('feedback_btn_cont').classList.remove("hidden");
+
+        if (results.warnings.length>0){
+            document.getElementById('warnings-toggle').classList.remove("hidden");
+            document.getElementById('warnings').innerText = "WARNING:\n" + results.warnings.join("\n")
+        }
+
+
+    }).catch((error) => {
+      alert("Error:\n" + error)
+    });
+
+    event.preventDefault()
+    return false
+
+})
+
+document.getElementById('feedback_btn').addEventListener('click',(event)=>{
+    document.getElementById('lang_fb_input').value = document.getElementById('lang').value;
+    document.getElementById('src_fb_input').value = document.getElementById('text').value;
+    document.getElementById('t_dir_fb_input').value = Array.from(
+        document.getElementsByName("t_dir")
+    ).find(r => r.checked).value;
+    document.getElementById('result_fb_input').value = document.getElementById('results').innerText;
+    document.getElementById('expected_fb_input').value = "";
+    document.getElementById('notes_fb_input').value = "";
+    document.getElementById('options_fb_input').value = ""; // TODO
+
+    document.getElementById('feedback_cont').classList.remove("hidden");
+
+    location.href = "#";
+    location.href = "#feedback_cont";
+})
+
+document.getElementById('feedback_form').addEventListener('submit',(event)=>{
+    const data = new URLSearchParams();
+    data.append('lang', document.getElementById('lang_fb_input').value);
+    data.append('src', document.getElementById('src_fb_input').value);
+    data.append('t_dir', document.getElementById('t_dir_fb_input').value);
+    data.append('result', document.getElementById('result_fb_input').value);
+    data.append('expected', document.getElementById('expected_fb_input').value);
+    data.append('contact', document.getElementById('contact_fb_input').value);
+    data.append('notes', document.getElementById('notes_fb_input').value);
+
+    let options = {};
+    let option_inputs = document.getElementsByClassName("option_i");
+    for (i = 0; i < option_inputs.length; i++) {
+        let el = option_inputs[i];
+        options[el.getAttribute('id')] = el.value;
+    };
+    data.append('options', JSON.stringify(options));
+
+    fetch('/feedback', {
+        method: 'post',
+        body: data,
+    })
+    .then(response=>response.json())
+    .then((results)=>{
+        alert(
+            "Thanks for your feedback. You should receive an email with "
+            + "a copy of your submission."
+        );
+
+        document.getElementById('feedback_cont').classList.add("hidden");
+        document.getElementById('feedback_form').reset();
+        location.href = "#";
+
+    })
+
+    event.preventDefault();
+    return false;
+})
+
+document.getElementById('cancel_fb_btn').addEventListener('click',(event)=>{
+    document.getElementById('feedback_cont').classList.add("hidden");
+    document.getElementById('feedback_form').reset();
+    location.href = "#";
+
+    event.preventDefault();
+    return false;
+})

+ 86 - 140
scriptshifter/templates/index.html

@@ -13,6 +13,7 @@
             height: 15vh;
             padding: 0.5em;
         }
+
         #results{
             font-size: 1.25em;
             background-color: whitesmoke;
@@ -20,11 +21,18 @@
             padding: 1em;
         }
 
+        #feedback_cont {
+            margin: 4em;
+            padding: 4em;
+            background-color: whitesmoke;
+        }
+
         pre.warnings{
             border-left: 0.3rem solid #FF5722 !important;
         }
-        #warnings-toggle{
-            display: none;
+
+        .hidden {
+            display: none !important;
         }
 
         p.input_descr {
@@ -33,9 +41,12 @@
             margin-bottom: .5rem;
         }
 
-    </style>
-
+        .center {
+            display: block;
+            margin: 20px auto;
+        }
 
+    </style>
 
 
     <form id="transliterate" action="/trans" method="POST">
@@ -52,24 +63,24 @@
         <fieldset>
             <legend>Direction</legend>
             <div>
-                <label class="label-inline" for="s2r">Script to Roman</label>
+                <label class="label-inline" for="t_dir">Script to Roman</label>
                 <input
                         type="radio" id="opt_s2r" name="t_dir" value="s2r"
                         checked>
             </div>
             <div>
                 <label class="label-inline" for="r2s">Roman to script</label>
-                <input
-                        type="radio" id="opt_r2s" name="t_dir" value="r2s">
+                <input type="radio" id="opt_r2s" name="t_dir" value="r2s">
             </div>
         </fieldset>
+
         <fieldset>
             <legend>Capitalize</legend>
             <div>
                 <label class="label-inline" for="no-change">No change</label>
                 <input
                         type="radio" id="no-change" name="capitalize"
-                                                     value="no_change" checked>
+                        value="no_change" checked>
             </div>
             <div>
                 <label class="label-inline" for="first">First word</label>
@@ -80,146 +91,81 @@
                 <input type="radio" id="all" name="capitalize" value="all">
             </div>
         </fieldset>
-        <div id="options"></div>
-        <fieldset>
-            <input class="button-primary" type="submit" value="Transliterate!">
-        </fieldset>
-    </form>
-
-    <div id="warnings-toggle"><pre class="warnings"><code id="warnings">
-
-    </code></pre></div>
-    <div id="results">
-        Results will appear here.
-    </div>
-
-    <script type="text/javascript">
-        document.getElementById('lang').addEventListener('change',(event)=>{
-            let lang = document.getElementById("lang").value;
-
-            fetch('/options/' + lang)
-              .then(response=>response.json())
-                .then((data) => {
-                    document.getElementById("options").replaceChildren();
-                    data.forEach((opt)=>{
-                        let fset = document.createElement("fieldset");
-                        let label = document.createElement("label");
-                        label.setAttribute("for", opt.id);
-                        label.append(opt.label);
-
-                        let input = document.createElement("input");
-                        input.setAttribute("id", opt.id);
-                        input.setAttribute("name", opt.id);
-                        input.classList.add("option_i");
-                        input.value = opt.default;
-
-                        let descr = document.createElement("p");
-                        descr.setAttribute("class", "input_descr");
-                        descr.append(opt.description);
-
-                        fset.append(label, descr, input);
-                        document.getElementById("options").append(fset);
-                    });
-                });
-
-            event.preventDefault();
-            return false;
-        })
-        document.getElementById('lang').dispatchEvent(new Event('change'));
-
-
-        document.getElementById('transliterate').addEventListener('submit',(event)=>{
-
-            const data = new URLSearchParams();
-
-            let t_dir = Array.from(document.getElementsByName("t_dir")).find(r => r.checked).value;
-
-            let capitalize = Array.from(document.getElementsByName("capitalize")).find(r => r.checked).value;
-
-
-            data.append('text',document.getElementById('text').value)
-            data.append('lang',document.getElementById('lang').value)
-            data.append('t_dir',t_dir)
-            data.append('capitalize',capitalize)
-
-            let options = {};
-            let option_inputs = document.getElementsByClassName("option_i");
-            for (i = 0; i < option_inputs.length; i++) {
-                let el = option_inputs[i];
-                options[el.getAttribute('id')] = el.value;
-            };
-            data.append('options', JSON.stringify(options));
-
-            fetch('/trans', {
-                method: 'post',
-                body: data,
-            })
-            .then(response=>response.json())
-            .then((results)=>{
-
-                document.getElementById('warnings-toggle').style.display="none"
-
-                document.getElementById('results').innerText = results.output
-
-                if (results.warnings.length>0){
-                    document.getElementById('warnings-toggle').style.display="block"
-                }
-                document.getElementById('warnings').innerText = "WARNING:\n" + results.warnings.join("\n")
-
-
-            }).catch((error) => {
-              alert("Error:\n" + error)
-            });
 
-            event.preventDefault()
-            return false
-
-        })
-
-
-
-    </script>
-
-
-<!-- 
-    <form action="/transliterate" method="POST">
         <fieldset>
-            <label for="text">Input text</label>
-            <textarea id="text" name="text"></textarea>
-            <label for="lang">Language</label>
-            <select id="lang" name="lang">
-                {% for k, v in languages.items() %}
-                    <option value="{{ k }}">{{ v["name"] }}</option>
-                {% endfor %}
-            </select>
-            <div>
-                <label class="label-inline" for="r2s">Roman to Script</label>
-                <input type="checkbox" id="r2s" name="r2s">
-            </div>
+            <div id="options"></div>
         </fieldset>
+
         <fieldset>
-            <legend>Capitalize</legend>
-            <div>
-                <label class="label-inline" for="no-change">No change</label>
-                <input
-                        type="radio" id="no-change" name="capitalize"
-                                                     value="no_change" checked>
-            </div>
-            <div>
-                <label class="label-inline" for="first">First word</label>
-                <input type="radio" id="first" name="capitalize" value="first">
-            </div>
-            <div>
-                <label class="label-inline" for="all">All words</label>
-                <input type="radio" id="all" name="capitalize" value="all">
-            </div>
+            <input class="button button-primary" type="submit" value="Transliterate!">
         </fieldset>
-        <fieldset>
-            <input class="button-primary" type="submit" value="Transliterate!">
+
+        <fieldset id="feedback_btn_cont" class="hidden">
+            <input
+                    id="feedback_btn" class="button button-outline"
+                    value="Suggest improvements">
         </fieldset>
-    </form> -->
+    </form>
 
+    <div id="warnings-toggle" class="hidden">
+        <pre class="warnings"><code id="warnings"></code></pre>
+    </div>
 
+    <div id="results_cont">
+        <img id="loader_results" src="/static/loading.gif" class="hidden"/>
+        <div id="results">Results will appear here.</div>
+    </div>
 
+    <div id="feedback_cont" class="hidden">
+        <h2>Submit feedback</h2>
+        <form
+                id="feedback_form" action="/feedback"
+                method="POST">
+            <fieldset>
+                <label class="label-inline" for="lang">Language</label>
+                <input id="lang_fb_input" name="lang" disabled />
+            </fieldset>
+            <fieldset>
+                <label class="label-inline" for="src">Input text</label>
+                <input id="src_fb_input" name="src" disabled />
+            </fieldset>
+            <fieldset>
+                <label class="label-inline" for="t_dir">Direction</label>
+                <input id="t_dir_fb_input" name="t_dir" disabled />
+            </fieldset>
+            <fieldset>
+                <label class="label-inline" for="result">Result</label>
+                <textarea id="result_fb_input" name="result" disabled>
+                </textarea>
+            </fieldset>
+            <fieldset>
+                <label class="label-inline" for="expected">
+                    Expected result
+                </label>
+                <textarea
+                        id="expected_fb_input" name="expected"
+                        style="background-color: white"></textarea>
+            </fieldset>
+            <fieldset>
+                <label class="label-inline" for="contact">Contact</label>
+                <input id="contact_fb_input" name="contact" />
+            </fieldset>
+            <fieldset>
+                <label class="label-inline" for="notes">Notes</label>
+                <textarea id="notes_fb_input" name="notes"></textarea>
+            </fieldset>
+                <input type="hidden" id="options_fb_input" name="options" />
+            <fieldset>
+            </fieldset>
+
+            <button type="submit" class="button button-primary">
+                Submit
+            </button>
+            <button
+                    id="cancel_fb_btn"
+                    class="button button-clear">Cancel</button>
+        </form>
     </div>
+
+    <script type="text/javascript" src="/static/ss.js"></script>
 {% endblock %}