Compare commits
18 Commits
c19a7941fa
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
58932cdd7b
|
|||
| 366d368446 | |||
| 4fd9d1eb81 | |||
|
872e10ba08
|
|||
|
ca644a6e66
|
|||
|
5cb2182f4c
|
|||
|
d25f7f2f0c
|
|||
|
3f2fe5c9a1
|
|||
|
095716c2eb
|
|||
|
0a9055431f
|
|||
|
bfb7f477af
|
|||
|
27f8b6d5d0
|
|||
|
99da9da68a
|
|||
| 64ca89eefb | |||
| 314c9e8c67 | |||
| 6cdf7dbb4d | |||
| bcbf149c6c | |||
| e3a15ac60f |
@ -3,6 +3,13 @@ FS Quiz Tool question format
|
||||
|
||||
This document provides examples for the question format
|
||||
|
||||
The web tool will accept these in TSV format, as copied from a spreadsheet like Google Sheets. One Question is one row of the TSV, and each column is one of the following fields, like:
|
||||
|
||||
Input Type | Question | Choices/Answer Options | Answers | Explanation | Author | Picture Link | Time in minutes
|
||||
-----------|----------|-------------------------|---------|-------------|--------|-----------------|----------------
|
||||
ChooseOne | ... | a<br>b | 2 | ... | ... | https://url.jpg | 3
|
||||
Text | ... | Answer in W, 2 decimals | 4.20 | ... | ... | | 5
|
||||
|
||||
## ChooseOne
|
||||
|
||||
Single-Choice question
|
||||
|
||||
@ -12,3 +12,7 @@ python3 -m fs-quiz-tool-db
|
||||
```
|
||||
|
||||
Then, open <http://localhost:12345> in your browser.
|
||||
|
||||
## License
|
||||
|
||||
See LICENSE.md
|
||||
@ -19,8 +19,11 @@ def startup():
|
||||
try:
|
||||
fd = open(filename)
|
||||
data = json.load(fd)
|
||||
fd.close()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
print("DB file not found, creating on POST/write.")
|
||||
|
||||
print("Read", len(data), "quizzes from DB")
|
||||
|
||||
|
||||
@app.route('/db/<id>', methods = ['GET'])
|
||||
@ -45,20 +48,16 @@ def post():
|
||||
id = generateNewKey()
|
||||
data[id] = request.data.decode('UTF-8')
|
||||
|
||||
return id, 201
|
||||
|
||||
|
||||
def dumpDB():
|
||||
|
||||
if len(data) == 0:
|
||||
return
|
||||
print("Received new quiz. ID:", id)
|
||||
|
||||
try:
|
||||
fd = open(filename, 'w+')
|
||||
json.dump(data, fd)
|
||||
finally:
|
||||
json.dump(data, fd, indent=2)
|
||||
fd.close()
|
||||
except e:
|
||||
print("Failed to open/create DB file for writing:", e)
|
||||
|
||||
return id, 201
|
||||
|
||||
|
||||
startup()
|
||||
atexit.register(dumpDB)
|
||||
|
||||
@ -3,7 +3,7 @@ server {
|
||||
|
||||
server_name quiz.fasttube.de;
|
||||
|
||||
root /root/fs-quiz-tool/web;
|
||||
root /opt/fs-quiz-tool/web;
|
||||
|
||||
index index.html;
|
||||
|
||||
@ -15,8 +15,6 @@ server {
|
||||
|
||||
location /db {
|
||||
proxy_pass http://127.0.0.1:12345;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
ssl_certificate /usr/local/etc/letsencrypt/live/quiz.fasttube.de/fullchain.pem;
|
||||
|
||||
29
res/fs-quiz-tool.openrc
Normal file
29
res/fs-quiz-tool.openrc
Normal file
@ -0,0 +1,29 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
name="$RC_SVCNAME"
|
||||
directory="/opt/fs-quiz-tool"
|
||||
command="/opt/fs-quiz-tool/.venv/bin/python"
|
||||
command_args="-m fs-quiz-tool-db"
|
||||
command_user="www"
|
||||
pidfile="/run/$RC_SVCNAME/$RC_SVCNAME.pid"
|
||||
command_background="yes"
|
||||
|
||||
depend() {
|
||||
need net
|
||||
}
|
||||
|
||||
start_pre() {
|
||||
checkpath --directory --owner $command_user:$command_user --mode 0775 \
|
||||
/run/$RC_SVCNAME /var/log/$RC_SVCNAME
|
||||
}
|
||||
|
||||
start() {
|
||||
ebegin "Starting ${RC_SVCNAME}"
|
||||
start-stop-daemon --start --exec ${command} \
|
||||
--background --make-pidfile --pidfile ${pidfile} \
|
||||
--chdir ${directory} \
|
||||
--stdout /var/log/fs-quiz-tool/out.log \
|
||||
--stderr /var/log/fs-quiz-tool/err.log \
|
||||
-- ${command_args}
|
||||
eend $?
|
||||
}
|
||||
@ -4,6 +4,9 @@ After=syslog.target network.target nginx.service
|
||||
|
||||
[Service]
|
||||
User=www-data
|
||||
Group=www-data
|
||||
ReadWriteDirectories=/var/fs-quiz-tool-db
|
||||
WorkingDirectory=/var/fs-quiz-tool-db
|
||||
ExecStart=/usr/bin/python3 -m fs-quiz-tool-db
|
||||
Restart=always
|
||||
|
||||
|
||||
@ -91,7 +91,11 @@
|
||||
</div>
|
||||
<form>
|
||||
</form>
|
||||
<div id="fsastuff">
|
||||
<div id="fsabonuspointsinfo"></div>
|
||||
<div id="fsateamcounttroll"></div>
|
||||
<div id="fsapointbar"><div></div></div>
|
||||
</div>
|
||||
<input type="button" value="Submit" onclick="submitQuiz()" style="background: #008029" id="quizSubmitButton">
|
||||
<input type="button" value="Abort" onclick="abortQuiz()">
|
||||
<span id="submitinfo"></span>
|
||||
|
||||
BIN
web/nyan.png
Normal file
BIN
web/nyan.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
146
web/quiz.js
146
web/quiz.js
@ -5,17 +5,16 @@ function showFSATeamCountTroll() {
|
||||
|
||||
}
|
||||
|
||||
function updateFSATeamCountTroll() {
|
||||
function updateFSATeamCountTroll(time) {
|
||||
|
||||
if (state.fsaTeamCountTroll > (128*Math.random())) {
|
||||
showFSATeamCountTroll()
|
||||
return
|
||||
}
|
||||
|
||||
var time = state.questions[state.currentQuestion].time
|
||||
var timeratio = 1 - (state.submitTimer / time)
|
||||
var timeratio = 1 - state.submitTimer / time
|
||||
var slowAnswerChance = Math.random()*2*Math.pow(timeratio, 2)
|
||||
var quickAnswerChance = (Math.random()+Math.sqrt(69/time))/3
|
||||
var quickAnswerChance = (Math.random()+Math.sqrt(69/time))/4
|
||||
var chance = Math.round(slowAnswerChance + quickAnswerChance)
|
||||
var magnitude = Math.round((Math.random()/2+timeratio)*6)
|
||||
var nt = chance * magnitude
|
||||
@ -25,19 +24,46 @@ function updateFSATeamCountTroll() {
|
||||
|
||||
}
|
||||
|
||||
function updateFSABonusPoints(time) {
|
||||
|
||||
if (state.waitNextQuestion)
|
||||
return
|
||||
|
||||
var bonuspart = (state.submitTimer - 0.333 * time) / (time * 0.666)
|
||||
|
||||
var barel = document.querySelector('#fsapointbar > div')
|
||||
barel.style.width = Math.round(bonuspart*100) + '%'
|
||||
|
||||
var bonusinfoel = document.getElementById('fsabonuspointsinfo')
|
||||
var points = Math.round(bonuspart*10)
|
||||
bonusinfoel.innerHTML = ((points > 0) ? points : 'No') + ' Bonus points'
|
||||
|
||||
}
|
||||
|
||||
function updateFSAStuff() {
|
||||
|
||||
// `|| state.submitTime` is a workaround for saved Qs that don't have a time set yet
|
||||
var time = state.questions[state.currentQuestion].time || state.submitTime
|
||||
|
||||
updateFSABonusPoints(time)
|
||||
updateFSATeamCountTroll(time)
|
||||
|
||||
}
|
||||
|
||||
function updateTotalTimer() {
|
||||
|
||||
var timerEls = document.querySelectorAll('.totaltimer')
|
||||
|
||||
for (timerEl of timerEls)
|
||||
timerEl.innerHTML = formatTime(state.totalTimer);
|
||||
timerEl.innerHTML = 'Total time: ' + formatTime(state.totalTimer);
|
||||
|
||||
if (!state.waitNextQuestion)
|
||||
state.totalTimer++
|
||||
|
||||
localStorage.setItem('state', JSON.stringify(state))
|
||||
|
||||
if (state.style == 'FSA')
|
||||
updateFSATeamCountTroll()
|
||||
updateFSAStuff()
|
||||
|
||||
}
|
||||
|
||||
@ -52,6 +78,18 @@ function startTotalTimer() {
|
||||
|
||||
}
|
||||
|
||||
function skipWaitNextQuestion(event) {
|
||||
|
||||
if (!event.shiftKey)
|
||||
return
|
||||
|
||||
console.log('Skipping/Bypassing wait timer')
|
||||
|
||||
state.submitTimer = 0
|
||||
updateSubmitTimer()
|
||||
|
||||
}
|
||||
|
||||
function updateSubmitInfo() {
|
||||
|
||||
var button = document.getElementById('quizSubmitButton')
|
||||
@ -61,29 +99,51 @@ function updateSubmitInfo() {
|
||||
if (state.submitTimer > 0) {
|
||||
si.innerHTML = state.waitNextQuestion ? 'Waiting for next question' : 'Wait to retry'
|
||||
button.value = 'Wait ' + formatTime(state.submitTimer)
|
||||
button.disabled = true
|
||||
button.readOnly = true
|
||||
button.addEventListener('click', skipWaitNextQuestion)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
si.innerHTML = ''
|
||||
button.disabled = false
|
||||
button.readOnly = false
|
||||
button.removeEventListener('click', skipWaitNextQuestion)
|
||||
|
||||
if (getRule('questionTimeout')) {
|
||||
if (state.submitTimer > 0) {
|
||||
if (getRule('allowQOvertime'))
|
||||
si.innerHTML = ('Losing bonus points in ' + formatTime(state.submitTimer))
|
||||
si.innerHTML = ('Next question timer starts in ' + formatTime(state.submitTimer))
|
||||
else
|
||||
si.innerHTML = ('Forced hand-in in ' + formatTime(state.submitTimer))
|
||||
si.innerHTML = ( 'Question failed in ' + formatTime(state.submitTimer))
|
||||
} else {
|
||||
if (getRule('allowQOvertime'))
|
||||
document.getElementById('submitinfo').innerHTML = 'Bonus points lost.'
|
||||
document.getElementById('submitinfo').innerHTML = 'Next question timer started.'
|
||||
}
|
||||
}
|
||||
|
||||
var lastQuestion = (state.currentQuestion == (state.questions.length - 1))
|
||||
|
||||
button.value = lastQuestion || !getRule('sequential') ? 'Submit Answers' : 'Next Question'
|
||||
button.value = lastQuestion || !getRule('sequential') ? 'Submit Answers' :
|
||||
((state.submitTimer > 0) ? 'Submit' : 'Next Question')
|
||||
|
||||
}
|
||||
|
||||
function resetQuestionResponse(qn) {
|
||||
|
||||
var q = document.querySelector('#quiz form #question' + qn)
|
||||
var inputs = q.querySelectorAll('input')
|
||||
|
||||
for (input of inputs) {
|
||||
switch (input.type) {
|
||||
case 'radio':
|
||||
case 'checkbox':
|
||||
input.checked = false
|
||||
break;
|
||||
case 'text':
|
||||
input.value = ""
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -96,9 +156,15 @@ function updateSubmitTimer() {
|
||||
|
||||
clearInterval(state.submitInterval)
|
||||
|
||||
if (getRule('questionTimeout'))
|
||||
if (state.waitNextQuestion || !getRule('allowQOvertime'))
|
||||
if (getRule('questionTimeout') && !getRule('allowQOvertime')) {
|
||||
|
||||
if (!state.waitNextQuestion) {
|
||||
console.log("Question not submitted, clearing input");
|
||||
resetQuestionResponse(state.currentQuestion);
|
||||
}
|
||||
|
||||
submitQuiz() // Force next question
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -122,7 +188,7 @@ function startSubmitTimer(time) {
|
||||
}
|
||||
|
||||
|
||||
function showSequentialQuestion(n) {
|
||||
function updateSequentialQuestion() {
|
||||
|
||||
var questions = document.querySelectorAll('.question')
|
||||
for (q of questions)
|
||||
@ -130,8 +196,8 @@ function showSequentialQuestion(n) {
|
||||
|
||||
updateSubmitInfo()
|
||||
|
||||
if (n !== null) {
|
||||
var q = document.querySelector('#quiz form #question' + n)
|
||||
if (state.currentQuestion !== null && !state.waitNextQuestion) {
|
||||
var q = document.querySelector('#quiz form #question' + state.currentQuestion)
|
||||
q.style.display = 'block'
|
||||
}
|
||||
|
||||
@ -142,10 +208,17 @@ function nextQuestion() {
|
||||
// Save time taken
|
||||
state.questions[state.currentQuestion].timeTaken = state.totalTimer - state.questionStartTotalTimer
|
||||
|
||||
if (getRule('questionTimeout') && (state.submitTimer > 0)
|
||||
&& (state.currentQuestion != (state.questions.length-1))) {
|
||||
// Last question
|
||||
if (state.currentQuestion == (state.questions.length - 1)) {
|
||||
state.currentQuestion = null
|
||||
updateSequentialQuestion()
|
||||
return
|
||||
}
|
||||
|
||||
// Waiting for next question
|
||||
if (getRule('questionTimeout') && (state.submitTimer > 0)) {
|
||||
state.waitNextQuestion = true;
|
||||
showSequentialQuestion(null) // hide question
|
||||
updateSequentialQuestion() // hide question
|
||||
return
|
||||
}
|
||||
|
||||
@ -154,6 +227,9 @@ function nextQuestion() {
|
||||
if (state.style == 'FSA') {
|
||||
state.fsaTeamCountTroll = 0
|
||||
showFSATeamCountTroll()
|
||||
// a lil' surprise
|
||||
document.querySelector('#fsapointbar > div').style.background =
|
||||
(Math.random() < 0.05) ? "left / auto 100% url('nyan.png')" : "darkred"
|
||||
}
|
||||
|
||||
// Start next question
|
||||
@ -161,12 +237,9 @@ function nextQuestion() {
|
||||
|
||||
state.currentQuestion++
|
||||
|
||||
if (state.currentQuestion == state.questions.length)
|
||||
state.currentQuestion = null
|
||||
updateSequentialQuestion()
|
||||
|
||||
showSequentialQuestion(state.currentQuestion)
|
||||
|
||||
if (state.currentQuestion !== null && getRule('questionTimeout'))
|
||||
if (getRule('questionTimeout'))
|
||||
startSubmitTimer()
|
||||
|
||||
}
|
||||
@ -216,6 +289,9 @@ function renderQuiz() {
|
||||
|
||||
}
|
||||
|
||||
var fsastuffel = document.getElementById('fsastuff')
|
||||
fsastuffel.style.display = (state.style == 'FSA') ? 'block' : 'none'
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -231,7 +307,7 @@ function startQuiz() {
|
||||
changeView('quiz')
|
||||
|
||||
if (getRule('sequential'))
|
||||
showSequentialQuestion(state.currentQuestion)
|
||||
updateSequentialQuestion()
|
||||
|
||||
if (state.submitTimer > 0)
|
||||
startSubmitTimer(state.submitTimer)
|
||||
@ -262,6 +338,7 @@ function reStartQuiz() {
|
||||
state.totalTimer = defaultState.totalTimer
|
||||
state.totalInterval = defaultState.totalInterval
|
||||
state.fsaTeamCountTroll = defaultState.fsaTeamCountTroll
|
||||
state.resultsShown = defaultState.resultsShown
|
||||
|
||||
changeView('prescreen')
|
||||
|
||||
@ -331,6 +408,10 @@ function showQuizResults() {
|
||||
|
||||
changeView('quiz')
|
||||
|
||||
// Prevent result metadata being added more than once
|
||||
if (state.resultsShown)
|
||||
return
|
||||
|
||||
for (var [idx, value] of state.responses.entries()) {
|
||||
|
||||
console.log(idx + ':', value)
|
||||
@ -372,13 +453,17 @@ function showQuizResults() {
|
||||
qinfo += ('. Time taken: ' + formatTime(q.timeTaken))
|
||||
if (value.correct && getRule('allowQOvertime'))
|
||||
qinfo += (', Bonus points ' + ((q.timeTaken <= q.time) ? 'received' : 'lost'))
|
||||
else if (q.timeTaken == q.time)
|
||||
else if (q.timeTaken >= (q.time - 1) && !getRule('allowQOvertime'))
|
||||
qinfo += ', Time ran out'
|
||||
}
|
||||
el.querySelector('h3').innerHTML += ` <span class="meta">${qinfo}</span>`
|
||||
|
||||
}
|
||||
|
||||
state.resultsShown = true
|
||||
|
||||
document.querySelector('#fsateamcounttroll').innerHTML = ''
|
||||
document.querySelector('#submitinfo').innerHTML = ''
|
||||
document.querySelector('#quizSubmitButton').value = 'Back'
|
||||
|
||||
}
|
||||
@ -467,7 +552,7 @@ function submitQuiz() {
|
||||
renderQuiz()
|
||||
if (getRule('sequential')) {
|
||||
state.currentQuestion = 0
|
||||
showSequentialQuestion(state.currentQuestion)
|
||||
updateSequentialQuestion()
|
||||
}
|
||||
|
||||
if (getRule('submitTimeout'))
|
||||
@ -547,6 +632,11 @@ window.onload = async function() {
|
||||
state.submitTime = quiz.submitTime
|
||||
state.id = urlId
|
||||
|
||||
// Workaround for quizzes that were saved before the fix
|
||||
// Where the stored value is null instead of Infinity
|
||||
if (state.style == 'FSCzech')
|
||||
state.submitTries = Infinity
|
||||
|
||||
changeView('prescreen')
|
||||
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ async function shareQuiz() {
|
||||
var response = await fetch(db, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'text/plain'},
|
||||
body: JSON.stringify(quizData)
|
||||
body: JSON.stringify(quizData, (k,v) => (v == Infinity) ? '__Infinity' : v)
|
||||
})
|
||||
|
||||
if (response.ok == false) {
|
||||
@ -92,7 +92,9 @@ async function fetchQuiz(id) {
|
||||
return
|
||||
}
|
||||
|
||||
var json = await response.json()
|
||||
var text = await response.text()
|
||||
|
||||
var json = JSON.parse(text, (k,v) => (v == '__Infinity') ? Infinity : v)
|
||||
|
||||
console.log('Quiz fetched. Response:')
|
||||
console.log(json)
|
||||
|
||||
@ -84,7 +84,7 @@ input[type="button"].center, input[type="submit"].center {
|
||||
float: none;
|
||||
}
|
||||
|
||||
input[type="button"]:disabled, input[type="submit"]:disabled {
|
||||
input[type="button"][readonly], input[type="submit"][readonly] {
|
||||
cursor: auto;
|
||||
background: #888 !important;
|
||||
}
|
||||
@ -189,8 +189,31 @@ input[type="radio"]:hover + p, input[type="checkbox"]:hover + p {
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
#fsastuff {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#fsabonuspointsinfo {
|
||||
float: left;
|
||||
font-size: 15px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#fsateamcounttroll {
|
||||
text-align: right;
|
||||
font-size: 15px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#fsapointbar {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
border: 1px solid black
|
||||
}
|
||||
|
||||
#fsapointbar > div {
|
||||
height: 100%;
|
||||
background: darkred;
|
||||
width: 100%;
|
||||
transition: 1s linear width;
|
||||
}
|
||||
|
||||
@ -16,7 +16,8 @@ const defaultState = {
|
||||
questionStartTotalTimer: 0,
|
||||
totalTimer: 0,
|
||||
totalInterval: null,
|
||||
fsaTeamCountTroll: 0
|
||||
fsaTeamCountTroll: 0,
|
||||
resultsShown: false
|
||||
}
|
||||
|
||||
var state
|
||||
@ -49,7 +50,7 @@ var rules = {
|
||||
'FSA' : { sequential: true, questionTimeout: 5, allowQOvertime: true },
|
||||
'FSN' : { sequential: true },
|
||||
'FSEast' : { sequential: false },
|
||||
'FSCzech' : { sequential: false, submitTries: Infinity, submitTimeout: 30 },
|
||||
'FSCzech' : { sequential: false, submitTries: Infinity, submitTimeout: 60 },
|
||||
'FSSpain' : { sequential: true, submitTries: 10 },
|
||||
'FSSwitzerland' : { sequential: true },
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user