Merge remote-tracking branch 'refs/remotes/origin/master'

This commit is contained in:
Vivek Santayana 2022-08-18 16:33:42 +01:00
commit 4caac25b14
37 changed files with 1742 additions and 491 deletions

View File

@ -10,7 +10,7 @@ The exam client is made with accessibility in mind, and has been designed to be
## Set Up and Installation ## Set Up and Installation
The clien is designed to work on a server. The app is designed to be hosted on a server.
### Pre-Requisites ### Pre-Requisites

View File

@ -19,6 +19,7 @@ server {
include /etc/nginx/ssl.conf; include /etc/nginx/ssl.conf;
include /etc/nginx/certbot-challenge.conf; include /etc/nginx/certbot-challenge.conf;
# Define locations for static files to be served by Nginx
location ^~ /quiz/static/ { location ^~ /quiz/static/ {
include /etc/nginx/mime.types; include /etc/nginx/mime.types;
alias /usr/share/nginx/html/quiz/static/; alias /usr/share/nginx/html/quiz/static/;
@ -34,6 +35,12 @@ server {
alias /usr/share/nginx/html/admin/editor/static/; alias /usr/share/nginx/html/admin/editor/static/;
} }
location ^~ /admin/view/static/ {
include /etc/nginx/mime.types;
alias /usr/share/nginx/html/admin/view/static/;
}
# Proxy to the main app for all other requests
location / { location / {
include /etc/nginx/conf.d/proxy_headers.conf; include /etc/nginx/conf.d/proxy_headers.conf;
proxy_pass http://reftest; proxy_pass http://reftest;

View File

@ -46,11 +46,13 @@ def create_app():
from .quiz.views import quiz from .quiz.views import quiz
from .views import views from .views import views
from .editor.views import editor from .editor.views import editor
from .view.views import view
app.register_blueprint(admin, url_prefix='/admin') app.register_blueprint(admin, url_prefix='/admin')
app.register_blueprint(api, url_prefix='/api') app.register_blueprint(api, url_prefix='/api')
app.register_blueprint(views) app.register_blueprint(views)
app.register_blueprint(quiz) app.register_blueprint(quiz)
app.register_blueprint(editor, url_prefix='/admin/editor') app.register_blueprint(editor, url_prefix='/admin/editor')
app.register_blueprint(view, url_prefix='/admin/view')
return app return app

View File

@ -1,25 +1,25 @@
// Menu Highlight Scripts // Menu Highlight Scripts
const menuItems = document.getElementsByClassName('nav-link'); const menuItems = document.getElementsByClassName('nav-link')
for(let i = 0; i < menuItems.length; i++) { for(let i = 0; i < menuItems.length; i++) {
if(menuItems[i].pathname == window.location.pathname) { if(menuItems[i].pathname == window.location.pathname) {
menuItems[i].classList.add('active'); menuItems[i].classList.add('active')
} }
} }
const dropdownItems = document.getElementsByClassName('dropdown-item'); const dropdownItems = document.getElementsByClassName('dropdown-item')
for(let i = 0; i< dropdownItems.length; i++) { for(let i = 0; i< dropdownItems.length; i++) {
if(dropdownItems[i].pathname == window.location.pathname) { if(dropdownItems[i].pathname == window.location.pathname) {
dropdownItems[i].classList.add('active'); dropdownItems[i].classList.add('active')
$( "#" + dropdownItems[i].id ).closest( '.dropdown' ).find('.dropdown-toggle').addClass('active'); $( "#" + dropdownItems[i].id ).closest( '.dropdown' ).find('.dropdown-toggle').addClass('active')
} }
} }
// General Post Method Form Processing Script // General Post Method Form Processing Script
$('form.form-post').submit(function(event) { $('form.form-post').submit(function(event) {
var $form = $(this); var $form = $(this)
var data = $form.serialize(); var data = $form.serialize()
var url = $(this).prop('action'); var url = $(this).prop('action')
var rel_success = $(this).data('rel-success'); var rel_success = $(this).data('rel-success')
$.ajax({ $.ajax({
url: url, url: url,
@ -28,25 +28,25 @@ $('form.form-post').submit(function(event) {
dataType: 'json', dataType: 'json',
success: function(response) { success: function(response) {
if (response.redirect_to) { if (response.redirect_to) {
window.location.href = response.redirect_to; window.location.href = response.redirect_to
} }
else { else {
window.location.href = rel_success; window.location.href = rel_success
} }
}, },
error: function(response) { error: function(response) {
error_response(response); error_response(response)
} }
}); })
event.preventDefault(); event.preventDefault()
}); })
// Form Upload Questions - Special case, needs to handle files. // Form Upload Questions - Special case, needs to handle files.
$('form[name=form-upload-questions]').submit(function(event) { $('form[name=form-upload-questions]').submit(function(event) {
var $form = $(this); var $form = $(this)
var data = new FormData($form[0]); var data = new FormData($form[0])
var file = $('input[name=data_file]')[0].files[0] var file = $('input[name=data_file]')[0].files[0]
data.append('file', file) data.append('file', file)
@ -57,21 +57,21 @@ $('form[name=form-upload-questions]').submit(function(event) {
processData: false, processData: false,
contentType: false, contentType: false,
success: function(response) { success: function(response) {
window.location.reload(); window.location.reload()
}, },
error: function(response) { error: function(response) {
error_response(response); error_response(response)
} }
}); })
event.preventDefault(); event.preventDefault()
}); })
// Edit and Delete Test Button Handlers // Edit and Delete Test Button Handlers
$('.test-action').click(function(event) { $('.test-action').click(function(event) {
let id = $(this).data('id'); let id = $(this).data('id')
let action = $(this).data('action'); let action = $(this).data('action')
if (action == 'delete' || action == 'start' || action == 'end') { if (action == 'delete' || action == 'start' || action == 'end') {
$.ajax({ $.ajax({
@ -80,25 +80,25 @@ $('.test-action').click(function(event) {
data: JSON.stringify({'id': id, 'action': action}), data: JSON.stringify({'id': id, 'action': action}),
contentType: 'application/json', contentType: 'application/json',
success: function(response) { success: function(response) {
window.location.href = '/admin/tests/'; window.location.href = '/admin/tests/'
}, },
error: function(response){ error: function(response){
error_response(response); error_response(response)
}, },
}); })
} else if (action == 'edit') { } else if (action == 'edit') {
window.location.href = `/admin/test/${id}/` window.location.href = `/admin/test/${id}/`
} }
event.preventDefault(); event.preventDefault()
}); })
// Edit Dataset Button Handlers // Edit Dataset Button Handlers
$('.edit-question-dataset').click(function(event) { $('.edit-question-dataset').click(function(event) {
var id = $(this).data('id'); var id = $(this).data('id')
var action = $(this).data('action'); var action = $(this).data('action')
var disabled = $(this).hasClass('disabled'); var disabled = $(this).hasClass('disabled')
if ( !disabled ) { if ( !disabled ) {
if (action == 'delete') { if (action == 'delete') {
@ -111,25 +111,27 @@ $('.edit-question-dataset').click(function(event) {
}), }),
contentType: 'application/json', contentType: 'application/json',
success: function(response) { success: function(response) {
window.location.reload(); window.location.reload()
}, },
error: function(response){ error: function(response){
error_response(response); error_response(response)
}, },
}); })
} else if (action == 'edit') { } else if (action == 'edit') {
window.location.href = `/admin/editor/${id}/` window.location.href = `/admin/editor/${id}/`
} else if (action == 'view') {
window.location.href = `/admin/view/${id}`
} else if (action == 'download') { } else if (action == 'download') {
window.location.href = `/admin/settings/questions/download/${id}/` window.location.href = `/admin/settings/questions/download/${id}/`
} }
}; }
event.preventDefault(); event.preventDefault()
}); })
function error_response(response) { function error_response(response) {
const $alert = $("#alert-box"); const $alert = $("#alert-box")
$alert.html(''); $alert.html('')
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) { if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
$alert.html(` $alert.html(`
@ -138,18 +140,18 @@ function error_response(response) {
${response.responseJSON.error} ${response.responseJSON.error}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
`); `)
} else if (response.responseJSON.error instanceof Array) { } else if (response.responseJSON.error instanceof Array) {
var output = '' var output = ''
for (var i = 0; i < response.responseJSON.error.length; i ++) { for (let i = 0; i < response.responseJSON.error.length; i ++) {
output += ` output += `
<div class="alert alert-danger alert-dismissible fade show" role="alert"> <div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i> <i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
${response.responseJSON.error[i]} ${response.responseJSON.error[i]}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
`; `
$alert.html(output); $alert.html(output)
} }
} }
@ -167,20 +169,20 @@ $('#dismiss-cookie-alert').click(function(event){
}, },
dataType: 'json', dataType: 'json',
success: function(response){ success: function(response){
console.log(response); console.log(response)
}, },
error: function(response){ error: function(response){
console.log(response); console.log(response)
} }
}) })
event.preventDefault(); event.preventDefault()
}) })
// Script for Result Actions // Script for Result Actions
$('.result-action-buttons').click(function(event){ $('.result-action-buttons').click(function(event){
var id = $(this).data('id'); var id = $(this).data('id')
if ($(this).data('result-action') == 'generate') { if ($(this).data('result-action') == 'generate') {
$.ajax({ $.ajax({
@ -190,13 +192,13 @@ $('.result-action-buttons').click(function(event){
contentType: 'application/json', contentType: 'application/json',
dataType: 'html', dataType: 'html',
success: function(response) { success: function(response) {
var display_window = window.open(); var display_window = window.open()
display_window.document.write(response); display_window.document.write(response)
}, },
error: function(response){ error: function(response){
error_response(response); error_response(response)
}, },
}); })
} else { } else {
var action = $(this).data('result-action') var action = $(this).data('result-action')
$.ajax({ $.ajax({
@ -206,23 +208,23 @@ $('.result-action-buttons').click(function(event){
contentType: 'application/json', contentType: 'application/json',
success: function(response) { success: function(response) {
if (action == 'delete') { if (action == 'delete') {
window.location.href = '/admin/results/'; window.location.href = '/admin/results/'
} else window.location.reload(); } else window.location.reload()
}, },
error: function(response){ error: function(response){
error_response(response); error_response(response)
}, },
}); })
} }
event.preventDefault(); event.preventDefault()
}); })
// Script for Deleting Time Adjustment // Script for Deleting Time Adjustment
$('.adjustment-delete').click(function(event){ $('.adjustment-delete').click(function(event){
var user_code = $(this).data('user_code'); var user_code = $(this).data('user_code')
var location = window.location.href; var location = window.location.href
location = location.replace('#', '') location = location.replace('#', '')
$.ajax({ $.ajax({
@ -231,12 +233,19 @@ $('.adjustment-delete').click(function(event){
data: JSON.stringify({'user_code': user_code}), data: JSON.stringify({'user_code': user_code}),
contentType: 'application/json', contentType: 'application/json',
success: function(response) { success: function(response) {
window.location.reload(); window.location.reload()
}, },
error: function(response){ error: function(response){
error_response(response); error_response(response)
}, },
}); })
event.preventDefault(); event.preventDefault()
}); })
// Detailed Results view questions
$('.view-full-questions').click(function(event) {
var dataset = $(this).data('dataset')
window.open(`/admin/view/${dataset}`, '_blank')
event.preventDefault()
})

View File

@ -77,10 +77,13 @@
<a href="{{ url_for('admin._users') }}" id="link-users" class="dropdown-item">Users</a> <a href="{{ url_for('admin._users') }}" id="link-users" class="dropdown-item">Users</a>
</li> </li>
<li> <li>
<a href="{{ url_for('admin._questions') }}" id="link-questions" class="dropdown-item">Question Datasets</a> <a href="{{ url_for('admin._questions') }}" id="link-questions" class="dropdown-item">Manage Questions</a>
</li> </li>
<li> <li>
<a href="{{ url_for('editor._editor') }}" id="link-editor" class="dropdown-item">Question Editor</a> <a href="{{ url_for('view._view') }}" id="link-editor" class="dropdown-item">View Questions</a>
</li>
<li>
<a href="{{ url_for('editor._editor') }}" id="link-editor" class="dropdown-item">Edit Questions</a>
</li> </li>
</ul> </ul>
</li> </li>

View File

@ -114,7 +114,7 @@
{{ scores.scored }} {{ scores.scored }}
</td> </td>
<td> <td>
{{scores.max}} {{ scores.max }}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -131,6 +131,7 @@
</h2> </h2>
<div id="by-question-breakdown" class="accordion-collapse collapse" aria-labelledby="by-question" data-bs-parent="#results-breakdown"> <div id="by-question-breakdown" class="accordion-collapse collapse" aria-labelledby="by-question" data-bs-parent="#results-breakdown">
<div class="accordion-body"> <div class="accordion-body">
<a class="view-full-questions" data-dataset="{{ entry.test.dataset.id }}">View Questions</a>
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@ -146,7 +147,7 @@
{% for question, answer in entry.answers.items() %} {% for question, answer in entry.answers.items() %}
<tr> <tr>
<td> <td>
{{ question }} {{ question|int + 1 }}
</td> </td>
<td> <td>
{{ answers[question|int][answer|int] }} {{ answers[question|int][answer|int] }}

View File

@ -57,28 +57,37 @@
class="btn btn-primary edit-question-dataset" class="btn btn-primary edit-question-dataset"
data-id="{{ element.id }}" data-id="{{ element.id }}"
data-action="download" data-action="download"
title="Download Dataset" title="Download Questions"
> >
<i class="bi bi-cloud-arrow-down-fill button-icon"></i> <i class="bi bi-cloud-arrow-down-fill button-icon"></i>
</button> </a>
<a
href="javascript:void(0)"
class="btn btn-primary view-question-dataset"
data-id="{{ element.id }}"
data-action="view"
title="View Questions"
>
<i class="bi bi-file-earmark-text-fill button-icon"></i>
</a>
<a <a
href="javascript:void(0)" href="javascript:void(0)"
class="btn btn-primary edit-question-dataset" class="btn btn-primary edit-question-dataset"
data-id="{{ element.id }}" data-id="{{ element.id }}"
data-action="edit" data-action="edit"
title="Edit Dataset" title="Edit Questions"
> >
<i class="bi bi-file-earmark-text-fill button-icon"></i> <i class="bi bi-pencil-fill button-icon"></i>
</button> </a>
<a <a
href="javascript:void(0)" href="javascript:void(0)"
class="btn btn-danger edit-question-dataset {% if element.default %}disabled{% endif %}" class="btn btn-danger edit-question-dataset {% if element.default %}disabled{% endif %}"
data-id="{{ element.id }}" data-id="{{ element.id }}"
data-action="delete" data-action="delete"
title="Delete Dataset" title="Delete Questions"
> >
<i class="bi bi-file-earmark-excel-fill button-icon"></i> <i class="bi bi-file-earmark-excel-fill button-icon"></i>
</button> </a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -2,7 +2,7 @@ from ..forms.admin import AddTimeAdjustment, CreateTest, CreateUser, DeleteUser,
from ..models import Dataset, Entry, Test, User from ..models import Dataset, Entry, Test, User
from ..tools.auth import disable_if_logged_in, require_account_creation from ..tools.auth import disable_if_logged_in, require_account_creation
from ..tools.forms import get_dataset_choices, get_time_options, send_errors_to_client from ..tools.forms import get_dataset_choices, get_time_options, send_errors_to_client
from ..tools.data import check_is_json, validate_json from ..tools.data import check_dataset_exists, check_is_json, validate_json
from ..tools.test import answer_options, get_correct_answers from ..tools.test import answer_options, get_correct_answers
from flask import abort, Blueprint, jsonify, render_template, redirect, request, send_file, session from flask import abort, Blueprint, jsonify, render_template, redirect, request, send_file, session
@ -247,15 +247,12 @@ def _download(id:str):
@admin.route('/tests/<string:filter>/', methods=['GET']) @admin.route('/tests/<string:filter>/', methods=['GET'])
@admin.route('/tests/', methods=['GET']) @admin.route('/tests/', methods=['GET'])
@login_required @login_required
@check_dataset_exists
def _tests(filter:str=None): def _tests(filter:str=None):
datasets = Dataset.query.all()
tests = None tests = None
_tests = Test.query.all() _tests = Test.query.all()
form = None form = None
now = datetime.now() now = datetime.now()
if not datasets:
flash('There are no available question datasets. Please upload a question dataset in order to set up an exam.', 'error')
return redirect(url_for('admin._questions'))
if filter not in ['create','active','scheduled','expired','all']: return redirect(url_for('admin._tests', filter='active')) if filter not in ['create','active','scheduled','expired','all']: return redirect(url_for('admin._tests', filter='active'))
if filter == 'create': if filter == 'create':
form = CreateTest() form = CreateTest()

View File

@ -71,7 +71,7 @@
margin: 30pt auto; margin: 30pt auto;
} }
.info-panel { .info-panel, .viewer-panel {
display: none; display: none;
} }
@ -84,4 +84,20 @@
#alert-box { #alert-box {
margin: 30px auto; margin: 30px auto;
max-width: 460px; max-width: 460px;
}
.block {
border: 2px solid black;
border-radius: 10px;
margin: 10px;
padding: 5px;
}
.question-body, .question-block {
padding: 0px 2em;
}
blockquote {
padding: 0px 2em;
font-style: italic;
} }

View File

@ -5,24 +5,69 @@ const id = $root.data('id')
const $control_panel = $('.control-panel') const $control_panel = $('.control-panel')
const $info_panel = $('.info-panel') const $info_panel = $('.info-panel')
const $viewer_panel = $('.viewer-panel')
const $editor_panel = $('.editor-panel') const $editor_panel = $('.editor-panel')
var toggle_info = false
var toggle_viewer = false
var element_index = 0 var element_index = 0
// Initialise Sortable and trigger renumbering on end of drag // Initialise Sortable and trigger renumbering on end of drag
Sortable.create($root.get(0), {handle: '.move-handle', onEnd: function(evt) {renumber_blocks()}}) Sortable.create($root.get(0), {handle: '.move-handle', onEnd: function(evt) {renumber_blocks()}})
// Info Button Listener // Info and Viewer Button Listener
$control_panel.find('button').click(function(event){ $control_panel.find('button').click(function(event){
if ($info_panel.is(":hidden")) { var action = $(this).data('action');
$editor_panel.hide()
$info_panel.fadeIn() if (action == 'info') {
$(this).addClass('active') if ($info_panel.is(":hidden")) {
} else { if ($viewer_panel.is(":visible")) {
$info_panel.hide() toggle_viewer = true
$editor_panel.fadeIn() $viewer_panel.hide()
$(this).removeClass('active') }
$editor_panel.hide()
$info_panel.fadeIn()
$(window).scrollTop(0)
toggle_info = false
$(this).addClass('active')
} else {
$info_panel.hide()
if (toggle_viewer) {
render_viewer()
$(window).scrollTop(0)
toggle_viewer = false
} else {
$editor_panel.fadeIn()
$(window).scrollTop(0)
}
$(this).removeClass('active')
}
} else if (action == 'view') {
if ($viewer_panel.is(":hidden")) {
if ($info_panel.is(':visible')) {
toggle_info = true
$info_panel.hide()
}
$editor_panel.hide()
render_viewer()
$(window).scrollTop(0)
toggle_viewer = false
$(this).addClass('active')
} else {
$viewer_panel.hide()
if (toggle_info) {
$info_panel.fadeIn()
$(window).scrollTop(0)
toggle_info = false
} else {
$editor_panel.fadeIn()
$(window).scrollTop(0)
}
$(this).removeClass('active')
}
} }
event.preventDefault() event.preventDefault()
}) })
@ -496,4 +541,102 @@ $(window).on('load', function() {
console.log(response) console.log(response)
} }
}) })
}) })
// Viewer Render Function
function render_viewer() {
$viewer_panel.fadeIn()
$viewer_panel.empty()
var heading = document.createElement('h3')
heading.innerText = 'View Questions'
$viewer_panel.append(heading)
var data = parse_input()
var block
var obj
for (let i = 0; i < data.length; i++) {
block = data[i]
obj = document.createElement('div')
obj.classList = 'block'
if (block['type'] == 'question') {
text = document.createElement('p')
text.innerHTML = `<strong>Question ${block['q_no'] + 1}.</strong> ${block['text']}`
obj.append(text)
question_body = document.createElement('div')
question_body.className ='question-body'
type = document.createElement('p')
type.innerHTML = `<strong>Question Type:</strong> ${block['q_type']}`
question_body.append(type)
options = document.createElement('p')
options.innerHTML = '<strong>Options:</strong>'
option_list = document.createElement('ul')
for (let _i = 0; _i < block['options'].length; _i++) {
option = document.createElement('li')
option.innerHTML = block['options'][_i]
if (block['correct'] == _i) {
option.innerHTML += ' <span class="badge rounded-pill bg-success">Correct</span>'
}
option_list.append(option)
}
options.append(option_list)
question_body.append(options)
tags = document.createElement('p')
tags.innerHTML = `<strong>Tags:</strong>`
tag_list = document.createElement('ul')
for (let _i = 0; _i < block['tags'].length; _i++) {
tag = document.createElement('li')
tag.innerHTML = block['tags'][_i]
tag_list.append(tag)
}
tags.append(tag_list)
question_body.append(tags)
obj.append(question_body)
} else if (block['type'] == 'block') {
meta = document.createElement('p')
meta.innerHTML = `<strong>Block ${i+1}.</strong> ${block['questions'].length} questions.`
obj.append(meta)
header = document.createElement('blockquote')
header.innerText = block['question_header']
obj.append(header)
var block_question = document.createElement('div')
var question
block_question.className = 'question-block'
for (let _i = 0; _i < block['questions'].length; _i++) {
question = block['questions'][_i]
text = document.createElement('p')
text.innerHTML = `<strong>Question ${question['q_no'] + 1}.</strong> ${question['text']}`
block_question.append(text)
question_body = document.createElement('div')
question_body.className ='question-body'
type = document.createElement('p')
type.innerHTML = `<strong>Question Type:</strong> ${question['q_type']}`
question_body.append(type)
options = document.createElement('p')
options.innerHTML = '<strong>Options:</strong>'
option_list = document.createElement('ul')
for (let __i = 0; __i < question['options'].length; __i++) {
option = document.createElement('li')
option.innerHTML = question['options'][__i]
if (question['correct'] == __i) {
option.innerHTML += ' <span class="badge rounded-pill bg-success">Correct</span>'
}
option_list.append(option)
}
options.append(option_list)
question_body.append(options)
tags = document.createElement('p')
tags.innerHTML = `<strong>Tags:</strong>`
tag_list = document.createElement('ul')
for (let __i = 0; __i < question['tags'].length; __i++) {
tag = document.createElement('li')
tag.innerHTML = question['tags'][__i]
tag_list.append(tag)
}
tags.append(tag_list)
question_body.append(tags)
block_question.append(question_body)
obj.append(block_question)
}
}
$viewer_panel.append(obj)
}
}

