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:
parent
294f1e42f7
commit
02290e968c
@ -19,6 +19,7 @@ server {
|
||||
include /etc/nginx/ssl.conf;
|
||||
include /etc/nginx/certbot-challenge.conf;
|
||||
|
||||
# Define locations for static files to be served by Nginx
|
||||
location ^~ /quiz/static/ {
|
||||
include /etc/nginx/mime.types;
|
||||
alias /usr/share/nginx/html/quiz/static/;
|
||||
@ -34,6 +35,12 @@ server {
|
||||
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 / {
|
||||
include /etc/nginx/conf.d/proxy_headers.conf;
|
||||
proxy_pass http://reftest;
|
||||
|
@ -46,11 +46,13 @@ def create_app():
|
||||
from .quiz.views import quiz
|
||||
from .views import views
|
||||
from .editor.views import editor
|
||||
from .view.views import view
|
||||
|
||||
app.register_blueprint(admin, url_prefix='/admin')
|
||||
app.register_blueprint(api, url_prefix='/api')
|
||||
app.register_blueprint(views)
|
||||
app.register_blueprint(quiz)
|
||||
app.register_blueprint(editor, url_prefix='/admin/editor')
|
||||
app.register_blueprint(view, url_prefix='/admin/view')
|
||||
|
||||
return app
|
@ -77,10 +77,13 @@
|
||||
<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">Question Datasets</a>
|
||||
<a href="{{ url_for('admin._questions') }}" id="link-questions" class="dropdown-item">Manage Questions</a>
|
||||
</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>
|
||||
</ul>
|
||||
</li>
|
||||
|
@ -57,28 +57,37 @@
|
||||
class="btn btn-primary edit-question-dataset"
|
||||
data-id="{{ element.id }}"
|
||||
data-action="download"
|
||||
title="Download Dataset"
|
||||
title="Download Questions"
|
||||
>
|
||||
<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
|
||||
href="javascript:void(0)"
|
||||
class="btn btn-primary edit-question-dataset"
|
||||
data-id="{{ element.id }}"
|
||||
data-action="edit"
|
||||
title="Edit Dataset"
|
||||
title="Edit Questions"
|
||||
>
|
||||
<i class="bi bi-file-earmark-text-fill button-icon"></i>
|
||||
</button>
|
||||
<i class="bi bi-pencil-fill button-icon"></i>
|
||||
</a>
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
class="btn btn-danger edit-question-dataset {% if element.default %}disabled{% endif %}"
|
||||
data-id="{{ element.id }}"
|
||||
data-action="delete"
|
||||
title="Delete Dataset"
|
||||
title="Delete Questions"
|
||||
>
|
||||
<i class="bi bi-file-earmark-excel-fill button-icon"></i>
|
||||
</button>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -71,7 +71,7 @@
|
||||
margin: 30pt auto;
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
.info-panel, .viewer-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -85,3 +85,19 @@
|
||||
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;
|
||||
}
|
@ -5,24 +5,69 @@ const id = $root.data('id')
|
||||
|
||||
const $control_panel = $('.control-panel')
|
||||
const $info_panel = $('.info-panel')
|
||||
const $viewer_panel = $('.viewer-panel')
|
||||
const $editor_panel = $('.editor-panel')
|
||||
|
||||
var toggle_info = false
|
||||
var toggle_viewer = false
|
||||
|
||||
var element_index = 0
|
||||
|
||||
// Initialise Sortable and trigger renumbering on end of drag
|
||||
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){
|
||||
var action = $(this).data('action');
|
||||
|
||||
if (action == 'info') {
|
||||
if ($info_panel.is(":hidden")) {
|
||||
if ($viewer_panel.is(":visible")) {
|
||||
toggle_viewer = true
|
||||
$viewer_panel.hide()
|
||||
}
|
||||
$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()
|
||||
})
|
||||
|
||||
@ -497,3 +542,101 @@ $(window).on('load', function() {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
@ -77,10 +77,13 @@
|
||||
<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">Question Datasets</a>
|
||||
<a href="{{ url_for('admin._questions') }}" id="link-questions" class="dropdown-item">Manage Questions</a>
|
||||
</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>
|
||||
</ul>
|
||||
</li>
|
||||
|
@ -11,11 +11,12 @@
|
||||
<h1>Editor</h1>
|
||||
<div class="container">
|
||||
<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>
|
||||
</div>
|
||||
<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 class="container info-panel">
|
||||
<h3>
|
||||
@ -77,9 +78,11 @@
|
||||
In order to show how many questions are remaining inside a block, e.g. to say ‘the next n questions are about a specific scenario’, use the placeholder <code><block_remaining_questions></code>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="container viewer-panel">
|
||||
</div>
|
||||
<div class="container editor-panel">
|
||||
<h3>
|
||||
Edit Dataset
|
||||
Edit Questions
|
||||
</h3>
|
||||
<div class="container dataset-metadata">
|
||||
<div class="input-group mb-3">
|
||||
@ -98,7 +101,7 @@
|
||||
<span class="input-group-text">Last Updated</span>
|
||||
<span class="form-control">
|
||||
{{ dataset.date.strftime('%d %b %Y %H:%M') }}
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">
|
||||
|
0
ref-test/app/view/__init__.py
Normal file
0
ref-test/app/view/__init__.py
Normal file
260
ref-test/app/view/static/css/style.css
Normal file
260
ref-test/app/view/static/css/style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
|
30
ref-test/app/view/static/css/view.css
Normal file
30
ref-test/app/view/static/css/view.css
Normal 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;
|
||||
}
|
2
ref-test/app/view/static/js/jquery-3.6.0.min.js
vendored
Normal file
2
ref-test/app/view/static/js/jquery-3.6.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
115
ref-test/app/view/static/js/script.js
Normal file
115
ref-test/app/view/static/js/script.js
Normal 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()
|
||||
})
|
130
ref-test/app/view/static/js/view.js
Normal file
130
ref-test/app/view/static/js/view.js
Normal 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)
|
||||
}
|
||||
})
|
||||
})
|
84
ref-test/app/view/templates/view/components/base.html
Normal file
84
ref-test/app/view/templates/view/components/base.html
Normal 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>
|
@ -0,0 +1 @@
|
||||
<div id="alert-box" tabindex="-1"></div>
|
28
ref-test/app/view/templates/view/components/datatable.html
Normal file
28
ref-test/app/view/templates/view/components/datatable.html
Normal 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 %}
|
2
ref-test/app/view/templates/view/components/footer.html
Normal file
2
ref-test/app/view/templates/view/components/footer.html
Normal 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’s personal GIT repository</a> under an MIT License.</p>
|
||||
<p>All questions in the test are © The Scottish Korfball Association {{ now.year }}. All rights are reserved.</p>
|
@ -0,0 +1,4 @@
|
||||
{% extends "view/components/base.html" %}
|
||||
{% import "bootstrap/wtf.html" as wtf %}
|
||||
{% block top_alerts %}
|
||||
{% endblock %}
|
117
ref-test/app/view/templates/view/components/navbar.html
Normal file
117
ref-test/app/view/templates/view/components/navbar.html
Normal 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>
|
18
ref-test/app/view/templates/view/components/og-meta.html
Normal file
18
ref-test/app/view/templates/view/components/og-meta.html
Normal 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') }}">
|
@ -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>
|
@ -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 %}
|
116
ref-test/app/view/templates/view/console.html
Normal file
116
ref-test/app/view/templates/view/console.html
Normal 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 ‘the next n questions are about a specific scenario’, the app uses the placeholder <code><block_remaining_questions></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 %}
|
27
ref-test/app/view/templates/view/index.html
Normal file
27
ref-test/app/view/templates/view/index.html
Normal 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 %}
|
41
ref-test/app/view/views.py
Normal file
41
ref-test/app/view/views.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user