Commit 76883051 authored by Vlad Dumitru's avatar Vlad Dumitru
Browse files

finish transitioning to JSON data files

parent c49167bb
......@@ -24,6 +24,7 @@
</div>
<h2 class="rhs">{{ title }}</h2>
</header>
<a id="go-back" class="interactive" href="{{ backlink }}">&larr;</a>
<main id="main">{% block content %}{% endblock %}</main>
<button class="scroller" id="scroll-up">&uarr;</button>
<button class="scroller" id="scroll-down">&darr;</button>
......
{% extends "base.html" %}
{% block content %}
<div class="container">
{% for question in questions %}
{% for question in quiz.questions %}
<div class="question">
<div class="text">{{ question.text }}</div>
{% if question.image %}
<a href="/quiz/{{ id }}/question/{{ question.id }}">
<img {% if question.image.alt %}alt="{{ question.image.alt }}"{% endif %} src="/img/{{ question.image.path }}">
<a href="/quiz/{{ quiz.id }}/question/{{ question.id }}">
<img src="/img/{{ question.image }}">
</a>
{% endif %}
<a href="/quiz/{{ id }}/results/{{ question.id }}">Ergebnisse</a>
<a href="/quiz/{{ quiz.id }}/results/{{ question.id }}">Ergebnisse</a>
</div>
{% endfor %}
</div>
......
{% extends "base.html" %}
{% block content %}
<div class="question-container">
<h2>{{ question.text }}</h2>
{% if media.kind = "video" %}
{% if question.media.type = "video" %}
<video controls>
<source src="/video/{{ media.payload }}" type="video/mp4">
<source src="/video/{{ question.media.file }}" type="video/mp4">
</video>
{% endif %}
{% if media.kind = "audio" %}
<audio id="audio" src="/audio/{{ media.payload }}"></audio>
{% if question.media.type = "audio" %}
<audio id="audio" src="/audio/{{ question.media.file }}"></audio>
<button id="play-button" style="margin-bottom: 1rem;">
<img src="/img/Ohr.jpeg">
</button>
{% endif %}
<form method="post" action="/quiz/{{ id }}/question/{{ question.id }}">
{% if question.kind = "multiple-choice" %}
<form method="post" action="/quiz/{{ quiz.id }}/question/{{ question.id }}">
{% if question.type = "multiple-choice" %}
<div class="flex row">
{% for choice in choices %}
{% for choice in question.choices %}
<div class="choice">
<input name="choice" id="choice-{{ choice.id }}" type="radio" value="{{ choice.id }}">
<label for="choice-{{ choice.id }}">{{ choice.text }}</label>
<input name="choice" id="choice-{{ forloop.counter }}" type="radio" value="{{ choice }}">
<label for="choice-{{ forloop.counter }}">{{ choice }}</label>
</div>
{% endfor %}
</div>
{% endif %}
{% if question.kind = "free-form" %}
{% if question.type = "free-form" %}
<input name="choice" type="text">
{% endif %}
<input type="submit" value="submit">
......@@ -33,7 +34,7 @@
{% endblock %}
{% block page-scripts %}
{% if media.kind = "audio" %}
{% if question.media.type = "audio" %}
<script>
window.addEventListener('load', () => {
const audioElement = document.getElementById('audio')
......
......@@ -6,7 +6,9 @@
{% block content %}
<div class="container">
<div id="my_dataviz"></div>
<div class="box">
<div id="my_dataviz"></div>
</div>
</div>
{% endblock %}
......
{% extends "base.html" %}
{% block content %}
<ul>
{% for recording in recordings %}
<li>
<strong>{{ recording.description }}</strong>
<audio src="/record/data/{{ recording.id }}" controls>
</li>
{% endfor %}
</ul>
<div class="container">
<ul class="box">
{% for recording in recordings %}
<li>
<strong>{{ recording.description }}</strong>
<audio src="/record/data/{{ recording.id }}" controls>
</li>
{% endfor %}
</ul>
</div>
{% endblock %}
{% extends "base.html" %}
{% block content %}
<div class="container" id="recorder">
<div class="container flex col" id="recorder">
<div class="box flex col">
<h2>
Schreibe die Sprache deines Zungenbrechers auf:
......@@ -138,9 +138,10 @@
uploadButton.addEventListener('click', () => {
const formData = new FormData()
formData.append('file', blob)
formData.append('lang', document.getElementsByName('language')[0].value)
fetch('record', { method: 'POST', body: formData })
.then(res => console.log(res))
.then(res => window.location = res.url)
.catch(err => console.error(err))
})
})
......
{% extends "base.html" %}
{% block content %}
<div class="card in-circle">
<a href="/record">
<span class="title">Sag mir einen Zungenbrecher!</span>
<img src="/img/Mikrophon.jpeg">
</a>
</div>
{% for quiz in quizzes %}
<div class="card in-circle">
<a href="/quiz/{{ quiz.id }}">
<span class="title">{{ quiz.title }}</span>
<img src="/img/{{ quiz.image.path }}">
</a>
</div>
{% endfor %}
<div class="card in-circle">
<a href="/record">
<span class="title">Sag mir einen Zungenbrecher!</span>
<img src="/img/Mikrophon.jpeg">
</a>
</div>
{% for quiz in quizzes %}
<div class="card in-circle">
<a href="/quiz/{{ quiz.id }}">
<span class="title">{{ quiz.title }}</span>
<img src="/img/{{ quiz.image }}">
</a>
</div>
{% endfor %}
{% endblock %}
......@@ -131,15 +131,17 @@ form {
}
main {
width: calc(100vw - 8rem);
width: calc(100vw - 16rem);
padding: 0.5rem;
overflow: scroll;
scrollbar-width: none;
scroll-behavior: smooth;
margin-left: 8rem;
}
button {
button, a.interactive {
border-radius: 50%;
min-width: 5rem;
border: none;
......@@ -149,12 +151,23 @@ button {
transition: ease-in-out all 0.25s;
}
a.interactive {
display: flex;
flex-flow: row nowrap;
justify-content: center;
align-items: center;
height: 5rem;
text-decoration: none;
font-size: 4rem;
}
button > img {
border-radius: 50%;
align-self: flex-start;
}
button:not(:disabled):hover {
button:not(:disabled):hover, a.interactive:hover {
transform: scale(1.25);
background-color: rgba(0, 0, 128, 0.5);
}
......@@ -178,6 +191,13 @@ button.scroller:disabled {
bottom: 2vw;
}
#go-back {
height: 5rem;
position: absolute;
top: 50vh;
left: 2vw;
}
.container {
display: flex;
flex-flow: row wrap;
......
......@@ -5,6 +5,7 @@
{
"type": "multiple-choice",
"text": "Hörst du „DA“ oder „BA“?",
"image": "Ohr.jpeg",
"choices": [ "DA", "BA" ],
"media": {
"type": "video",
......@@ -14,6 +15,7 @@
{
"type": "multiple-choice",
"text": "Hörst du „DA“ oder „GA“?",
"image": "Ohr.jpeg",
"choices": [ "DA", "GA" ],
"media": {
"type": "audio",
......@@ -23,6 +25,7 @@
{
"type": "multiple-choice",
"text": "Hörst du „DA“ oder „BA“?",
"image": "Ohr.jpeg",
"choices": [ "DA", "BA" ],
"media": {
"type": "audio",
......@@ -32,6 +35,7 @@
{
"type": "multiple-choice",
"text": "Hörst du „DA“ oder „GA“?",
"image": "Ohr.jpeg",
"choices": [ "DA", "GA" ],
"media": {
"type": "audio",
......@@ -41,6 +45,7 @@
{
"type": "multiple-choice",
"text": "Hörst du „GA“ oder „DA“?",
"image": "Ohr.jpeg",
"choices": [ "GA", "DA" ],
"media": {
"type": "audio",
......
......@@ -4,7 +4,8 @@
"questions": [
{
"type": "free-form",
"text": "'In diesem Reim fehlt ein Wort. Welches Wort ist es?",
"text": "In diesem Reim fehlt ein Wort. Welches Wort ist es?",
"image": "headphones.jpg",
"media": {
"type": "audio",
"file": "Stefan_Katze.wav"
......@@ -12,7 +13,8 @@
},
{
"type": "free-form",
"text": "'In diesem Reim fehlt ein Wort. Welches Wort ist es?",
"text": "In diesem Reim fehlt ein Wort. Welches Wort ist es?",
"image": "headphones.jpg",
"media": {
"type": "audio",
"file": "Eric_Messer.wav"
......@@ -20,7 +22,8 @@
},
{
"type": "free-form",
"text": "'In diesem Reim fehlt ein Wort. Welches Wort ist es?",
"text": "In diesem Reim fehlt ein Wort. Welches Wort ist es?",
"image": "headphones.jpg",
"media": {
"type": "audio",
"file": "Saskia_Backe.wav"
......@@ -28,7 +31,8 @@
},
{
"type": "free-form",
"text": "'In diesem Reim fehlt ein Wort. Welches Wort ist es?",
"text": "In diesem Reim fehlt ein Wort. Welches Wort ist es?",
"image": "headphones.jpg",
"media": {
"type": "audio",
"file": "Stefan_Bayern.wav"
......@@ -36,7 +40,8 @@
},
{
"type": "free-form",
"text": "'In diesem Reim fehlt ein Wort. Welches Wort ist es?",
"text": "In diesem Reim fehlt ein Wort. Welches Wort ist es?",
"image": "headphones.jpg",
"media": {
"type": "audio",
"file": "Eric_Hocker.wav"
......
......@@ -4,8 +4,8 @@
"questions": [
{
"type": "multiple-choice",
"image": "cucumber.jpg",
"text": "Hör Genau Hin, mache dabei die Augen zu! Was hörst du?",
"image": "cucumber.jpg",
"choices": [ "DADA", "DATA", "TADA", "BABA" ],
"media": {
"type": "video",
......@@ -14,8 +14,8 @@
},
{
"type": "multiple-choice",
"image": "cucumber.jpg",
"text": "Hör Genau Hin, und schaue den Jungen dabei gut zu! Was hörst du?",
"image": "cucumber.jpg",
"choices": [ "DADA", "DATA", "TADA", "BABA" ],
"media": {
"type": "video",
......
......@@ -131,7 +131,7 @@ insert into media (id, kind, payload) values
(7, "audio", "Saskia_Schwimmen.wav");
insert into question (id, quiz_id, kind, image_id, media_id, text) values
(8, 3, "multiple-choice", 4, 7, "Wie fühlt sich der Sprecher?");
(8, 3, "multiple-choice", 4, 7, "Wie fühlt sich die Sprecherin?");
insert into choice (question_id, text) values
(8, "🙁"),
......
......@@ -2,20 +2,19 @@
(:require [clojure.java.jdbc :as jdbc]
[mount.core :as mount]))
(def spec
{:classname "org.sqlite.JDBC"
:subprotocol "sqlite"
:subname ":memory:"})
(def db-uri "jdbc:sqlite::memory:")
(declare db)
(defn on-start []
(let [conn (jdbc/get-connection spec)]
(assoc spec :connection conn)
(jdbc/execute! spec
"create table answer (id integer primary key autoincrement, quiz integer, question integer, answer text)")
(jdbc/execute! spec
"create table recording (id integer primary key autoincrement, description text, data blob)")))
(let [spec {:connection-uri db-uri}
conn (jdbc/get-connection spec)
db (assoc spec :connection conn)]
(jdbc/execute! db
"create table answer (id integer primary key autoincrement, quiz_id text, question_id integer, value text)")
(jdbc/execute! db
"create table recording (id integer primary key autoincrement, description text, data blob)")
db))
(defn on-stop []
(-> db :connection .close)
......@@ -31,9 +30,11 @@
(defn answer! [question-id choice]
(jdbc/insert! db :answer {:question_id question-id
:choice choice}))
(defn answer! [quiz question value]
(jdbc/insert! db :answer
{:quiz_id (:id quiz)
:question_id (:id question)
:value value}))
(defn answers-for [question-id]
(jdbc/query db ["select * from answer where question_id = ?" question-id]))
......
......@@ -16,14 +16,11 @@
:start ((or (:init defaults) (fn [])))
:stop ((or (:stop defaults) (fn []))))
(defn all-quiz-routes []
["/quiz" routes])
(mount/defstate app-routes
:start
(ring/ring-handler
(ring/router
[(home-routes) (all-quiz-routes) (record-routes)])
[(home-routes) (routes) (record-routes)])
(ring/routes
(ring/create-resource-handler
{:path "/"})
......
(ns tunein.quiz
(:require [clojure.java.io :as io]
[clojure.data.json :as json]))
(defn- read-json-resource [path]
"Reads a resource and parses it as JSON."
(let [resource-url (io/resource path)
content (slurp resource-url)]
(json/read-str content :key-fn keyword)))
(defn- pretty-hash [datum]
"A pretty-printed (read: hex) hash."
(format "%08x" (hash datum)))
(defn- assoc-hash-id [datum]
(let [h (pretty-hash datum)]
[h (assoc datum :id h)]))
(defn- assoc-seq-id [idx datum]
(assoc datum :id idx))
(defn- read-quiz [filename]
"Reads a quiz definition file."
(-> (str "quizzes/" filename)
read-json-resource
(update :questions
#(into [] (map-indexed assoc-seq-id %)))
assoc-hash-id))
(def quizzes
"A content-addressable map of available quizzes.
The key (hash of the quiz data) is used to uniquely identify a given quiz,
without using an increasing series of indices."
(let [listing (read-json-resource "quizzes/listing.json")]
(into {} (map read-quiz listing))))
(defn validate-answer [question answer]
"Checks whether the given answer is within the choices available.
In case of free-form questions, it always returns true.
In case the question is neither multiple-choice nor free-form, returns nil."
(case (:type question)
"multiple-choice" (some #(= answer %) (:choices question))
"free-form" true
nil))
......@@ -6,21 +6,12 @@
[ring.util.http-response :as response]
[tunein.layout :as layout]
[tunein.middleware :as middleware]
[tunein.db.core :as db]))
(defn- read-json-resource [path]
(let [res (io/resource path)
content (slurp res)]
(json/read-str content :key-fn keyword)))
(def quizzes
(let [listing (read-json-resource "quizzes/listing.json")
read (fn [filename] (read-json-resource (str "quizzes/" filename)))]
(into [] (map read listing))))
[tunein.db.core :as db]
[tunein.quiz :as quiz]))
(defn home-page [request]
(layout/render request "things.html"
{:quizzes quizzes
{:quizzes (vals quiz/quizzes)
:title "Stationen"}))
(defn about-page [request]
......
......@@ -7,81 +7,73 @@
[reitit.ring :as ring]
[tunein.layout :as layout]
[tunein.middleware :as middleware]
[tunein.db.core :as db]))
(defn- read-json-resource [path]
(let [res (io/resource path)
content (slurp res)]
(json/read-str content :key-fn keyword)))
(def quizzes
(let [listing (read-json-resource "quizzes/listing.json")
read (fn [filename] (read-json-resource (str "quizzes/" filename)))]
(into [] (map read listing))))
[tunein.db.core :as db]
[tunein.quiz :as quiz]))
(defn- render-question-list [quiz]
(fn [request]
(layout/render request "quiz/question-list.html" quiz)))
(layout/render request "quiz/question-list.html"
{:quiz quiz :backlink "/"})))
(defn- render-question [quiz question]
(fn [request]
(layout/render request "quiz/question.html"
{:title (:title quiz)
:question question})))
(layout/render request "quiz/question.html"
{:title (:title quiz)
:quiz quiz
:question question
:backlink (str "/quiz/" (:id quiz))})))
(defn- answer-multiple-choice-question [quiz question answer]
(let [choices (:choices question)
value (try (Integer/parseInt answer)
(catch NumberFormatException _ nil))]
(if-let [choice (first (filter #(= value (:id %)) choices))]
(do
(db/answer! (hash question) (:text choice))
{:status 301
:headers {"Location"
(str "/quiz/" (hash quiz) "/results/" (hash question))}})
{:status 500
:headers {"Content-Type" "text/plain"}
:body "unacceptable answer"})))
(if (some #(= answer %) (:choices question))
(do
(db/answer! quiz question answer)
{:status 301
:headers {"Location"
(str "/quiz/" (:id quiz) "/question/" (:id question) "/results")}})
{:status 500
:headers {"Content-Type" "text/plain"}
:body "unacceptable answer"}))
(defn- answer-free-form-question [quiz question answer]
(db/answer! (hash question) answer)
(db/answer! (:id question) answer)
{:status 301
:headers {"Location"
(str "/quiz/" (hash quiz) "/results/" (hash question))}})
(str "/quiz/" (:id quiz) "/results/" (:id question))}})
(defn- answer-question [quiz question]
(fn [request]
(let [answer (-> request :form-params (get "choice"))]
(case (:type question)
"multiple-choice" (answer-multiple-choice-question quiz question answer)
"free-form" (answer-free-form-question quiz question answer)))))
"multiple-choice"
(answer-multiple-choice-question quiz question answer)
"free-form"
(answer-free-form-question quiz question answer)))))
(defn- render-question-results [quiz question]
(fn [request]
(let [answers (db/answers-for (hash question))
results (->> (map :choice answers) frequencies)]
(let [answers (db/answers-for (:id question))
results (->> (map :value answers) frequencies)]
(layout/render request "quiz/results.html"
{:title (:title quiz)
:question (:text question)
:results results}))))
:results results
:backlink (str "/quiz/" (:id quiz))}))))
(defn- question-routes [quiz question]
(let [base-path (str "/question/" (hash question))]
(let [base-path (str "/question/" (:id question))]
[[base-path
{:get (render-question quiz question)
:post (answer-question quiz question)}]
[(str base-path "/results")
{:get render-question-results quiz question}]]))
{:get (render-question-results quiz question)}]]))
(defn quiz-routes [quiz]
(concat
[(str "/" (hash quiz)) ["" {:get (render-question-list quiz)}]]
(map (fn [question] (question-routes quiz question)) (:questions quiz))))
(into [(str "/" (:id quiz)) ["" {:get (render-question-list quiz)}]]
(map #(question-routes quiz %) (:questions quiz))))
(defn routes []
(into [] (map quiz-routes quizzes)))
(into ["/quiz"] (map quiz-routes (vals quiz/quizzes))))