View File

@ -57,7 +57,7 @@ function error_response(response) {
`); `);
} else if (response.responseJSON.error instanceof Array) { } else if (response.responseJSON.error instanceof Array) {
var output = '' var output = ''
for (var i = 0; i < response.responseJSON.error.length; i ++) { for (let i = 0; i < response.responseJSON.error.length; i ++) {
output += ` output += `
<div class="alert alert-danger alert-dismissible fade show" role="alert"> <div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i> <i class="bi bi-exclamation-triangle-fill" title="Danger"></i>

View File

@ -77,10 +77,13 @@
<a href="{{ url_for('admin._users') }}" id="link-users" class="dropdown-item">Users</a> <a href="{{ url_for('admin._users') }}" id="link-users" class="dropdown-item">Users</a>
</li> </li>
<li> <li>
<a href="{{ url_for('admin._questions') }}" id="link-questions" class="dropdown-item">Question Datasets</a> <a href="{{ url_for('admin._questions') }}" id="link-questions" class="dropdown-item">Manage Questions</a>
</li> </li>
<li> <li>
<a href="{{ url_for('editor._editor') }}" id="link-editor" class="dropdown-item">Question Editor</a> <a href="{{ url_for('view._view') }}" id="link-editor" class="dropdown-item">View Questions</a>
</li>
<li>
<a href="{{ url_for('editor._editor') }}" id="link-editor" class="dropdown-item">Edit Questions</a>
</li> </li>
</ul> </ul>
</li> </li>

View File

@ -11,11 +11,12 @@
<h1>Editor</h1> <h1>Editor</h1>
<div class="container"> <div class="container">
<p class="lead"> <p class="lead">
Use this console to edit the questions in this dataset. For more information on using the editor console, click on the the blue information button. Use this console to edit the questions in this dataset. For more information on using the editor console, click on the the blue Information button. To preview the questions in the current dataset, click on the green View Questions button.
</p> </p>
</div> </div>
<div class="container control-panel"> <div class="container control-panel">
<button class="btn btn-primary" aria-title="Infrmation" title="Information"><i class="bi bi-info-circle-fill"></i></button> <button class="btn btn-primary" aria-title="Information" title="Information" data-action="info"><i class="bi bi-info-circle-fill"></i></button>
<button class="btn btn-success" aria-title="View Questions" title="View Questions" data-action="view"><i class="bi bi-book-fill"></i></button>
</div> </div>
<div class="container info-panel"> <div class="container info-panel">
<h3> <h3>
@ -77,9 +78,11 @@
In order to show how many questions are remaining inside a block, e.g. to say &lsquo;the next n questions are about a specific scenario&rsquo;, use the placeholder <code>&lt;block_remaining_questions&gt;</code>. In order to show how many questions are remaining inside a block, e.g. to say &lsquo;the next n questions are about a specific scenario&rsquo;, use the placeholder <code>&lt;block_remaining_questions&gt;</code>.
</p> </p>
</div> </div>
<div class="container viewer-panel">
</div>
<div class="container editor-panel"> <div class="container editor-panel">
<h3> <h3>
Edit Dataset Edit Questions
</h3> </h3>
<div class="container dataset-metadata"> <div class="container dataset-metadata">
<div class="input-group mb-3"> <div class="input-group mb-3">
@ -98,7 +101,7 @@
<span class="input-group-text">Last Updated</span> <span class="input-group-text">Last Updated</span>
<span class="form-control"> <span class="form-control">
{{ dataset.date.strftime('%d %b %Y %H:%M') }} {{ dataset.date.strftime('%d %b %Y %H:%M') }}
</select> </span>
</div> </div>
<div class="input-group mb-3"> <div class="input-group mb-3">
<span class="input-group-text"> <span class="input-group-text">

View File

@ -4,7 +4,7 @@
<div class="form-container"> <div class="form-container">
<form name="form-editor" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for(request.endpoint, **request.view_args) }}"> <form name="form-editor" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for(request.endpoint, **request.view_args) }}">
{% include "admin/components/server-alerts.html" %} {% include "admin/components/server-alerts.html" %}
<h2 class="form">Dataset Editor</h2> <h2 class="form">Edit Questions</h2>
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
<div class="form-select-input"> <div class="form-select-input">
{{ form.dataset(placeholder="Select Question Dataset") }} {{ form.dataset(placeholder="Select Question Dataset") }}

View File

@ -1,6 +1,7 @@
from ..forms.admin import EditDataset from ..forms.admin import EditDataset
from ..models import Dataset, User from ..models import Dataset, User
from ..tools.forms import get_dataset_choices, send_errors_to_client from ..tools.forms import get_dataset_choices, send_errors_to_client
from ..tools.data import check_dataset_exists
from flask import Blueprint, flash, jsonify, redirect, render_template, request from flask import Blueprint, flash, jsonify, redirect, render_template, request
from flask.helpers import url_for from flask.helpers import url_for
@ -27,6 +28,7 @@ def _editor():
return render_template('/editor/index.html', form=form) return render_template('/editor/index.html', form=form)
@editor.route('/<string:id>/') @editor.route('/<string:id>/')
@check_dataset_exists
@login_required @login_required
def _editor_console(id:str=None): def _editor_console(id:str=None):
dataset = Dataset.query.filter_by(id=id).first() dataset = Dataset.query.filter_by(id=id).first()

View File

@ -1,142 +1,142 @@
// Bind Listeners // Bind Listeners
$("input[name='font-select']").change(function(){ $("input[name='font-select']").change(function(){
let $choice = $(this).val(); let $choice = $(this).val()
set_font($choice); set_font($choice)
}); })
$("input[name='font-size']").change(function(){ $("input[name='font-size']").change(function(){
let $choice = $(this).val(); let $choice = $(this).val()
set_font_size($choice); set_font_size($choice)
}); })
$("input[name='bg-select']").change(function(){ $("input[name='bg-select']").change(function(){
let $choice = $(this).val(); let $choice = $(this).val()
set_bg_colour($choice); set_bg_colour($choice)
}); })
$(".bg-select-area").click(function(event){ $(".bg-select-area").click(function(event){
$(this).find("input[name='bg-select']").prop("checked", true).change(); $(this).find("input[name='bg-select']").prop("checked", true).change()
}); })
$("#btn-toggle-navigator").click(function(event){ $("#btn-toggle-navigator").click(function(event){
check_answered(); check_answered()
update_navigator(); update_navigator()
if ($quiz_navigator.is(":hidden")) { if ($quiz_navigator.is(":hidden")) {
if ($quiz_settings.is(":visible")) { if ($quiz_settings.is(":visible")) {
toggle_settings = true; toggle_settings = true
$quiz_settings.fadeOut(); $quiz_settings.fadeOut()
} }
$quiz_render.fadeOut(); $quiz_render.fadeOut()
$quiz_navigator.fadeIn(); $quiz_navigator.fadeIn()
$(".navigator-text").fadeIn(); $(".navigator-text").fadeIn()
$(".review-text").fadeOut(); $(".review-text").fadeOut()
toggle_navigator = false; toggle_navigator = false
$(window).scrollTop(0); $(window).scrollTop(0)
} else { } else {
$quiz_navigator.fadeOut(); $quiz_navigator.fadeOut()
if (toggle_settings) { if (toggle_settings) {
$quiz_settings.fadeIn(); $quiz_settings.fadeIn()
$(window).scrollTop(0); $(window).scrollTop(0)
toggle_settings = false; toggle_settings = false
} else { } else {
$quiz_render.fadeIn(); $quiz_render.fadeIn()
$(window).scrollTop(0); $(window).scrollTop(0)
} }
} }
event.preventDefault(); event.preventDefault()
}); })
$("#btn-toggle-settings").click(function(event){ $("#btn-toggle-settings").click(function(event){
if (($quiz_settings).is(":hidden")) { if (($quiz_settings).is(":hidden")) {
if ($quiz_navigator.is(":visible")) { if ($quiz_navigator.is(":visible")) {
toggle_navigator = true; toggle_navigator = true
$quiz_navigator.fadeOut(); $quiz_navigator.fadeOut()
} }
$quiz_render.fadeOut(); $quiz_render.fadeOut()
$quiz_settings.fadeIn(); $quiz_settings.fadeIn()
$(window).scrollTop(0); $(window).scrollTop(0)
toggle_settings = false; toggle_settings = false
} else { } else {
$quiz_settings.fadeOut(); $quiz_settings.fadeOut()
if (toggle_navigator) { if (toggle_navigator) {
$quiz_navigator.fadeIn(); $quiz_navigator.fadeIn()
toggle_navigator = false; toggle_navigator = false
$(window).scrollTop(0); $(window).scrollTop(0)
} else { } else {
$quiz_render.fadeIn(); $quiz_render.fadeIn()
$(window).scrollTop(0); $(window).scrollTop(0)
} }
} }
event.preventDefault(); event.preventDefault()
}); })
$(".btn-quiz-return").click(function(event){ $(".btn-quiz-return").click(function(event){
$quiz_navigator.fadeOut(); $quiz_navigator.fadeOut()
$quiz_settings.fadeOut(); $quiz_settings.fadeOut()
$quiz_render.fadeIn(); $quiz_render.fadeIn()
$(window).scrollTop(0); $(window).scrollTop(0)
toggle_settings = false; toggle_settings = false
toggle_navigator = false; toggle_navigator = false
event.preventDefault(); event.preventDefault()
}); })
$(".btn-dummy").click(function(event){ $(".btn-dummy").click(function(event){
event.preventDefault(); event.preventDefault()
}); })
$("#navigator-container").on("click", ".q-navigator-button", function(event){ $("#navigator-container").on("click", ".q-navigator-button", function(event){
check_answered(); check_answered()
update_navigator(); update_navigator()
current_question = parseInt($(this).prop("name")); current_question = parseInt($(this).prop("name"))
$quiz_navigator.fadeOut(); $quiz_navigator.fadeOut()
$quiz_render.fadeIn(); $quiz_render.fadeIn()
$question_title.focus(); $question_title.focus()
$(window).scrollTop(0); $(window).scrollTop(0)
toggle_navigator = false; toggle_navigator = false
toggle_settings = false; toggle_settings = false
render_question(); render_question()
check_flag(); check_flag()
event.preventDefault(); event.preventDefault()
}); })
$(".q-question-nav").click(function(event){ $(".q-question-nav").click(function(event){
check_answered(); check_answered()
update_navigator(); update_navigator()
if ($(this).prop("id") == "q-nav-next") { if ($(this).prop("id") == "q-nav-next") {
if (current_question < questions.length) { if (current_question < questions.length) {
current_question ++; current_question ++
} }
} else if ($(this).prop("id") == "q-nav-prev") { } else if ($(this).prop("id") == "q-nav-prev") {
if (current_question > 0) { if (current_question > 0) {
current_question --; current_question --
} }
} else if ($(this).hasClass("q-navigator-button")) { } else if ($(this).hasClass("q-navigator-button")) {
current_question = $(this).prop("name"); current_question = $(this).prop("name")
$quiz_render.fadeIn(); $quiz_render.fadeIn()
$quiz_navigator.fadeOut(); $quiz_navigator.fadeOut()
toggle_navigator = false; toggle_navigator = false
toggle_settings = false; toggle_settings = false
} }
render_question(); render_question()
check_flag(); check_flag()
event.preventDefault(); event.preventDefault()
}); })
$("#q-nav-flag").click(function(event){ $("#q-nav-flag").click(function(event){
if (question_status[current_question] != 1) { if (question_status[current_question] != 1) {
question_status[current_question] = 1; question_status[current_question] = 1
$(this).removeClass().addClass("btn btn-warning"); $(this).removeClass().addClass("btn btn-warning")
$(this).prop("title", "Question Flagged for revision. Click to un-flag."); $(this).prop("title", "Question Flagged for revision. Click to un-flag.")
} else { } else {
question_status[current_question] = 0; question_status[current_question] = 0
$(this).removeClass().addClass("btn btn-secondary"); $(this).removeClass().addClass("btn btn-secondary")
$(this).prop("title", "Question Un-Flagged. Click to flag for revision."); $(this).prop("title", "Question Un-Flagged. Click to flag for revision.")
} }
window.localStorage.setItem('question_status', JSON.stringify(question_status)); window.localStorage.setItem('question_status', JSON.stringify(question_status))
update_navigator(); update_navigator()
event.preventDefault(); event.preventDefault()
}); })
$("#btn-start-quiz").click(function(event){ $("#btn-start-quiz").click(function(event){
@ -146,29 +146,29 @@ $("#btn-start-quiz").click(function(event){
data: JSON.stringify({'id': id}), data: JSON.stringify({'id': id}),
contentType: "application/json", contentType: "application/json",
success: function(response) { success: function(response) {
$("#btn-start-quiz").fadeOut(); $("#btn-start-quiz").fadeOut()
$(".btn-quiz-return").fadeIn(); $(".btn-quiz-return").fadeIn()
$(".quiz-console").fadeIn(); $(".quiz-console").fadeIn()
$("#quiz-settings").fadeOut(); $("#quiz-settings").fadeOut()
$("#quiz-navigator").fadeOut(); $("#quiz-navigator").fadeOut()
$(".quiz-start-text").fadeOut(); $(".quiz-start-text").fadeOut()
time_limit = response.time_limit; time_limit = response.time_limit
start_time = response.start_time; start_time = response.start_time
questions = response.questions; questions = response.questions
total_questions = questions.length; total_questions = questions.length
window.localStorage.setItem('questions', JSON.stringify(questions)); window.localStorage.setItem('questions', JSON.stringify(questions))
window.localStorage.setItem('start_time', JSON.stringify(start_time)); window.localStorage.setItem('start_time', JSON.stringify(start_time))
window.localStorage.setItem('time_limit', JSON.stringify(time_limit)); window.localStorage.setItem('time_limit', JSON.stringify(time_limit))
render_question(); render_question()
build_navigator(); build_navigator()
check_flag(); check_flag()
if (time_limit != 'null' && time_limit != null) { if (time_limit != 'null' && time_limit != null) {
$("#q-timer-widget").fadeIn(); $("#q-timer-widget").fadeIn()
time_remaining = get_time_remaining(); time_remaining = get_time_remaining()
clock = setInterval(timer, 1000); clock = setInterval(timer, 1000)
} }
if (response.time_adjustment > 0) { if (response.time_adjustment > 0) {
const $alert = $("#alert-box"); const $alert = $("#alert-box")
$alert.html( $alert.html(
`<div class="alert alert-primary alert-dismissible fade show" role="alert"> `<div class="alert alert-primary alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill" title="Alert"></i> <i class="bi bi-exclamation-triangle-fill" title="Alert"></i>
@ -176,50 +176,50 @@ $("#btn-start-quiz").click(function(event){
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
` `
); )
$alert.focus(); $alert.focus()
} }
}, },
error: function(response) { error: function(response) {
error_response(response); error_response(response)
} }
}); })
event.preventDefault(); event.preventDefault()
}); })
$("#quiz-question-options").on("change", ".quiz-option", function(event){ $("#quiz-question-options").on("change", ".quiz-option", function(event){
$name = parseInt($(this).prop("name")); $name = parseInt($(this).prop("name"))
$value = $(this).prop("value"); $value = $(this).prop("value")
answers[$name] = $value; answers[$name] = $value
window.localStorage.setItem('answers', JSON.stringify(answers)); window.localStorage.setItem('answers', JSON.stringify(answers))
}); })
$("#q-review-answers").click(function(event){ $("#q-review-answers").click(function(event){
check_answered(); check_answered()
update_navigator(); update_navigator()
if ($quiz_navigator.is(":hidden")) { if ($quiz_navigator.is(":hidden")) {
if ($quiz_settings.is(":visible")) { if ($quiz_settings.is(":visible")) {
toggle_settings = true; toggle_settings = true
$quiz_settings.fadeOut(); $quiz_settings.fadeOut()
} }
$quiz_render.fadeOut(); $quiz_render.fadeOut()
$quiz_navigator.fadeIn(); $quiz_navigator.fadeIn()
$(".navigator-text").fadeOut(); $(".navigator-text").fadeOut()
$(".review-text").fadeIn(); $(".review-text").fadeIn()
toggle_navigator = false; toggle_navigator = false
$(window).scrollTop(0); $(window).scrollTop(0)
} else { } else {
$quiz_navigator.fadeOut(); $quiz_navigator.fadeOut()
if (toggle_settings) { if (toggle_settings) {
$quiz_settings.fadeIn(); $quiz_settings.fadeIn()
toggle_settings = false; toggle_settings = false
} else { } else {
$quiz_render.fadeIn(); $quiz_render.fadeIn()
} }
} }
event.preventDefault(); event.preventDefault()
}); })
$(".quiz-button-submit").click(function(event){ $(".quiz-button-submit").click(function(event){
let submission = { let submission = {
@ -233,16 +233,16 @@ $(".quiz-button-submit").click(function(event){
data: JSON.stringify(submission), data: JSON.stringify(submission),
contentType: "application/json", contentType: "application/json",
success: function(response) { success: function(response) {
window.localStorage.clear(); window.localStorage.clear()
window.location.href = `/result/`; window.location.href = `/result/`
}, },
error: function(response) { error: function(response) {
error_response(response); error_response(response)
} }
}); })
event.preventDefault(); event.preventDefault()
}); })
// Functions // Functions
@ -251,17 +251,17 @@ function set_font(value = 'osdefault') {
for (let i = 0; i < font_styles.length; i ++) { for (let i = 0; i < font_styles.length; i ++) {
if (font_styles[i] != value) { if (font_styles[i] != value) {
$("body").removeClass( `q-f-${font_styles[i]}` ); $("body").removeClass( `q-f-${font_styles[i]}` )
}; }
}; }
if (value != 'osdefault') { if (value != 'osdefault') {
$("body").addClass(`q-f-${value}`); $("body").addClass(`q-f-${value}`)
}; }
display_settings['font-select'] = value; display_settings['font-select'] = value
window.localStorage.setItem('display_settings', JSON.stringify(display_settings)); window.localStorage.setItem('display_settings', JSON.stringify(display_settings))
$('input[name="font-select"][value="' + value + '"]').prop('checked', true); $('input[name="font-select"][value="' + value + '"]').prop('checked', true)
} }
function set_font_size(value = '14pt') { function set_font_size(value = '14pt') {
@ -269,17 +269,17 @@ function set_font_size(value = '14pt') {
for (let i = 0; i < font_sizes.length; i ++) { for (let i = 0; i < font_sizes.length; i ++) {
if (font_sizes[i] != value) { if (font_sizes[i] != value) {
$("body").removeClass( `q-f-${font_sizes[i]}` ); $("body").removeClass( `q-f-${font_sizes[i]}` )
}; }
}; }
if (value != '14pt') { if (value != '14pt') {
$("body").addClass(`q-f-${value}`); $("body").addClass(`q-f-${value}`)
}; }
display_settings['font-size'] = value; display_settings['font-size'] = value
window.localStorage.setItem('display_settings', JSON.stringify(display_settings)); window.localStorage.setItem('display_settings', JSON.stringify(display_settings))
$('input[name="font-size"][value="' + value + '"]').prop('checked', true); $('input[name="font-size"][value="' + value + '"]').prop('checked', true)
} }
function set_bg_colour(value = 'bg-light') { function set_bg_colour(value = 'bg-light') {
@ -287,34 +287,34 @@ function set_bg_colour(value = 'bg-light') {
for (let i = 0; i < backgrounds.length; i ++) { for (let i = 0; i < backgrounds.length; i ++) {
if (backgrounds[i] != value) { if (backgrounds[i] != value) {
$("body").removeClass(backgrounds[i]); $("body").removeClass(backgrounds[i])
if (backgrounds[i] == 'bg-dark') { if (backgrounds[i] == 'bg-dark') {
$("body").removeClass('text-light'); $("body").removeClass('text-light')
}; }
if (backgrounds[i] == 'alert-primary' || backgrounds[i] == 'alert-secondary' || backgrounds[i] == 'alert-dark') { if (backgrounds[i] == 'alert-primary' || backgrounds[i] == 'alert-secondary' || backgrounds[i] == 'alert-dark') {
$("body").removeClass('text-dark'); $("body").removeClass('text-dark')
}; }
}; }
}; }
$("body").addClass(value); $("body").addClass(value)
if (value == 'bg-dark') { if (value == 'bg-dark') {
$("body").addClass('text-light'); $("body").addClass('text-light')
}; }
if (value == 'alert-primary' || value == 'alert-secondary' || value == 'alert-dark') { if (value == 'alert-primary' || value == 'alert-secondary' || value == 'alert-dark') {
$("body").addClass('text-dark'); $("body").addClass('text-dark')
}; }
display_settings['bg-select'] = value; display_settings['bg-select'] = value
window.localStorage.setItem('display_settings', JSON.stringify(display_settings)); window.localStorage.setItem('display_settings', JSON.stringify(display_settings))
$('input[name="bg-select"][value="' + value + '"]').prop('checked', true); $('input[name="bg-select"][value="' + value + '"]').prop('checked', true)
} }
function get_settings_from_storage() { function get_settings_from_storage() {
let display_settings = window.localStorage.getItem('display_settings') let display_settings = window.localStorage.getItem('display_settings')
if (display_settings != null) { if (display_settings != null) {
return JSON.parse(display_settings); return JSON.parse(display_settings)
}; }
return { return {
'font-select': 'osdefault', 'font-select': 'osdefault',
'font-size': '14pt', 'font-size': '14pt',
@ -323,133 +323,133 @@ function get_settings_from_storage() {
} }
function apply_settings(settings) { function apply_settings(settings) {
set_font(settings['font-select']); set_font(settings['font-select'])
set_font_size(settings['font-size']); set_font_size(settings['font-size'])
set_bg_colour(settings['bg-select']); set_bg_colour(settings['bg-select'])
} }
function render_question() { function render_question() {
if (current_question == 0) { if (current_question == 0) {
$nav_prev.addClass('disabled'); $nav_prev.addClass('disabled')
} }
if (current_question == questions.length - 1) { if (current_question == questions.length - 1) {
$nav_next.addClass('disabled'); $nav_next.addClass('disabled')
} }
if ($nav_prev.hasClass('disabled') && current_question > 0) { if ($nav_prev.hasClass('disabled') && current_question > 0) {
$nav_prev.removeClass('disabled'); $nav_prev.removeClass('disabled')
} }
if ($nav_next.hasClass('disabled') && current_question < questions.length - 1) { if ($nav_next.hasClass('disabled') && current_question < questions.length - 1) {
$nav_next.removeClass('disabled'); $nav_next.removeClass('disabled')
} }
var question = questions[current_question]; var question = questions[current_question]
let header_text = question.question_header; let header_text = question.question_header
var block_length = 0; var block_length = 0
if ('block_length' in question) { if ('block_length' in question) {
block_length = question['block_length']; block_length = question['block_length']
}; }
var block_q_no = 0; var block_q_no = 0
if ('block_q_no' in question) { if ('block_q_no' in question) {
block_q_no = question['block_q_no']; block_q_no = question['block_q_no']
} }
let remaining_qs = (block_length - block_q_no).toString(); let remaining_qs = (block_length - block_q_no).toString()
if (block_length - block_q_no > 1) { if (block_length - block_q_no > 1) {
remaining_qs += ' questions'; remaining_qs += ' questions'
} else { } else {
remaining_qs += ' question'; remaining_qs += ' question'
} }
header_text = header_text.replace('<block_remaining_questions>', remaining_qs); header_text = header_text.replace('<block_remaining_questions>', remaining_qs)
$question_header.html(header_text); $question_header.html(header_text)
$question_text.html(question.text); $question_text.html(question.text)
$question_title.html(`Question ${current_question + 1} of ${ questions.length }.`); $question_title.html(`Question ${current_question + 1} of ${ questions.length }.`)
var q_no = question['q_no']; var q_no = question['q_no']
var options = question.options; var options = question.options
var options_output = ''; var options_output = ''
for (let i = 0; i < options.length; i ++) { for (let i = 0; i < options.length; i ++) {
var add_checked = '' var add_checked = ''
if (q_no in answers) { if (q_no in answers) {
if (answers[q_no] == options[i][0]) { if (answers[q_no] == options[i][0]) {
add_checked = 'checked'; add_checked = 'checked'
} }
} }
options_output += `<div class="form-check"> options_output += `<div class="form-check">
<input type="radio" class="form-check-input quiz-option" id="q${current_question}-${i}" name="${q_no}" value="${options[i][0]}" ${add_checked}> <input type="radio" class="form-check-input quiz-option" id="q${current_question}-${i}" name="${q_no}" value="${options[i][0]}" ${add_checked}>
<label for="q${current_question}-${i}" class="form-check-label">${options[i][1]}</label> <label for="q${current_question}-${i}" class="form-check-label">${options[i][1]}</label>
</div>`; </div>`
} }
$question_options.html(options_output); $question_options.html(options_output)
let skipped = count_questions(-1); let skipped = count_questions(-1)
let answered = count_questions(2); let answered = count_questions(2)
let flagged = count_questions(1); let flagged = count_questions(1)
$progress_skipped.prop('title', `Skipped: ${skipped}`); $progress_skipped.prop('title', `Skipped: ${skipped}`)
$progress_skipped.prop('aria-valuenow', skipped); $progress_skipped.prop('aria-valuenow', skipped)
$progress_skipped.css('width', `${skipped}%`); $progress_skipped.css('width', `${skipped}%`)
$skipped_count.text(`Skipped: ${skipped}`); $skipped_count.text(`Skipped: ${skipped}`)
if (skipped < 1) { if (skipped < 1) {
$skipped_count.fadeOut() $skipped_count.fadeOut()
} else { } else {
$skipped_count.fadeIn() $skipped_count.fadeIn()
} }
$progress_flagged.prop('title', `Flagged: ${flagged}`); $progress_flagged.prop('title', `Flagged: ${flagged}`)
$progress_flagged.prop('aria-valuenow', flagged); $progress_flagged.prop('aria-valuenow', flagged)
$progress_flagged.css('width', `${flagged}%`); $progress_flagged.css('width', `${flagged}%`)
$flagged_count.text(`Flagged: ${flagged}`); $flagged_count.text(`Flagged: ${flagged}`)
if (flagged < 1) { if (flagged < 1) {
$flagged_count.fadeOut() $flagged_count.fadeOut()
} else { } else {
$flagged_count.fadeIn() $flagged_count.fadeIn()
} }
$progress_answered.prop('title', `Answered: ${answered}`); $progress_answered.prop('title', `Answered: ${answered}`)
$progress_answered.prop('aria-valuenow', answered); $progress_answered.prop('aria-valuenow', answered)
$progress_answered.css('width', `${answered}%`); $progress_answered.css('width', `${answered}%`)
$answered_count.text(`Answered: ${answered}`); $answered_count.text(`Answered: ${answered}`)
if (answered < 1) { if (answered < 1) {
$answered_count.fadeOut() $answered_count.fadeOut()
} else { } else {
$answered_count.fadeIn() $answered_count.fadeIn()
} }
$question_title.focus(); $question_title.focus()
$(window).scrollTop(0); $(window).scrollTop(0)
} }
function check_answered() { function check_answered() {
var question = questions[current_question]; var question = questions[current_question]
var name = question.q_no; var name = question.q_no
if (question_status[current_question] == 0 || question_status[current_question] == -1) { if (question_status[current_question] == 0 || question_status[current_question] == -1) {
if (!$(`input[name='${name}']:checked`).val()) { if (!$(`input[name='${name}']:checked`).val()) {
question_status[current_question] = -1; question_status[current_question] = -1
} else { } else {
question_status[current_question] = 2; question_status[current_question] = 2
} }
window.localStorage.setItem('question_status', JSON.stringify(question_status)); window.localStorage.setItem('question_status', JSON.stringify(question_status))
} }
} }
function check_flag() { function check_flag() {
if (!(current_question in question_status)) { if (!(current_question in question_status)) {
question_status[current_question] = 0; question_status[current_question] = 0
window.localStorage.setItem('question_status', JSON.stringify(question_status)); window.localStorage.setItem('question_status', JSON.stringify(question_status))
} }
switch (question_status[current_question]) { switch (question_status[current_question]) {
case -1: case -1:
$nav_flag.removeClass().addClass('btn btn-danger progress-bar-striped'); $nav_flag.removeClass().addClass('btn btn-danger progress-bar-striped')
$nav_flag.prop("title", "Question Incomplete. Click to flag for revision."); $nav_flag.prop("title", "Question Incomplete. Click to flag for revision.")
break; break
case 1: case 1:
$nav_flag.removeClass().addClass('btn btn-warning'); $nav_flag.removeClass().addClass('btn btn-warning')
$nav_flag.prop("title", "Question Flagged for revision. Click to un-flag."); $nav_flag.prop("title", "Question Flagged for revision. Click to un-flag.")
break; break
case 2: case 2:
$nav_flag.removeClass().addClass('btn btn-success'); $nav_flag.removeClass().addClass('btn btn-success')
$nav_flag.prop("title", "Question Answered. Click to flag for revision."); $nav_flag.prop("title", "Question Answered. Click to flag for revision.")
break; break
default: default:
$nav_flag.removeClass().addClass('btn btn-secondary'); $nav_flag.removeClass().addClass('btn btn-secondary')
$nav_flag.prop("title", "Question Un-Flagged. Click to flag for revision."); $nav_flag.prop("title", "Question Un-Flagged. Click to flag for revision.")
} }
} }
@ -457,31 +457,31 @@ function build_navigator() {
$nav_container.html('') $nav_container.html('')
var output = '' var output = ''
for (let i = 0; i < questions.length; i ++) { for (let i = 0; i < questions.length; i ++) {
let add_class, add_href, add_status = ''; let add_class, add_href, add_status = ''
switch (question_status[i]) { switch (question_status[i]) {
case -1: case -1:
add_class = 'btn-danger progress-bar-striped'; add_class = 'btn-danger progress-bar-striped'
add_href = 'href="#"'; add_href = 'href="#"'
add_status = 'Incomplete'; add_status = 'Incomplete'
break; break
case 1: case 1:
add_class = 'btn-warning'; add_class = 'btn-warning'
add_href = 'href="#"'; add_href = 'href="#"'
add_status = 'Flagged'; add_status = 'Flagged'
break; break
case 2: case 2:
add_class = 'btn-success'; add_class = 'btn-success'
add_href = 'href="#"'; add_href = 'href="#"'
add_status = 'Answered'; add_status = 'Answered'
break; break
default: default:
add_class = 'btn-secondary disabled'; add_class = 'btn-secondary disabled'
add_href = ''; add_href = ''
add_status = 'Unseen'; add_status = 'Unseen'
} }
output += `<a ${add_href} class="q-navigator-button btn ${add_class}" name=${i} title="Question ${i+1}: ${add_status}">Q${i + 1}</a>`; output += `<a ${add_href} class="q-navigator-button btn ${add_class}" name=${i} title="Question ${i+1}: ${add_status}">Q${i + 1}</a>`
} }
$nav_container.html(output); $nav_container.html(output)
} }
function update_navigator() { function update_navigator() {
@ -489,162 +489,162 @@ function update_navigator() {
if (current_question in question_status) { if (current_question in question_status) {
switch (question_status[current_question]) { switch (question_status[current_question]) {
case -1: case -1:
button.removeClass().addClass("q-navigator-button btn btn-danger progress-bar-striped"); button.removeClass().addClass("q-navigator-button btn btn-danger progress-bar-striped")
button.prop("title", `Question ${current_question + 1}: Incomplete`); button.prop("title", `Question ${current_question + 1}: Incomplete`)
break; break
case 1: case 1:
button.removeClass().addClass("q-navigator-button btn btn-warning"); button.removeClass().addClass("q-navigator-button btn btn-warning")
button.prop("title", `Question ${current_question + 1}: Flagged`); button.prop("title", `Question ${current_question + 1}: Flagged`)
break; break
case 2: case 2:
button.removeClass().addClass("q-navigator-button btn btn-success"); button.removeClass().addClass("q-navigator-button btn btn-success")
button.prop("title", `Question ${current_question + 1}: Answered`); button.prop("title", `Question ${current_question + 1}: Answered`)
break; break
default: default:
button.removeClass().addClass("q-navigator-button btn btn-secondary disabled"); button.removeClass().addClass("q-navigator-button btn btn-secondary disabled")
button.prop("title", `Question ${current_question + 1}: Unseen`); button.prop("title", `Question ${current_question + 1}: Unseen`)
} }
} }
} }
function start() { function start() {
$("#btn-start-quiz").fadeOut(); $("#btn-start-quiz").fadeOut()
$(".btn-quiz-return").fadeIn(); $(".btn-quiz-return").fadeIn()
$(".quiz-console").fadeIn(); $(".quiz-console").fadeIn()
$("#quiz-settings").fadeOut(); $("#quiz-settings").fadeOut()
$("#quiz-navigator").fadeOut(); $("#quiz-navigator").fadeOut()
$(".quiz-start-text").fadeOut(); $(".quiz-start-text").fadeOut()
questions = JSON.parse(window.localStorage.getItem('questions')); questions = JSON.parse(window.localStorage.getItem('questions'))
total_questions = questions.length; total_questions = questions.length
start_time = window.localStorage.getItem('start_time'); start_time = window.localStorage.getItem('start_time')
time_limit = window.localStorage.getItem('time_limit'); time_limit = window.localStorage.getItem('time_limit')
let get_answers = window.localStorage.getItem('answers'); let get_answers = window.localStorage.getItem('answers')
if (get_answers != null) { if (get_answers != null) {
answers = JSON.parse(get_answers); answers = JSON.parse(get_answers)
} }
let get_status = window.localStorage.getItem('question_status'); let get_status = window.localStorage.getItem('question_status')
if (get_status != null) { if (get_status != null) {
question_status = JSON.parse(get_status); question_status = JSON.parse(get_status)
} }
render_question(); render_question()
build_navigator(); build_navigator()
check_flag(); check_flag()
if (time_limit != 'null' && time_limit != null) { if (time_limit != 'null' && time_limit != null) {
$("#q-timer-widget").fadeIn(); $("#q-timer-widget").fadeIn()
time_remaining = get_time_remaining(); time_remaining = get_time_remaining()
clock = setInterval(timer, 1000); clock = setInterval(timer, 1000)
} }
} }
function check_started() { function check_started() {
let questions = window.localStorage.getItem('questions'); let questions = window.localStorage.getItem('questions')
let time_limit = window.localStorage.getItem('time_limit'); let time_limit = window.localStorage.getItem('time_limit')
let start_time = window.localStorage.getItem('start_time') let start_time = window.localStorage.getItem('start_time')
if (questions != null && start_time != null && time_limit != null) { if (questions != null && start_time != null && time_limit != null) {
start(); start()
} }
} }
function get_time_remaining() { function get_time_remaining() {
var end_time = new Date(time_limit).getTime(); var end_time = new Date(time_limit).getTime()
var _start_time = new Date().getTime(); var _start_time = new Date().getTime()
return end_time - _start_time; return end_time - _start_time
} }
function timer() { function timer() {
var hours = Math.floor((time_remaining % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); var hours = Math.floor((time_remaining % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
var minutes = Math.floor((time_remaining % (1000 * 60 * 60)) / (1000 * 60)); var minutes = Math.floor((time_remaining % (1000 * 60 * 60)) / (1000 * 60))
var seconds = Math.floor((time_remaining % (1000 * 60)) / 1000); var seconds = Math.floor((time_remaining % (1000 * 60)) / 1000)
if (time_remaining > 0) { if (time_remaining > 0) {
var timer_display = ''; var timer_display = ''
if (hours > 0) { if (hours > 0) {
timer_display = `${hours.toString()}:`; timer_display = `${hours.toString()}:`
} }
if (minutes > 0 || hours > 0) { if (minutes > 0 || hours > 0) {
if (minutes < 10) { if (minutes < 10) {
timer_display += `0${minutes.toString()}:`; timer_display += `0${minutes.toString()}:`
} else { } else {
timer_display += `${minutes.toString()}:`; timer_display += `${minutes.toString()}:`
} }
} }
if (seconds < 10) { if (seconds < 10) {
timer_display += `0${seconds.toString()}`; timer_display += `0${seconds.toString()}`
} else { } else {
timer_display += seconds.toString(); timer_display += seconds.toString()
} }
$timer.html(timer_display); $timer.html(timer_display)
time_remaining -= 1000 time_remaining -= 1000
} else { } else {
$timer.html('Expired'); $timer.html('Expired')
clearInterval(clock); clearInterval(clock)
stop() stop()
} }
} }
function stop() { function stop() {
$quiz_render.fadeOut(); $quiz_render.fadeOut()
$quiz_navigator.fadeOut(); $quiz_navigator.fadeOut()
$quiz_timeout.fadeIn(); $quiz_timeout.fadeIn()
$("#btn-toggle-navigator").addClass('disabled'); $("#btn-toggle-navigator").addClass('disabled')
$("#btn-toggle-settings").addClass('disabled') $("#btn-toggle-settings").addClass('disabled')
} }
function count_questions(status) { function count_questions(status) {
output = 0; output = 0
for (let i = 0; i < Object.keys(question_status).length; i++) { for (let i = 0; i < Object.keys(question_status).length; i++) {
key = Object.keys(question_status)[i]; key = Object.keys(question_status)[i]
if (question_status[key] == status){ if (question_status[key] == status){
output ++; output ++
} }
} }
return output; return output
} }
// Variable Definitions // Variable Definitions
const id = window.localStorage.getItem('id'); const id = window.localStorage.getItem('id')
var current_question = 0; var current_question = 0
var total_questions = 0; var total_questions = 0
var question_status = {}; var question_status = {}
var answers = {}; var answers = {}
var questions = []; var questions = []
var time_limit, start_time, time_remaining; var time_limit, start_time, time_remaining
var display_settings = get_settings_from_storage(); var display_settings = get_settings_from_storage()
const $quiz_settings = $("#quiz-settings"); const $quiz_settings = $("#quiz-settings")
const $quiz_navigator = $("#quiz-navigator"); const $quiz_navigator = $("#quiz-navigator")
const $quiz_render = $("#quiz-render"); const $quiz_render = $("#quiz-render")
const $quiz_timeout = $("#quiz-timeout"); const $quiz_timeout = $("#quiz-timeout")
const $nav_flag = $("#q-nav-flag"); const $nav_flag = $("#q-nav-flag")
const $nav_next = $("#q-nav-next"); const $nav_next = $("#q-nav-next")
const $nav_prev = $("#q-nav-prev"); const $nav_prev = $("#q-nav-prev")
const $nav_container = $("#navigator-container"); const $nav_container = $("#navigator-container")
const $timer = $("#q-timer-display"); const $timer = $("#q-timer-display")
var clock var clock
var toggle_settings = false; var toggle_settings = false
var toggle_navigator = false; var toggle_navigator = false
const $question_title = $("#quiz-question-title"); const $question_title = $("#quiz-question-title")
const $question_header = $("#quiz-question-header"); const $question_header = $("#quiz-question-header")
const $question_text = $("#quiz-question-text"); const $question_text = $("#quiz-question-text")
const $question_options = $("#quiz-question-options"); const $question_options = $("#quiz-question-options")
const $progress_skipped = $("#skipped-bar"); const $progress_skipped = $("#skipped-bar")
const $progress_answered = $("#answered-bar"); const $progress_answered = $("#answered-bar")
const $progress_flagged = $("#flagged-bar"); const $progress_flagged = $("#flagged-bar")
const $skipped_count = $("#skipped-count"); const $skipped_count = $("#skipped-count")
const $answered_count = $("#answered-count"); const $answered_count = $("#answered-count")
const $flagged_count = $("#flagged-count"); const $flagged_count = $("#flagged-count")
// Execution on Load // Execution on Load
apply_settings(display_settings); apply_settings(display_settings)
check_started(); check_started()

View File

@ -1,21 +1,21 @@
$(document).ready(function() { $(document).ready(function() {
$("#od-font-test").click(function(){ $("#od-font-test").click(function(){
$("body").css("font-family", "opendyslexic3regular") $("body").css("font-family", "opendyslexic3regular")
}); })
$('.test-code-input').keyup(function() { $('.test-code-input').keyup(function() {
var input = $(this).val().split("-").join("").split("—").join(""); var input = $(this).val().split("-").join("").split("—").join("")
if (input.length > 0) { if (input.length > 0) {
input = input.match(new RegExp('.{1,4}', 'g')).join("—"); input = input.match(new RegExp('.{1,4}', 'g')).join("—")
} }
$(this).val(input); $(this).val(input)
}); })
}); })
$('form[name=form-quiz-start]').submit(function(event) { $('form[name=form-quiz-start]').submit(function(event) {
var $form = $(this); var $form = $(this)
var data = $form.serialize(); var data = $form.serialize()
$.ajax({ $.ajax({
url: window.location.pathname, url: window.location.pathname,
@ -24,21 +24,21 @@ $('form[name=form-quiz-start]').submit(function(event) {
dataType: 'json', dataType: 'json',
success: function(response) { success: function(response) {
var id = response.id var id = response.id
window.localStorage.setItem('id', id); window.localStorage.setItem('id', id)
window.location.href = `/quiz/`; window.location.href = `/quiz/`
}, },
error: function(response) { error: function(response) {
error_response(response); error_response(response)
} }
}); })
event.preventDefault(); event.preventDefault()
}); })
function error_response(response) { function error_response(response) {
const $alert = $("#alert-box"); const $alert = $("#alert-box")
$alert.html(''); $alert.html('')
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) { if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
$alert.html(` $alert.html(`
@ -47,18 +47,18 @@ function error_response(response) {
${response.responseJSON.error} ${response.responseJSON.error}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
`); `)
} else if (response.responseJSON.error instanceof Array) { } else if (response.responseJSON.error instanceof Array) {
var output = '' var output = ''
for (var i = 0; i < response.responseJSON.error.length; i ++) { for (let i = 0; i < response.responseJSON.error.length; i ++) {
output += ` output += `
<div class="alert alert-danger alert-dismissible fade show" role="alert"> <div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i> <i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
${response.responseJSON.error[i]} ${response.responseJSON.error[i]}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
`; `
$alert.html(output); $alert.html(output)
} }
} }
} }
@ -74,13 +74,13 @@ $('#dismiss-cookie-alert').click(function(event){
}, },
dataType: 'json', dataType: 'json',
success: function(response){ success: function(response){
console.log(response); console.log(response)
}, },
error: function(response){ error: function(response){
console.log(response); console.log(response)
} }
}) })
event.preventDefault(); event.preventDefault()
}) })

View File

@ -1,21 +1,21 @@
$(document).ready(function() { $(document).ready(function() {
$("#od-font-test").click(function(){ $("#od-font-test").click(function(){
$("body").css("font-family", "opendyslexic3regular") $("body").css("font-family", "opendyslexic3regular")
}); })
$('.test-code-input').keyup(function() { $('.test-code-input').keyup(function() {
var input = $(this).val().split("-").join("").split("—").join(""); var input = $(this).val().split("-").join("").split("—").join("")
if (input.length > 0) { if (input.length > 0) {
input = input.match(new RegExp('.{1,4}', 'g')).join("—"); input = input.match(new RegExp('.{1,4}', 'g')).join("—")
} }
$(this).val(input); $(this).val(input)
}); })
}); })
$('form[name=form-quiz-start]').submit(function(event) { $('form[name=form-quiz-start]').submit(function(event) {
var $form = $(this); var $form = $(this)
var data = $form.serialize(); var data = $form.serialize()
$.ajax({ $.ajax({
url: window.location.pathname, url: window.location.pathname,
@ -24,21 +24,21 @@ $('form[name=form-quiz-start]').submit(function(event) {
dataType: 'json', dataType: 'json',
success: function(response) { success: function(response) {
var id = response.id var id = response.id
window.localStorage.setItem('id', id); window.localStorage.setItem('id', id)
window.location.href = `/quiz/`; window.location.href = `/quiz/`
}, },
error: function(response) { error: function(response) {
error_response(response); error_response(response)
} }
}); })
event.preventDefault(); event.preventDefault()
}); })
function error_response(response) { function error_response(response) {
const $alert = $("#alert-box"); const $alert = $("#alert-box")
$alert.html(''); $alert.html('')
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) { if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
$alert.html(` $alert.html(`
@ -47,18 +47,18 @@ function error_response(response) {
${response.responseJSON.error} ${response.responseJSON.error}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
`); `)
} else if (response.responseJSON.error instanceof Array) { } else if (response.responseJSON.error instanceof Array) {
var output = '' var output = ''
for (var i = 0; i < response.responseJSON.error.length; i ++) { for (let i = 0; i < response.responseJSON.error.length; i ++) {
output += ` output += `
<div class="alert alert-danger alert-dismissible fade show" role="alert"> <div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i> <i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
${response.responseJSON.error[i]} ${response.responseJSON.error[i]}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
`; `
$alert.html(output); $alert.html(output)
} }
} }
} }
@ -74,13 +74,13 @@ $('#dismiss-cookie-alert').click(function(event){
}, },
dataType: 'json', dataType: 'json',
success: function(response){ success: function(response){
console.log(response); console.log(response)
}, },
error: function(response){ error: function(response){
console.log(response); console.log(response)
} }
}) })
event.preventDefault(); event.preventDefault()
}) })

