Added question viewer functionality

Added view questions panel to editor interface
Added view questions section of web site
Added links to navbars
This commit is contained in:
Vivek Santayana 2022-08-17 16:32:58 +01:00
parent 294f1e42f7
commit 02290e968c
26 changed files with 1253 additions and 26 deletions

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

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

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

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

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

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)