Compare commits

...

18 Commits

Author SHA1 Message Date
58932cdd7b Add OpenRC config and update nginx conf 2025-11-07 10:53:10 +01:00
366d368446 Refer to license 2023-11-29 14:12:11 +01:00
4fd9d1eb81 Explain TSV field 2023-11-29 14:10:54 +01:00
872e10ba08 Fix regression resetting inputs after wait
The wait timer for the next question would trigger the reset of inputs
because FSG thought the question was over without a question submitted
2023-11-22 20:38:53 +01:00
ca644a6e66 Update systemd service file 2023-11-22 20:38:33 +01:00
5cb2182f4c Add FSA progress bar and some bonus point counting
Resolves #14
2023-01-31 00:03:57 +01:00
d25f7f2f0c FSG: Not activey submitting will always be regarded as no answer (fail)
resolves #12
2023-01-30 22:15:18 +01:00
3f2fe5c9a1 Set default FSCzech wait time to 60s. Resolves #15 2023-01-28 23:11:18 +01:00
095716c2eb Fix #11 by remembering whether results are shown in state 2022-01-15 22:33:54 +01:00
0a9055431f Skip question using only shift+click because ctrl is hard on mac/safari 2022-01-03 20:53:17 +01:00
bfb7f477af Only close files when they've been opened 2021-11-11 19:33:45 +01:00
27f8b6d5d0 Add debug statements 2021-11-11 19:03:45 +01:00
99da9da68a Write to FS on every POST to prevent data loss on server crash 2021-11-11 18:56:36 +01:00
64ca89eefb Introduce timer skip/bypass using Ctrl+Shift (Resolves #10) 2021-02-09 12:02:42 +01:00
314c9e8c67 Fix total time counting for timed questions and lots of cleanup 2021-01-23 19:47:19 +01:00
6cdf7dbb4d FSA counter workaround for questions without time 2021-01-23 11:16:32 +01:00
bcbf149c6c Add workaround so the fsczech quizzes saved before work again 2021-01-23 11:05:03 +01:00
e3a15ac60f Fix bug that didn't allow infinity values to be saved 2021-01-23 11:02:11 +01:00
12 changed files with 210 additions and 50 deletions

View File

@ -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

View File

@ -12,3 +12,7 @@ python3 -m fs-quiz-tool-db
```
Then, open <http://localhost:12345> in your browser.
## License
See LICENSE.md

View File

@ -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)

View File

@ -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
View 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 $?
}

View File

@ -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

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -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 += ` &nbsp; <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')
}

View File

@ -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)

View File

@ -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;
}

View File

@ -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 },
}