View File

@ -1,8 +1,13 @@
from ..models import Dataset
from flask import current_app as app from flask import current_app as app
from flask import flash, redirect
from flask.helpers import url_for
import json import json
from pathlib import Path from pathlib import Path
from random import shuffle from random import shuffle
from functools import wraps
def load(filename:str): def load(filename:str):
data_dir = Path(app.config.get('DATA')) data_dir = Path(app.config.get('DATA'))
@ -66,4 +71,14 @@ def get_tag_list(dataset:list):
if block['type'] == 'question': output = list(set(output) | set(block['tags'])) if block['type'] == 'question': output = list(set(output) | set(block['tags']))
if block['type'] == 'block': if block['type'] == 'block':
for question in block['questions']: output = list(set(output) | set(question['tags'])) for question in block['questions']: output = list(set(output) | set(question['tags']))
return output return output
def check_dataset_exists(function):
@wraps(function)
def wrapper(*args, **kwargs):
datasets = Dataset.query.all()
if not datasets:
flash('There are no available question datasets. Please upload a question dataset first, or use the question editor to create a new dataset.', 'error')
return redirect(url_for('admin._questions'))
return function(*args, **kwargs)
return wrapper

View File

View File

@ -0,0 +1,260 @@
body {
padding: 80px 0;
}
.site-footer {
background-color: lightgray;
font-size: small;
}
.site-footer p {
margin: 0;
}
.form-container {
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
}
.form-display {
width: 100%;
max-width: 420px;
padding: 15px;
margin: auto;
}
.form-heading {
margin-bottom: 2rem;
}
.form-label-group {
position: relative;
margin-bottom: 2rem;
}
.form-label-group input,
.form-label-group label {
padding: var(--input-padding-y) var(--input-padding-x);
font-size: 16pt;
}
.form-label-group label {
position: absolute;
top: 0;
left: 0;
display: block;
width: 100%;
margin-bottom: 0; /* Override default `<label>` margin */
line-height: 1.5;
color: #495057;
cursor: text; /* Match the input under the label */
border: 1px solid transparent;
border-radius: .25rem;
transition: all .1s ease-in-out;
z-index: -1;
}
.form-label-group input {
background-color: transparent;
border: none;
border-radius: 0%;
border-bottom: 2px solid #585858;
}
.form-label-group input:active, .form-label-group input:focus {
background-color: transparent;
}
.form-label-group input::-webkit-input-placeholder {
color: transparent;
}
.form-label-group input:-ms-input-placeholder {
color: transparent;
}
.form-label-group input::-ms-input-placeholder {
color: transparent;
}
.form-label-group input::-moz-placeholder {
color: transparent;
}
.form-label-group input::placeholder {
color: transparent;
}
.form-label-group input:not(:placeholder-shown) {
padding-top: calc(var(--input-padding-y) + var(--input-padding-y) * (2 / 3));
padding-bottom: calc(var(--input-padding-y) / 3);
}
.form-label-group input:not(:placeholder-shown) ~ label {
padding-top: calc(var(--input-padding-y) / 3);
padding-bottom: calc(var(--input-padding-y) / 3);
font-size: 12px;
color: #777;
}
.form-check {
margin-bottom: 2rem;
}
.checkbox input {
transform: scale(1.5);
margin-right: 1rem;
}
.signin-forgot-password {
font-size: 14pt;
}
.form-submission-button {
margin-bottom: 2rem;
}
.form-submission-button button, .form-submission-button a {
margin: 1rem;
vertical-align: middle;
}
.form-submission-button button span, .form-submission-button button svg, .form-submission-button a span, .form-submission-button a svg {
margin: 0 2px;
}
table.dataTable {
border-collapse: collapse;
width: 100%;
}
.table-row {
vertical-align: middle;
}
.row-actions {
text-align: center;
white-space: nowrap;
}
.dataTables_wrapper .dt-buttons {
left: 50%;
transform: translateX(-50%);
float:none;
text-align:center;
}
.row-actions button, .row-actions a {
margin: 0px 5px;
}
#cookie-alert {
padding-right: 16px;
}
#dismiss-cookie-alert {
margin-top: 16px;
width: fit-content;
}
.alert-db-empty {
width: 100%;
max-width: 720px;
font-size: 14pt;
margin: 20px auto;
}
.form-date-input, .form-select-input {
position: relative;
margin: 2rem 0;
}
.form-date-input input,
.form-date-input label, .form-select-input select, .form-select-input label {
padding: var(--input-padding-y) var(--input-padding-x);
font-size: 16pt;
width: 100%;
background-color: transparent;
border: none;
border-bottom: 2px solid #585858;
}
.datepicker::-webkit-calendar-picker-indicator {
border: 1px;
border-color: gray;
border-radius: 10%;
}
.form-date-input label, .form-select-input label {
/* position: absolute; */
/* top: 0;
left: 0; */
display: block;
width: 100%;
margin-bottom: 0; /* Override default `<label>` margin */
line-height: 1.5;
color: #495057;
cursor: text; /* Match the input under the label */
border: 1px solid transparent;
border-radius: .25rem;
transition: all .1s ease-in-out;
z-index: -1;
}
.button-icon {
font-size: 20px;
}
.form-upload {
margin: 2rem 0;
font-size: 14pt;
}
.result-action-buttons, .test-action {
margin: 5px auto;
width: fit-content;
}
.accordion-item {
background-color: unset;
}
/* Change Autocomplete styles in Chrome*/
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
transition: background-color 5000s ease-in-out 0s;
}
/* Fallback for Edge
-------------------------------------------------- */
@supports (-ms-ime-align: auto) {
.form-label-group label {
display: none;
}
.form-label-group input::-ms-input-placeholder {
color: #777;
}
}
/* Fallback for IE
-------------------------------------------------- */
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.form-label-group label {
display: none;
}
.form-label-group input:-ms-input-placeholder {
color: #777;
}
}

View File

@ -0,0 +1,30 @@
.info-panel {
display: none;
}
.control-panel {
margin-left: auto;
margin-right: 0;
width:fit-content;
}
#alert-box {
margin: 30px auto;
max-width: 460px;
}
.block {
border: 2px solid black;
border-radius: 10px;
margin: 10px;
padding: 5px;
}
.question-body, .question-block {
padding: 0px 2em;
}
blockquote {
padding: 0px 2em;
font-style: italic;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,115 @@
// Menu Highlight Scripts
const menuItems = document.getElementsByClassName('nav-link')
for(let i = 0; i < menuItems.length; i++) {
if(menuItems[i].pathname == window.location.pathname) {
menuItems[i].classList.add('active')
}
}
const dropdownItems = document.getElementsByClassName('dropdown-item')
for(let i = 0; i< dropdownItems.length; i++) {
if(dropdownItems[i].pathname == window.location.pathname) {
dropdownItems[i].classList.add('active')
$( "#" + dropdownItems[i].id ).closest( '.dropdown' ).find('.dropdown-toggle').addClass('active')
}
}
// General Post Method Form Processing Script
$('form.form-post').submit(function(event) {
var $form = $(this)
var data = $form.serialize()
var url = $(this).prop('action')
var rel_success = $(this).data('rel-success')
$.ajax({
url: url,
type: 'POST',
data: data,
dataType: 'json',
success: function(response) {
if (response.redirect_to) {
window.location.href = response.redirect_to
}
else {
window.location.href = rel_success
}
},
error: function(response) {
error_response(response)
}
})
event.preventDefault()
})
function error_response(response) {
const $alert = $("#alert-box")
$alert.html('')
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
$alert.html(`
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
${response.responseJSON.error}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`)
} else if (response.responseJSON.error instanceof Array) {
var output = ''
for (let i = 0; i < response.responseJSON.error.length; i ++) {
output += `
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
${response.responseJSON.error[i]}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`
$alert.html(output)
}
}
$alert.focus()
}
// Dismiss Cookie Alert
$('#dismiss-cookie-alert').click(function(event){
$.ajax({
url: '/cookies/',
type: 'POST',
data: {
time: Date.now()
},
dataType: 'json',
success: function(response){
console.log(response)
},
error: function(response){
console.log(response)
}
})
event.preventDefault()
})
// Create New Dataset
$('.create-new-dataset').click(function(event){
$.ajax({
url: '/api/editor/new/',
type: 'POST',
data: {
time: Date.now()
},
dataType: 'json',
success: function(response){
if (response.redirect_to) {
window.location.href = response.redirect_to
}
},
error: function(response){
console.log(response)
}
})
event.preventDefault()
})

View File

@ -0,0 +1,130 @@
// Variable Declarations
const $control_panel = $('.control-panel')
const $info_panel = $('.info-panel')
const $viewer_panel = $('.viewer-panel')
var element_index = 0
// Info Button Listener
$control_panel.find('button').click(function(event){
if ($info_panel.is(":hidden")) {
$viewer_panel.hide()
$info_panel.fadeIn()
$(this).addClass('active')
} else {
$info_panel.hide()
$viewer_panel.fadeIn()
$(this).removeClass('active')
}
event.preventDefault()
})
function parse_data(data) {
var block
var obj
for (let i = 0; i < data.length; i++) {
block = data[i]
obj = document.createElement('div')
obj.classList = 'block'
if (block['type'] == 'question') {
text = document.createElement('p')
text.innerHTML = `<strong>Question ${block['q_no'] + 1}.</strong> ${block['text']}`
obj.append(text)
question_body = document.createElement('div')
question_body.className ='question-body'
type = document.createElement('p')
type.innerHTML = `<strong>Question Type:</strong> ${block['q_type']}`
question_body.append(type)
options = document.createElement('p')
options.innerHTML = '<strong>Options:</strong>'
option_list = document.createElement('ul')
for (let _i = 0; _i < block['options'].length; _i++) {
option = document.createElement('li')
option.innerHTML = block['options'][_i]
if (block['correct'] == _i) {
option.innerHTML += ' <span class="badge rounded-pill bg-success">Correct</span>'
}
option_list.append(option)
}
options.append(option_list)
question_body.append(options)
tags = document.createElement('p')
tags.innerHTML = `<strong>Tags:</strong>`
tag_list = document.createElement('ul')
for (let _i = 0; _i < block['tags'].length; _i++) {
tag = document.createElement('li')
tag.innerHTML = block['tags'][_i]
tag_list.append(tag)
}
tags.append(tag_list)
question_body.append(tags)
obj.append(question_body)
} else if (block['type'] == 'block') {
meta = document.createElement('p')
meta.innerHTML = `<strong>Block ${i+1}.</strong> ${block['questions'].length} questions.`
obj.append(meta)
header = document.createElement('blockquote')
header.innerText = block['question_header']
obj.append(header)
var block_question = document.createElement('div')
var question
block_question.className = 'question-block'
for (let _i = 0; _i < block['questions'].length; _i++) {
question = block['questions'][_i]
text = document.createElement('p')
text.innerHTML = `<strong>Question ${question['q_no'] + 1}.</strong> ${question['text']}`
block_question.append(text)
question_body = document.createElement('div')
question_body.className ='question-body'
type = document.createElement('p')
type.innerHTML = `<strong>Question Type:</strong> ${question['q_type']}`
question_body.append(type)
options = document.createElement('p')
options.innerHTML = '<strong>Options:</strong>'
option_list = document.createElement('ul')
for (let __i = 0; __i < question['options'].length; __i++) {
option = document.createElement('li')
option.innerHTML = question['options'][__i]
if (question['correct'] == __i) {
option.innerHTML += ' <span class="badge rounded-pill bg-success">Correct</span>'
}
option_list.append(option)
}
options.append(option_list)
question_body.append(options)
tags = document.createElement('p')
tags.innerHTML = `<strong>Tags:</strong>`
tag_list = document.createElement('ul')
for (let __i = 0; __i < question['tags'].length; __i++) {
tag = document.createElement('li')
tag.innerHTML = question['tags'][__i]
tag_list.append(tag)
}
tags.append(tag_list)
question_body.append(tags)
block_question.append(question_body)
obj.append(block_question)
}
}
$viewer_panel.append(obj)
}
}
// Fetch data once page finishes loading
$(window).on('load', function() {
$.ajax({
url: target,
type: 'POST',
data: JSON.stringify({
'id': id,
'action': 'fetch'
}),
contentType: 'application/json',
success: function(response) {
parse_data(response['data'])
},
error: function(response) {
console.log(response)
}
})
})

View File

@ -0,0 +1,84 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
crossorigin="anonymous">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/bootstrap-icons.css">
<link
rel="stylesheet"
href="{{ url_for('.static', filename='css/style.css') }}"
/>
<link
rel="stylesheet"
href="{{ url_for('.static', filename='css/view.css') }}"
/>
{% block style %}
{% endblock %}
<title>{% block title %} SKA Referee Test | Admin Console {% endblock %}</title>
{% include "view/components/og-meta.html" %}
</head>
<body class="bg-light">
{% block navbar %}
{% include "view/components/navbar.html" %}
{% endblock %}
<div class="container">
{% block top_alerts %}
{% include "view/components/server-alerts.html" %}
{% endblock %}
{% block content %}{% endblock %}
</div>
<footer class="container site-footer mt-5">
{% block footer %}
{% include "view/components/footer.html" %}
{% endblock %}
</footer>
<!-- JQuery, Popper, and Bootstrap js dependencies -->
<script
src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous">
</script>
<script>
window.jQuery || document.write(`<script src="{{ url_for('.static', filename='js/jquery-3.6.0.min.js') }}"><\/script>`)
</script>
<script
src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js"
integrity="sha384-7+zCNj/IqJ95wo16oMtfsKbZ9ccEh31eOz1HGyDuCQ6wgnyJNSYdrPa03rtR1zdB"
crossorigin="anonymous">
</script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js"
integrity="sha384-QJHtvGhmr9XOIpI6YVutG+2QOK9T+ZnN4kzFN1RtK3zEFEIsxhlmWl5/YESvpZ13"
crossorigin="anonymous"
></script>
<!-- Custom js -->
<script type="text/javascript">
var csrf_token = "{{ csrf_token() }}";
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrf_token);
}
}
});
</script>
<script
type="text/javascript"
src="{{ url_for('.static', filename='js/script.js') }}"
></script>
{% block script %}
{% endblock %}
</body>
</html>

View File

@ -0,0 +1 @@
<div id="alert-box" tabindex="-1"></div>

View File

@ -0,0 +1,28 @@
{% extends "view/components/base.html" %}
{% block datatable_css %}
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.11.3/css/dataTables.bootstrap5.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/buttons/2.0.1/css/buttons.bootstrap5.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/colreorder/1.5.5/css/colReorder.bootstrap5.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/fixedheader/3.2.0/css/fixedHeader.bootstrap5.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/keytable/2.6.4/css/keyTable.bootstrap5.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/responsive/2.2.9/css/responsive.bootstrap5.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/searchbuilder/1.3.0/css/searchBuilder.dataTables.min.css"/>
{% endblock %}
{% block datatable_scripts %}
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jszip/2.5.0/jszip.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.36/pdfmake.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.36/vfs_fonts.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/1.11.3/js/jquery.dataTables.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/1.11.3/js/dataTables.bootstrap5.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/dataTables.buttons.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.bootstrap5.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.colVis.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.html5.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.print.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/colreorder/1.5.5/js/dataTables.colReorder.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/fixedheader/3.2.0/js/dataTables.fixedHeader.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/keytable/2.6.4/js/dataTables.keyTable.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/responsive/2.2.9/js/dataTables.responsive.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/responsive/2.2.9/js/responsive.bootstrap5.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/searchbuilder/1.3.0/js/dataTables.searchBuilder.min.js"></script>
{% endblock %}

View File

@ -0,0 +1,2 @@
<p>This web app was developed by Vivek Santayana. The source code for the web app, excluding any data pertaining to the questions in the quiz, is freely available at <a href="https://git.vsnt.uk/viveksantayana/ska-referee-test">Vivek&rsquo;s personal GIT repository</a> under an MIT License.</p>
<p>All questions in the test are &copy; The Scottish Korfball Association {{ now.year }}. All rights are reserved.</p>

View File

@ -0,0 +1,4 @@
{% extends "view/components/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block top_alerts %}
{% endblock %}

View File

@ -0,0 +1,117 @@
<nav class="navbar fixed-top navbar-expand-md navbar-dark bg-dark">
<div class="container">
<a href="{{ url_for('admin._home') }}" class="navbar-brand mb-0 h1">RefTest (Beta) | Admin</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbar"
aria-controls="navbar"
aria-expanded="false"
aria-label="Toggle Navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse justify-content-end" id="navbar">
<ul class="navbar-nav">
{% if not current_user.is_authenticated %}
<li class="nav-item" id="nav-login">
<a href="{{ url_for('admin._login') }}" id="link-login" class="nav-link">Log In</a>
</li>
{% endif %}
{% if current_user.is_authenticated %}
<li class="nav-item" id="nav-results">
<a href="{{ url_for('admin._view_entries') }}" id="link-results" class="nav-link">View Results</a>
</li>
<li class="nav-item dropdown" id="nav-tests">
<a
class="nav-link dropdown-toggle"
id="dropdown-tests"
role="button"
href="{{ url_for('admin._tests') }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Exams
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-settings"
>
<li>
<a href="{{ url_for('admin._tests', filter='active') }}" id="link-active" class="dropdown-item">Active</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='scheduled') }}" id="link-scheduled" class="dropdown-item">Scheduled</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='expired') }}" id="link-expired" class="dropdown-item">Expired</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='all') }}" id="link-all" class="dropdown-item">All</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='create') }}" id="link-create" class="dropdown-item">Create</a>
</li>
</ul>
</li>
<li class="nav-item dropdown" id="nav-settings">
<a
class="nav-link dropdown-toggle"
id="dropdown-account"
role="button"
href="{{ url_for('admin._settings') }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Settings
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-settings"
>
<li>
<a href="{{ url_for('admin._settings') }}" id="link-settings" class="dropdown-item">View Settings</a>
</li>
<li>
<a href="{{ url_for('admin._users') }}" id="link-users" class="dropdown-item">Users</a>
</li>
<li>
<a href="{{ url_for('admin._questions') }}" id="link-questions" class="dropdown-item">Manage Questions</a>
</li>
<li>
<a href="{{ url_for('view._view') }}" id="link-editor" class="dropdown-item">View Questions</a>
</li>
<li>
<a href="{{ url_for('editor._editor') }}" id="link-editor" class="dropdown-item">Edit Questions</a>
</li>
</ul>
</li>
<li class="nav-item dropdown" id="nav-account">
<a
class="nav-link dropdown-toggle"
id="dropdown-account"
role="button"
href="{{ url_for('admin._update_user', id=current_user.id) }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Account
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-account"
>
<li>
<a href="{{ url_for('admin._update_user', id=current_user.id) }}" id="link-account" class="dropdown-item">Account Settings</a>
</li>
<li>
<a href="{{ url_for('admin._logout') }}" id="link-logout" class="dropdown-item">Log Out</a>
</li>
</ul>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>

View File

@ -0,0 +1,18 @@
<meta name="description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta property="og:locale" content="en_UK" />
<meta property="og:type" content="website" />
<meta property="og:description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta property="og:url" content="{{ url_for(request.endpoint, _external = True, **(request.view_args or {})) }}" />
<meta property="og:site_name" content="Scottish Korfball Association Referee Theory Exam" />
<meta property="og:image" content="{{ url_for('.static', filename='favicon.png', _external = True) }}" />
<meta property="og:image:alt" content="Logo of the SKA Refereeing Exam App" />
<meta property="og:image:width" content="512" />
<meta property="og:image:height" content="512" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta name="twitter:image" content="{{ url_for('.static', filename='favicon.png', _external = True) }}" />
<meta name="twitter:image:alt" content="Logo of the SKA Refereeing Exam App" />
<meta name="twitter:creator" content="@viveksantayana" />
<meta name="twitter:site" content="@viveksantayana" />
<meta name="theme-color" content="#343a40" />
<link rel="shortcut icon" href="{{ url_for('.static', filename='favicon.ico') }}">

View File

@ -0,0 +1,23 @@
<div class="navbar navbar-expand-sm navbar-light bg-light">
<div class="container-fluid">
<div class="expand navbar-expand justify-content-center" id="navbar_secondary">
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='active') }}">Active</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='scheduled') }}">Scheduled</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='expired') }}">Expired</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='all') }}">All</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='create') }}">Create</a>
</li>
</ul>
</div>
</div>
</div>

View File

@ -0,0 +1,43 @@
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% set cookie_flash_flag = namespace(value=False) %}
{% for category, message in messages %}
{% if category == "error" %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill" title="Error" aria-title="Error"></i>
{{ message|safe }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% elif category == "success" %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check2-circle" title="Success" aria-title="Success"></i>
{{ message|safe }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% elif category == "warning" %}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<i class="bi bi-info-circle-fill" aria-title="Warning" title="Warning"></i>
{{ message|safe }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% elif category == "cookie_alert" %}
{% if not cookie_flash_flag.value %}
<div class="alert alert-primary alert-dismissible fade show" id="cookie-alert" role="alert">
<i class="bi bi-info-circle-fill" title="Cookie Alert" aria-title="Cookie Alert"></i>
{{ message|safe }}
<div class="d-flex justify-content-center w-100">
<button type="button" id="dismiss-cookie-alert" class="btn btn-success" data-bs-dismiss="alert" aria-label="Close">Accept</button>
</div>
</div>
{% set cookie_flash_flag.value = True %}
{% endif %}
{% else %}
<div class="alert alert-primary alert-dismissible fade show" role="alert">
<i class="bi bi-info-circle-fill" title="Alert"></i>
{{ message|safe }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% endfor %}
{% endif %}
{% endwith %}

View File

@ -0,0 +1,116 @@
{% extends "view/components/base.html" %}
{% block style %}
<link
rel="stylesheet"
href="{{ url_for('.static', filename='css/view.css') }}"
/>
{% endblock %}
{% block content %}
<h1>View Questions</h1>
<div class="container">
<p class="lead">
This page lists all the questions in the selected dataset.
</p>
</div>
<div class="container control-panel">
<button class="btn btn-primary" aria-title="Information" title="Information"><i class="bi bi-info-circle-fill"></i></button>
</div>
<div class="container info-panel">
<h3>
Information
</h3>
<p>
Questions in the test are arranged in blocks. Blocks can be of two types: <strong>Blocks</strong> of multiple related questions, and <strong>Single Questions</strong> that are not part of a block.
You can add, remove, or edit both Blockss and Questions through this editor.
</p>
<p>
<strong>Blocks</strong> are useful when you have a section of the test that contains multiple questions that are related to each other, for example if there is a scenario-based section where a series of questions are about the same situation.
</p>
<p>
Blocks can contain any number of questions within them, but cannot contain nested blocks.
</p>
<p>
When you set up a block, you can also add <strong>header text</strong> that will be displayed with each question.
You can use this to provide common information for a scenario across a series of questions.
</p>
<p>
Questions come in three types:
<ul>
<li>
<strong>Yes/No</strong> for when there is only a yes or no option,
</li>
<li>
<strong>Multiple Choice</strong> for your regular multiple choice questions, and
</li>
<li>
<strong>Ordered List</strong> for multiple choice questions that will be displayed in the same order as listed here.
</li>
</ul>
</p>
<p>
Normally, multiple choice questions will have the order of the options randomised.
</p>
<p>
Questions will be displayed to candidates in a randomised order.
Blocks of questions will be kept together, but the order within the block will also be randomised.
</p>
<p>
Questions can also be categorised using <strong>tags</strong>.
</p>
<p class="lead">
Placeholder for Questions Remaining in a Block
</p>
<p>
In order to show how many questions are remaining inside a block, e.g. to say &lsquo;the next n questions are about a specific scenario&rsquo;, the app uses the placeholder <code>&lt;block_remaining_questions&gt;</code>.
</p>
</div>
<div class="container viewer-panel">
<h3>
Question Dataset
</h3>
<div class="container dataset-metadata">
<div class="input-group mb-3">
<span class="input-group-text">Dataset Name</span>
<span class="form-control">
{{ dataset.get_name() }}
</span>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Author</span>
<span class="form-control">
{{ dataset.creator.get_username() }}
</span>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Last Updated</span>
<span class="form-control">
{{ dataset.date.strftime('%d %b %Y %H:%M') }}
</span>
</div>
{% if dataset.default %}
<div class="input-group mb-3">
<span class="input-group-text">
<input type="checkbox" aria-label="Default" class="dataset-default" checked disabled>
</span>
<span class="form-control">
Default Dataset
</select>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block script %}
<script>
const target = "{{ url_for('api._editor') }}"
const id = "{{ dataset.id }}"
</script>
<script
type="text/javascript"
src="{{ url_for('.static', filename='js/view.js') }}"
></script>
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends "view/components/input-forms.html" %}
{% block content %}
<div class="form-container">
<form name="form-editor" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for(request.endpoint, **request.view_args) }}">
{% include "admin/components/server-alerts.html" %}
<h2 class="form">View Questions</h2>
{{ form.hidden_tag() }}
<div class="form-select-input">
{{ form.dataset(placeholder="Select Question Dataset") }}
{{ form.dataset.label }}
</div>
{% include "admin/components/client-alerts.html" %}
<div class="container form-submission-button">
<div class="row">
<div class="col text-center">
<button class="btn btn-md btn-success btn-block" type="submit">
<i class="bi bi-book-fill button-icon"></i>
View
</button>
</div>
</div>
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,41 @@
from ..forms.admin import EditDataset
from ..models import Dataset, User
from ..tools.forms import get_dataset_choices, send_errors_to_client
from ..tools.data import check_dataset_exists
from flask import Blueprint, flash, jsonify, redirect, render_template, request
from flask.helpers import url_for
from flask_login import login_required
view = Blueprint(
name='view',
import_name=__name__,
template_folder='templates',
static_folder='static'
)
@view.route('/', methods=['GET','POST'])
@login_required
@check_dataset_exists
def _view():
form = EditDataset()
form.dataset.choices = get_dataset_choices()
if request.method == 'POST':
if form.validate_on_submit():
id = request.form.get('dataset')
return jsonify({'success': 'Selected dataset', 'redirect_to': url_for('view._view_console', id=id)}),200
return send_errors_to_client(form=form)
form.process()
return render_template('/view/index.html', form=form)
@view.route('/<string:id>/')
@login_required
@check_dataset_exists
def _view_console(id:str=None):
dataset = Dataset.query.filter_by(id=id).first()
datasets = Dataset.query.count()
users = User.query.all()
if not dataset:
flash('Invalid dataset ID.', 'error')
return redirect(url_for('admin._questions'))
return render_template('/view/console.html', dataset=dataset, datasets=datasets, users=users)