Compare commits
30 Commits
f314566591
...
editor
Author | SHA1 | Date | |
---|---|---|---|
c04c824585 | |||
8eb7fb6869 | |||
db88b84ecb | |||
13c587b7da | |||
2b2a6ddd25 | |||
26a6b45d75 | |||
c6c62fc34c | |||
6bbdb8fced | |||
c633a474b5 | |||
5af99d85b5 | |||
1e7124262e | |||
2f509af1de | |||
3c8c1b5c16 | |||
3988559920 | |||
8988fee55d | |||
86d1522ca1 | |||
ed53b771ef | |||
bc3b811fc9 | |||
4b6dbd4441 | |||
39acebb3a6 | |||
a02a58a8db | |||
7bb93afacb | |||
d83999aa43 | |||
d8d5e92453 | |||
8d91dd1d30 | |||
4ce6536e33 | |||
33bc7993fa | |||
645f69440f | |||
c197f6cb76 | |||
bed186f6b5 |
@ -96,15 +96,19 @@ $('.test-action').click(function(event) {
|
||||
// Edit Dataset Button Handlers
|
||||
$('.edit-question-dataset').click(function(event) {
|
||||
|
||||
var filename = $(this).data('filename');
|
||||
var id = $(this).data('id');
|
||||
var action = $(this).data('action');
|
||||
var disabled = $(this).hasClass('disabled');
|
||||
|
||||
if ( !disabled ) {
|
||||
if (action == 'delete') {
|
||||
$.ajax({
|
||||
url: `/admin/settings/questions/${action}/`,
|
||||
type: 'POST',
|
||||
data: JSON.stringify({'filename': filename}),
|
||||
data: JSON.stringify({
|
||||
'id': id,
|
||||
'action': action,
|
||||
}),
|
||||
contentType: 'application/json',
|
||||
success: function(response) {
|
||||
window.location.reload();
|
||||
@ -113,6 +117,11 @@ $('.edit-question-dataset').click(function(event) {
|
||||
error_response(response);
|
||||
},
|
||||
});
|
||||
} else if (action == 'edit') {
|
||||
window.location.href = `/admin/editor/${id}/`
|
||||
} else if (action == 'download') {
|
||||
window.location.href = `/admin/settings/questions/download/${id}/`
|
||||
}
|
||||
};
|
||||
event.preventDefault();
|
||||
});
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container">
|
||||
<form name="form-login" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin._home') }}">
|
||||
<form name="form-login" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ next or url_for('admin._home') }}">
|
||||
{% include "admin/components/server-alerts.html" %}
|
||||
<h2 class="form">Log In</h2>
|
||||
{{ form.hidden_tag() }}
|
||||
|
@ -79,6 +79,9 @@
|
||||
<li>
|
||||
<a href="{{ url_for('admin._questions') }}" id="link-questions" class="dropdown-item">Question Datasets</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('editor._editor') }}" id="link-editor" class="dropdown-item">Question Editor</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown" id="nav-account">
|
||||
|
@ -57,7 +57,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Uploaded
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Exams
|
||||
@ -68,7 +68,9 @@
|
||||
{% for dataset in datasets %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ dataset.date.strftime('%d %b %Y %H:%M') }}
|
||||
<a href="{{ url_for('editor._editor_console', id=dataset.id) }}">
|
||||
{{ dataset.get_name() }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ dataset.tests|length }}
|
||||
|
@ -9,9 +9,12 @@
|
||||
<tr>
|
||||
<th>
|
||||
|
||||
</th>
|
||||
<th data-priority="1">
|
||||
Name
|
||||
</th>
|
||||
<th data-priority="2">
|
||||
Uploaded
|
||||
Updated
|
||||
</th>
|
||||
<th data-priority="3">
|
||||
Author
|
||||
@ -36,6 +39,9 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ element.get_name() }}
|
||||
</td>
|
||||
<td>
|
||||
{{ element.date.strftime('%d %b %Y %H:%M') }}
|
||||
</td>
|
||||
@ -47,18 +53,27 @@
|
||||
</td>
|
||||
<td class="row-actions">
|
||||
<a
|
||||
href="#"
|
||||
class="btn btn-primary edit-question-dataset {% if element.filename == default %}disabled{% endif %}"
|
||||
data-filename="{{ element.filename }}"
|
||||
data-action="default"
|
||||
title="Make Default"
|
||||
href="javascript:void(0)"
|
||||
class="btn btn-primary edit-question-dataset"
|
||||
data-id="{{ element.id }}"
|
||||
data-action="download"
|
||||
title="Download Dataset"
|
||||
>
|
||||
<i class="bi bi-cloud-arrow-down-fill button-icon"></i>
|
||||
</button>
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
class="btn btn-primary edit-question-dataset"
|
||||
data-id="{{ element.id }}"
|
||||
data-action="edit"
|
||||
title="Edit Dataset"
|
||||
>
|
||||
<i class="bi bi-file-earmark-text-fill button-icon"></i>
|
||||
</button>
|
||||
<a
|
||||
href="#"
|
||||
class="btn btn-danger edit-question-dataset {% if element.filename == default %}disabled{% endif %}"
|
||||
data-filename="{{ element.filename }}"
|
||||
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"
|
||||
>
|
||||
@ -72,13 +87,23 @@
|
||||
{% else %}
|
||||
<div class="alert alert-primary alert-db-empty">
|
||||
<i class="bi bi-info-circle-fill" aria-title="Alert" title="Alert"></i>
|
||||
There are no question datasets uploaded. Please use the panel below to upload a new question dataset.
|
||||
There are no question datasets uploaded. Please use the panel below to upload a new question dataset or create a new dataset using the editor console.
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col text-center">
|
||||
<button title="Create New" class="btn btn-md btn-primary btn-block create-new-dataset">
|
||||
<i class="bi bi-cloud-plus-fill button-icon"></i>
|
||||
Create New Dataset
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-container">
|
||||
<form name="form-upload-questions" class="form-display" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="" enctype="multipart/form-data">
|
||||
<h2 class="form-heading">Upload Question Dataset</h2>
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="form-label-group">
|
||||
{{ form.name(class_="form-control", autofocus=true, placeholder="Enter Name of Dataset") }}
|
||||
{{ form.name.label }}
|
||||
</div>
|
||||
<div class="form-upload">
|
||||
{{ form.data_file() }}
|
||||
</div>
|
||||
@ -89,8 +114,8 @@
|
||||
<div class="container form-submission-button">
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<button title="Create User" class="btn btn-md btn-success btn-block" type="submit">
|
||||
<i class="bi bi-file-earmark-arrow-up-fill button-icon"></i>
|
||||
<button title="Upload Dataset" class="btn btn-md btn-success btn-block" type="submit">
|
||||
<i class="bi bi-cloud-arrow-up-fill button-icon"></i>
|
||||
Upload Dataset
|
||||
</button>
|
||||
</div>
|
||||
@ -106,10 +131,10 @@
|
||||
$(document).ready(function() {
|
||||
$('#question-datasets-table').DataTable({
|
||||
'columnDefs': [
|
||||
{'sortable': false, 'targets': [0,4]},
|
||||
{'searchable': false, 'targets': [0,3,4]}
|
||||
{'sortable': false, 'targets': [0,5]},
|
||||
{'searchable': false, 'targets': [1,2,3]}
|
||||
],
|
||||
'order': [[1, 'desc'], [2, 'asc']],
|
||||
'order': [[1, 'asc'], [2, 'desc'], [3, 'asc']],
|
||||
'responsive': 'true',
|
||||
'fixedHeader': 'true',
|
||||
});
|
||||
|
@ -1 +0,0 @@
|
||||
{% extends "admin/components/base.html" %}
|
@ -5,12 +5,13 @@ from ..tools.forms import get_dataset_choices, get_time_options, send_errors_to_
|
||||
from ..tools.data import check_is_json, validate_json
|
||||
from ..tools.test import answer_options, get_correct_answers
|
||||
|
||||
from flask import Blueprint, jsonify, render_template, redirect, request, session
|
||||
from flask import abort, Blueprint, jsonify, render_template, redirect, request, send_file, session
|
||||
from flask.helpers import flash, url_for
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
from datetime import date, datetime
|
||||
from json import loads
|
||||
from os import path
|
||||
import secrets
|
||||
|
||||
admin = Blueprint(
|
||||
@ -207,10 +208,13 @@ def _questions():
|
||||
if form.validate_on_submit():
|
||||
upload = form.data_file.data
|
||||
if not check_is_json(upload): return jsonify({'error': 'Invalid file. Please upload a JSON file.'}), 400
|
||||
if not validate_json(upload): return jsonify({'error': 'The data in the file is invalid.'}), 400
|
||||
upload.stream.seek(0)
|
||||
data = loads(upload.read())
|
||||
if not validate_json(data=data): return jsonify({'error': 'The data in the file is invalid.'}), 400
|
||||
new_dataset = Dataset()
|
||||
new_dataset.set_name(request.form.get('name'))
|
||||
success, message = new_dataset.create(
|
||||
upload = upload,
|
||||
data = data,
|
||||
default = request.form.get('default')
|
||||
)
|
||||
if success: return jsonify({'success': message}), 200
|
||||
@ -220,18 +224,25 @@ def _questions():
|
||||
data = Dataset.query.all()
|
||||
return render_template('/admin/settings/questions.html', form=form, data=data)
|
||||
|
||||
@admin.route('/settings/questions/edit/', methods=['POST'])
|
||||
@admin.route('/settings/questions/delete/', methods=['POST'])
|
||||
@login_required
|
||||
def _edit_questions():
|
||||
id = request.get_json()['id']
|
||||
action = request.get_json()['action']
|
||||
if action not in ['defailt', 'delete']: return jsonify({'error': 'Invalid action.'}), 400
|
||||
if not action == 'delete': return jsonify({'error': 'Invalid action.'}), 400
|
||||
dataset = Dataset.query.filter_by(id=id).first()
|
||||
if action == 'delete': success, message = dataset.delete()
|
||||
elif action == 'default': success, message = dataset.make_default()
|
||||
if success: return jsonify({'success': message}), 200
|
||||
return jsonify({'error': message}), 400
|
||||
|
||||
@admin.route('/settings/questions/download/<string:id>/')
|
||||
@login_required
|
||||
def _download(id:str):
|
||||
dataset = Dataset.query.filter_by(id=id).first()
|
||||
if not dataset: return abort(404)
|
||||
data_path = path.abspath(dataset.get_file())
|
||||
return send_file(data_path, as_attachment=True, attachment_filename=f'{dataset.get_name()}.json')
|
||||
|
||||
@admin.route('/tests/<string:filter>/', methods=['GET'])
|
||||
@admin.route('/tests/', methods=['GET'])
|
||||
@login_required
|
||||
|
@ -1,7 +1,9 @@
|
||||
from ..models import Dataset, Entry
|
||||
from ..models import Dataset, Entry, User
|
||||
from ..tools.data import validate_json
|
||||
from ..tools.test import evaluate_answers, generate_questions
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
from flask import Blueprint, flash, jsonify, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from json import loads
|
||||
@ -26,7 +28,7 @@ def _fetch_questions():
|
||||
time_adjustment = test.adjustments[user_code]
|
||||
_time_limit += time_adjustment
|
||||
end_delta = timedelta(minutes=_time_limit)
|
||||
end_time = datetime.utcnow() + end_delta
|
||||
end_time = datetime.now() + end_delta
|
||||
else:
|
||||
end_time = None
|
||||
entry.start()
|
||||
@ -64,3 +66,36 @@ def _submit_quiz():
|
||||
'id': id
|
||||
}), 200
|
||||
|
||||
@api.route('/editor/', methods=['POST'])
|
||||
@login_required
|
||||
def _editor(id:str=None):
|
||||
request_data = request.get_json()
|
||||
id = request_data['id']
|
||||
dataset = Dataset.query.filter_by(id=id).first()
|
||||
if not dataset: return jsonify({'error': 'Invalid request. Dataset not found.'}), 404
|
||||
data_path = dataset.get_file()
|
||||
if request_data['action'] == 'fetch':
|
||||
with open(data_path, 'r') as data_file:
|
||||
data = loads(data_file.read())
|
||||
return jsonify({'success': 'Successfully downloaded dataset', 'data': data}), 200
|
||||
default = request_data['default']
|
||||
creator = request_data['creator']
|
||||
name = request_data['name']
|
||||
data = request_data['data']
|
||||
if not validate_json(data): return jsonify({'error': 'The data you submitted was invalid.'}), 400
|
||||
user = User.query.filter_by(id=creator).first()
|
||||
dataset.set_name(name)
|
||||
dataset.creator = user
|
||||
success, message = dataset.update(data=data, default=default)
|
||||
if not success: return jsonify({'error': message}), 400
|
||||
return jsonify({'success': message}), 200
|
||||
|
||||
@api.route('/editor/new/', methods=['POST'])
|
||||
@login_required
|
||||
def _editor_new():
|
||||
new_dataset = Dataset()
|
||||
new_dataset.set_name('New Dataset')
|
||||
success, message = new_dataset.create(data=[], default=False)
|
||||
if not success: return jsonify({'error':message}), 400
|
||||
flash(message, 'success')
|
||||
return jsonify({'success': message, 'redirect_to': url_for('editor._editor_console', id=new_dataset.id)}), 200
|
@ -13,22 +13,35 @@
|
||||
}
|
||||
|
||||
.editor-controls a {
|
||||
margin: 10px 10px;
|
||||
}
|
||||
|
||||
.editor-controls a i {
|
||||
font-size: larger;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.option-controls, .block-controls {
|
||||
width: fit-content;
|
||||
display: block;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
.option-controls a, .block-controls a {
|
||||
margin: 0 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.accordion-button div {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
.option-controls a i, .block-controls a i {
|
||||
font-size: larger;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.accordion-button a {
|
||||
transform: translate(-50%, -50%);
|
||||
.accordion-button div {
|
||||
margin: 0;
|
||||
position: relative;
|
||||
top: 50%;
|
||||
right: 0%;
|
||||
position: absolute;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
|
||||
.accordion-button::after {
|
||||
@ -44,3 +57,31 @@
|
||||
background-color: #bb2d3b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.panel-button {
|
||||
padding: 6px;
|
||||
margin: 0px 2px;
|
||||
}
|
||||
|
||||
.panel-button i {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.editor-panel, .info-panel {
|
||||
margin: 30pt auto;
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
width:fit-content;
|
||||
}
|
||||
|
||||
#alert-box {
|
||||
margin: 30px auto;
|
||||
max-width: 460px;
|
||||
}
|
@ -1,68 +1,499 @@
|
||||
const root = $('#editor-root')
|
||||
// Variable Declarations
|
||||
const $root = $('#editor-root')
|
||||
const target = $root.data('target')
|
||||
const id = $root.data('id')
|
||||
|
||||
var data = [
|
||||
{
|
||||
"type": "question",
|
||||
"q_no": 3,
|
||||
"text": "The ball is gathered by the defensive division and a player throws it forward, hitting the korf in the other division.",
|
||||
"options": [
|
||||
"Play on",
|
||||
"Opposite team restart under their defensive post",
|
||||
"Opposite team restart where the ball was thrown from"
|
||||
],
|
||||
"correct": 0,
|
||||
"q_type": "Multiple Choice",
|
||||
"tags": [
|
||||
"scoring from the defence zone"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "question",
|
||||
"q_no": 4,
|
||||
"text": "At the end of the game, after the final whistle, a coach loudly confronts the referee. What can the referee do?",
|
||||
"options": [
|
||||
"Ignore the coach as the match has ended",
|
||||
"Ignore the coach or inform the coach they will be reported",
|
||||
"Ignore the coach, inform the coach they will be reported and/or show the coach a card"
|
||||
],
|
||||
"correct": 2,
|
||||
"q_type": "List",
|
||||
"tags": [
|
||||
"discipline"
|
||||
]
|
||||
},
|
||||
]
|
||||
const $control_panel = $('.control-panel')
|
||||
const $info_panel = $('.info-panel')
|
||||
const $editor_panel = $('.editor-panel')
|
||||
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
if (data[i]['type'] == 'question') {
|
||||
var obj = `
|
||||
<div class="accordion-item" id="i${i}">
|
||||
<h2 class="accordion-header" id="h${i}">
|
||||
<div class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#c${i}" aria-expanded="true" aria-controls="c${i}">
|
||||
<div class="float-start">Question ${i+1}</div>
|
||||
<a href="javascript:void(0)" class="btn btn-success panel-control" data-id="${i}" data-action="remove">X</a>
|
||||
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
|
||||
$control_panel.find('button').click(function(event){
|
||||
if ($info_panel.is(":hidden")) {
|
||||
$editor_panel.hide()
|
||||
$info_panel.fadeIn()
|
||||
$(this).addClass('active')
|
||||
} else {
|
||||
$info_panel.hide()
|
||||
$editor_panel.fadeIn()
|
||||
$(this).removeClass('active')
|
||||
}
|
||||
event.preventDefault()
|
||||
})
|
||||
|
||||
// Control Button Listeners
|
||||
$root.on('click', '.block-controls > a', function(event){
|
||||
event.preventDefault()
|
||||
var action = $(this).data('action')
|
||||
var root_accordion = $(this).closest('div').siblings('.accordion')
|
||||
if (action == 'add-question') {
|
||||
var question = generate_single_question(root_container_id=`#${root_accordion.attr('id')}`)
|
||||
$(question).appendTo(root_accordion).hide().fadeIn()
|
||||
if (root_accordion.children().length > 1 ) {
|
||||
root_accordion.find('.panel-controls > a[data-action="delete"]').removeClass('disabled')
|
||||
} else {
|
||||
root_accordion.find('.panel-controls > a[data-action="delete"]').addClass('disabled')
|
||||
}
|
||||
renumber_blocks()
|
||||
}
|
||||
})
|
||||
|
||||
$root.on('click', '.panel-controls > a', function(event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
var action = $(this).data('action')
|
||||
var element = $(this).closest('.accordion-item')
|
||||
var root_container = $(this).closest('.accordion')
|
||||
if (action == 'delete') {
|
||||
element.fadeOut(function(){
|
||||
$(this).remove()
|
||||
renumber_blocks()
|
||||
if (root_container.get(0) != $root.get(0) && root_container.children().length < 2 ) {
|
||||
root_container.find('.panel-controls > a[data-action="delete"]').addClass('disabled')
|
||||
}
|
||||
})
|
||||
} else if (action == 'add-question') {
|
||||
var question = generate_single_question(root_container_id=`#${root_container.attr('id')}`)
|
||||
$(question).insertBefore(element).hide().fadeIn()
|
||||
if (root_container.get(0) != $root.get(0) && root_container.children().length > 1 ) {
|
||||
root_container.find('.panel-controls > a[data-action="delete"]').removeClass('disabled')
|
||||
}
|
||||
} else if (action == 'add-block') {
|
||||
var block = generate_block(root_container_id=`#${root_container.attr('id')}`)
|
||||
$(block).insertBefore(element).hide().fadeIn()
|
||||
var block_container = $(`#element${element_index-1}`).children().find('.accordion')
|
||||
Sortable.create(block_container.get(0), {handle: '.move-handle', onEnd: function(evt) {renumber_blocks()}})
|
||||
var question = generate_single_question(root_container_id=`#${block_container.attr('id')}`)
|
||||
block_container.append(question)
|
||||
block_container.find('.panel-controls > a[data-action="delete"]').addClass('disabled')
|
||||
}
|
||||
renumber_blocks()
|
||||
})
|
||||
|
||||
$root.on('click', '.option-controls > a', function(event) {
|
||||
event.preventDefault()
|
||||
var action = $(this).data('action')
|
||||
var options = $(this).closest('div.option-controls').siblings('.options')
|
||||
var length = options.children().length
|
||||
var correct = $(this).closest('div.option-controls').siblings().find('.question-correct')
|
||||
if (action == 'delete') {
|
||||
if (length > 2) {
|
||||
options.children().last().fadeOut(function(){
|
||||
$(this).remove()
|
||||
length = options.children().length
|
||||
if (length <= 2) {
|
||||
options.siblings('div.option-controls').children('a[data-action="delete"]').addClass('disabled')
|
||||
} else {
|
||||
options.siblings('div.option-controls').children('a[data-action="delete"]').removeClass('disabled')
|
||||
}
|
||||
})
|
||||
correct.children().last().fadeOut(function(){
|
||||
$(this).remove()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
var opt = `
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">${length}</span>
|
||||
<input type="text" class="form-control" value="Option ${length}">
|
||||
</div>
|
||||
`
|
||||
$(opt).appendTo(options).hide().fadeIn()
|
||||
var cor = `<option value="${length}">${length}</option>`
|
||||
correct.append(cor)
|
||||
}
|
||||
length = options.children().length
|
||||
if (length <= 2) {
|
||||
$(this).closest('div.option-controls').children('a[data-action="delete"]').addClass('disabled')
|
||||
} else {
|
||||
$(this).closest('div.option-controls').children('a[data-action="delete"]').removeClass('disabled')
|
||||
}
|
||||
})
|
||||
|
||||
$('.editor-controls > a').click(function(event){
|
||||
event.preventDefault()
|
||||
var action = $(this).data('action')
|
||||
var root_accordion = $(this).closest('div').siblings('.accordion')
|
||||
if (action == 'add-question') {
|
||||
var obj = generate_single_question(root_container_id=`#${root_accordion.attr('id')}`)
|
||||
$(obj).appendTo($root).hide().fadeIn()
|
||||
} else if (action == 'add-block') {
|
||||
var obj = generate_block(root_container_id=`#${root_accordion.attr('id')}`)
|
||||
$(obj).appendTo($root).hide().fadeIn()
|
||||
var block_container = $(`#element${element_index-1}`).children().find('.accordion')
|
||||
Sortable.create(block_container.get(0), {handle: '.move-handle', onEnd: function(evt) {renumber_blocks()}})
|
||||
var question = generate_single_question(root_container_id=`#${block_container.attr('id')}`)
|
||||
block_container.append(question)
|
||||
block_container.find('.panel-controls > a[data-action="delete"]').addClass('disabled')
|
||||
} else if (action == 'discard') {
|
||||
window.location.href = '/admin/settings/questions/'
|
||||
} else if (action == 'delete') {
|
||||
$.ajax({
|
||||
url: '/admin/settings/questions/delete/',
|
||||
type: 'POST',
|
||||
data: JSON.stringify({
|
||||
'id': id,
|
||||
'action': action
|
||||
}),
|
||||
contentType: 'application/json',
|
||||
success: function(response) {
|
||||
window.location.href = '/admin/settings/questions/'
|
||||
},
|
||||
error: function(response) {
|
||||
error_response(response)
|
||||
}
|
||||
})
|
||||
} else if (action == 'save') {
|
||||
var input = parse_input()
|
||||
var def = $('.dataset-default').is(':checked')
|
||||
var name = $('.dataset-name').val()
|
||||
var creator = $('.dataset-creator').val()
|
||||
console.log([def, name, creator])
|
||||
$.ajax({
|
||||
url: target,
|
||||
type: 'POST',
|
||||
data: JSON.stringify({
|
||||
'id': id,
|
||||
'action': 'upload',
|
||||
'data': input,
|
||||
'default': def,
|
||||
'name': name,
|
||||
'creator': creator
|
||||
}),
|
||||
contentType: 'application/json',
|
||||
success: function(response) {
|
||||
window.location.href = '/admin/settings/questions/'
|
||||
},
|
||||
error: function(response) {
|
||||
error_response(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
renumber_blocks()
|
||||
})
|
||||
|
||||
// Question Type Select Menu Listener
|
||||
$root.on('change', '.form-select.question-type', function(event) {
|
||||
event.preventDefault()
|
||||
var type = $(this).val()
|
||||
var options = $(this).closest('div.input-group').siblings('.options')
|
||||
var option_controls = $(this).closest('div.input-group').siblings('.option-controls')
|
||||
var correct = $(this).closest('div.input-group').siblings().find('.question-correct')
|
||||
if (type == 'Yes/No') {
|
||||
options.empty()
|
||||
correct.empty()
|
||||
var opt = `
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">0</span>
|
||||
<input type="text" class="form-control" value="Yes" disabled>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">1</span>
|
||||
<input type="text" class="form-control" value="No" disabled>
|
||||
</div>
|
||||
`
|
||||
$(opt).appendTo(options).hide().fadeIn()
|
||||
option_controls.children('a').addClass('disabled')
|
||||
var cor = `
|
||||
<option value ="0" default>0</option>
|
||||
<option value="1">1</option>
|
||||
`
|
||||
correct.append(cor)
|
||||
} else {
|
||||
option_controls.children('a').removeClass('disabled')
|
||||
options.find('input').removeAttr('disabled')
|
||||
if (options.children().length <= 2 ){
|
||||
option_controls.children('a[data-action="delete"]').addClass('disabled')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Data and Rendering Functions
|
||||
function renumber_blocks () {
|
||||
$( ".block-number" ).each(function(index) {
|
||||
$( this ).text($( this ).closest('.accordion-item').index() + 1)
|
||||
})
|
||||
}
|
||||
|
||||
function parse_input() {
|
||||
var data = []
|
||||
var element = {}
|
||||
var question = {}
|
||||
var block_container
|
||||
var q_no = 0
|
||||
$root.children().each(function(index) {
|
||||
element = {}
|
||||
if ($(this).data('type') == 'block') {
|
||||
element['type'] = 'block'
|
||||
element['question_header'] = $(this).find('.block-header-text').val()
|
||||
element['questions'] = []
|
||||
block_container = $(this).children().find('.accordion')
|
||||
block_container.children().each(function(index) {
|
||||
question = {}
|
||||
question['q_no'] = q_no
|
||||
question['text'] = $(this).find('.question-text').val()
|
||||
question['q_type'] = $(this).find('.question-type').val()
|
||||
question['correct'] = parseInt($(this).find('.question-correct').val())
|
||||
question['options'] = []
|
||||
$(this).find('.options').find('input').each(function(index) {
|
||||
question['options'].push($(this).val())
|
||||
})
|
||||
question['tags'] = $(this).find('.question-tags').val().split('\r\n')
|
||||
element['questions'].push(question)
|
||||
q_no ++
|
||||
})
|
||||
} else if ( $(this).data('type') == 'question') {
|
||||
element['type'] = 'question'
|
||||
element['q_no'] = q_no
|
||||
element['text'] = $(this).find('.question-text').val()
|
||||
element['q_type'] = $(this).find('.question-type').val()
|
||||
element['correct'] = parseInt($(this).find('.question-correct').val())
|
||||
element['options'] = []
|
||||
$(this).find('.options').find('input').each(function(index) {
|
||||
element['options'].push($(this).val())
|
||||
})
|
||||
element['tags'] = $(this).find('.question-tags').val().split('\r\n')
|
||||
q_no ++
|
||||
}
|
||||
data.push(element)
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
function parse_data(data) {
|
||||
var block, obj, new_block, block_container, question, _question, new_question, options, correct, opt, tags
|
||||
for (let c = 0; c < data.length; c++) {
|
||||
block = data[c]
|
||||
if (block['type'] == 'block') {
|
||||
obj = generate_block(root_container_id=`#${$root.attr('id')}`)
|
||||
$root.append(obj)
|
||||
new_block = $(`#element${element_index-1}`)
|
||||
new_block.find('.block-header-text').val(block['question_header']).trigger('change')
|
||||
block_container = $(`#element${element_index-1}`).children().find('.accordion')
|
||||
Sortable.create(block_container.get(0), {handle: '.move-handle', onEnd: function(evt) {renumber_blocks()}})
|
||||
for (let _c = 0; _c < block['questions'].length; _c ++) {
|
||||
question = block['questions'][_c]
|
||||
_question = generate_single_question(root_container_id=`#${block_container.attr('id')}`)
|
||||
block_container.append(_question)
|
||||
if (block_container.children().length <= 1) {
|
||||
block_container.find('.panel-controls > a[data-action="delete"]').addClass('disabled')
|
||||
} else {
|
||||
block_container.find('.panel-controls > a[data-action="delete"]').removeClass('disabled')
|
||||
}
|
||||
new_question = $(`#element${element_index-1}`)
|
||||
new_question.find('.question-text').val(question['text']).trigger('change')
|
||||
new_question.find('.question-type').val(question['q_type']).trigger('change')
|
||||
correct = new_question.find('.question-correct')
|
||||
if (question['q_type'] != 'Yes/No') {
|
||||
options = new_question.find('.options')
|
||||
options.empty()
|
||||
correct.empty()
|
||||
for ( var __c = 0; __c < question['options'].length; __c++) {
|
||||
option = question['options'][__c]
|
||||
opt = `
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">${__c}</span>
|
||||
<input type="text" class="form-control" value="${option}">
|
||||
</div>
|
||||
`
|
||||
options.append(opt)
|
||||
correct.append(`<option value="${__c}">${__c}</option>`)
|
||||
}
|
||||
}
|
||||
correct.val(String(question['correct']))
|
||||
tags = question['tags'].join('\r\n')
|
||||
new_question.find('.question-tags').val(tags)
|
||||
}
|
||||
} else {
|
||||
question = block
|
||||
obj = generate_single_question(root_container_id=`#${$root.attr('id')}`)
|
||||
$root.append(obj)
|
||||
new_question = $(`#element${element_index-1}`)
|
||||
new_question.find('.question-text').val(question['text']).trigger('change')
|
||||
new_question.find('.question-type').val(question['q_type']).trigger('change')
|
||||
correct = new_question.find('.question-correct')
|
||||
if (question['q_type'] != 'Yes/No') {
|
||||
options = new_question.find('.options')
|
||||
options.empty()
|
||||
correct.empty()
|
||||
for ( var _c = 0; _c < question['options'].length; _c++) {
|
||||
option = question['options'][_c]
|
||||
opt = `
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">${_c}</span>
|
||||
<input type="text" class="form-control" value="${option}">
|
||||
</div>
|
||||
`
|
||||
options.append(opt)
|
||||
correct.append(`<option value="${_c}">${_c}</option>`)
|
||||
}
|
||||
}
|
||||
correct.val(String(question['correct']))
|
||||
tags = question['tags'].join('\r\n')
|
||||
new_question.find('.question-tags').val(tags)
|
||||
}
|
||||
}
|
||||
renumber_blocks()
|
||||
}
|
||||
|
||||
// Content Generator Functions
|
||||
function generate_single_question(root_container_id) {
|
||||
if (root_container_id == `#${$root.attr('id')}`) {
|
||||
var block_button = `
|
||||
<a href="javascript:void(0)" class="btn btn-primary" data-action="add-block" title="Add Block Above" aria-title="Add Block Above" data-bs-toggle="collapse" data-bs-target>
|
||||
<i class="bi bi-folder-plus"></i>
|
||||
</a>
|
||||
`
|
||||
} else {
|
||||
var block_button = ''
|
||||
}
|
||||
var question = `
|
||||
<div class="accordion-item" id="element${element_index}" data-type="question">
|
||||
<h2 class="accordion-header" id="element${element_index}-header">
|
||||
<div class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#element${element_index}-content" aria-expanded="true" aria-controls="element${element_index}-content">
|
||||
<div class="float-start">
|
||||
<div class="accordion-caption">
|
||||
<span class="block-number"></span>.
|
||||
Question
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-controls float-end">
|
||||
<a href="javascript:void(0)" class="btn btn-success move-handle" data-action="move-question" title="Move Question" aria-title="Move Question" data-bs-toggle="collapse" data-bs-target>
|
||||
<i class="bi bi-arrows-move"></i>
|
||||
</a>
|
||||
${block_button}
|
||||
<a href="javascript:void(0)" class="btn btn-primary" data-action="add-question" title="Add Question Above" aria-title="Add Question Above" data-bs-toggle="collapse" data-bs-target>
|
||||
<i class="bi bi-file-plus-fill"></i>
|
||||
</a>
|
||||
<a href="javascript:void(0)" class="btn btn-danger" data-action="delete" title="Delete Block" aria-title="Delete Block" data-bs-toggle="collapse" data-bs-target>
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
<div id="c${i}" class="accordion-collapse collapse" aria-labelledby="h${i}" data-bs-parent="#editor-root">
|
||||
<div id="element${element_index}-content" class="accordion-collapse collapse" aria-labelledby="element${element_index}-header" data-bs-parent="${root_container_id}">
|
||||
<div class="accordion-body">
|
||||
<div class="question-text">
|
||||
${data[i]['text']}
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Question</span>
|
||||
<textarea type="text" class="form-control question-text">Enter question here.</textarea>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Question Type</span>
|
||||
<select class="form-select question-type">
|
||||
<option value ="Multiple Choice" default>Multiple Choice</option>
|
||||
<option value="Yes/No">Yes/No</option>
|
||||
<option value="List">Ordered List</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="form-label">Options</label>
|
||||
<ul class="options">
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">0</span>
|
||||
<input type="text" class="form-control" value="Option 0">
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">1</span>
|
||||
<input type="text" class="form-control" value="Option 1">
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
${data[i]['options'].join("</li><li>")}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="option-controls">
|
||||
<a href="javascript:void(0)" class="btn btn-danger disabled" data-action="delete" title="Delete Question" aria-title="Delete Question">
|
||||
<i class="bi bi-patch-minus-fill"></i>
|
||||
Delete
|
||||
</a>
|
||||
<a href="javascript:void(0)" class="btn btn-success" data-action="add" title="Add Question" aria-title="Add Question">
|
||||
<i class="bi bi-patch-plus-fill"></i>
|
||||
Add
|
||||
</a>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Correct</span>
|
||||
<select class="form-select question-correct">
|
||||
<option value ="0" default>0</option>
|
||||
<option value="1">1</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Tags</span>
|
||||
<textarea type="text" class="form-control question-tags"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
root.append(obj)
|
||||
}
|
||||
element_index ++
|
||||
return question
|
||||
}
|
||||
|
||||
$('.panel-control').click(function(event) {
|
||||
console.log($(this).data('id'))
|
||||
var id = $(this).data('id')
|
||||
$(`#i${id}`).remove()
|
||||
function generate_block(root_container_id) {
|
||||
var block = `
|
||||
<div class="accordion-item" id="element${element_index}" data-type="block">
|
||||
<h2 class="accordion-header" id="element${element_index}-header">
|
||||
<div class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#element${element_index}-content" aria-expanded="true" aria-controls="element${element_index}-content">
|
||||
<div class="float-start">
|
||||
<div class="accordion-caption">
|
||||
<span class="block-number"></span>.
|
||||
Block
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-controls float-end">
|
||||
<a href="javascript:void(0)" class="btn btn-success move-handle" data-action="move-question" title="Move Question" aria-title="Move Question" data-bs-toggle="collapse" data-bs-target>
|
||||
<i class="bi bi-arrows-move"></i>
|
||||
</a>
|
||||
<a href="javascript:void(0)" class="btn btn-primary" data-action="add-block" title="Add Block Above" aria-title="Add Block Above" data-bs-toggle="collapse" data-bs-target>
|
||||
<i class="bi bi-folder-plus"></i>
|
||||
</a>
|
||||
<a href="javascript:void(0)" class="btn btn-primary" data-action="add-question" title="Add Question Above" aria-title="Add Question Above" data-bs-toggle="collapse" data-bs-target>
|
||||
<i class="bi bi-file-plus-fill"></i>
|
||||
</a>
|
||||
<a href="javascript:void(0)" class="btn btn-danger" data-action="delete" title="Delete Block" aria-title="Delete Block" data-bs-toggle="collapse" data-bs-target>
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
<div id="element${element_index}-content" class="accordion-collapse collapse" aria-labelledby="element${element_index}-header" data-bs-parent="${root_container_id}">
|
||||
<div class="accordion-body">
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Block Header</span>
|
||||
<textarea type="text" class="form-control block-header-text">Enter the header text for this block of questions.</textarea>
|
||||
</div>
|
||||
<div class="accordion" id="element${element_index}-questions">
|
||||
</div>
|
||||
<div class="block-controls">
|
||||
<a href="javascript:void(0)" class="btn btn-success" data-action="add-question" title="Add Question" aria-title="Add Question">
|
||||
<i class="bi bi-file-plus-fill"></i>
|
||||
Add Question
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
element_index ++
|
||||
return block
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
})
|
@ -42,81 +42,6 @@ $('form.form-post').submit(function(event) {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
// Form Upload Questions - Special case, needs to handle files.
|
||||
$('form[name=form-upload-questions]').submit(function(event) {
|
||||
|
||||
var $form = $(this);
|
||||
var data = new FormData($form[0]);
|
||||
var file = $('input[name=data_file]')[0].files[0]
|
||||
data.append('file', file)
|
||||
|
||||
$.ajax({
|
||||
url: window.location.pathname,
|
||||
type: 'POST',
|
||||
data: data,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(response) {
|
||||
window.location.reload();
|
||||
},
|
||||
error: function(response) {
|
||||
error_response(response);
|
||||
}
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
// Edit and Delete Test Button Handlers
|
||||
$('.test-action').click(function(event) {
|
||||
|
||||
let id = $(this).data('id');
|
||||
let action = $(this).data('action');
|
||||
|
||||
if (action == 'delete' || action == 'start' || action == 'end') {
|
||||
$.ajax({
|
||||
url: `/admin/tests/edit/`,
|
||||
type: 'POST',
|
||||
data: JSON.stringify({'id': id, 'action': action}), // TODO Change how CRUD operations work
|
||||
contentType: 'application/json',
|
||||
success: function(response) {
|
||||
window.location.href = '/admin/tests/';
|
||||
},
|
||||
error: function(response){
|
||||
error_response(response);
|
||||
},
|
||||
});
|
||||
} else if (action == 'edit') {
|
||||
window.location.href = `/admin/test/${id}/`
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
// Edit Dataset Button Handlers
|
||||
$('.edit-question-dataset').click(function(event) {
|
||||
|
||||
var filename = $(this).data('filename');
|
||||
var action = $(this).data('action');
|
||||
var disabled = $(this).hasClass('disabled');
|
||||
|
||||
if ( !disabled ) {
|
||||
$.ajax({
|
||||
url: `/admin/settings/questions/${action}/`,
|
||||
type: 'POST',
|
||||
data: JSON.stringify({'filename': filename}),
|
||||
contentType: 'application/json',
|
||||
success: function(response) {
|
||||
window.location.reload();
|
||||
},
|
||||
error: function(response){
|
||||
error_response(response);
|
||||
},
|
||||
});
|
||||
};
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
function error_response(response) {
|
||||
|
||||
const $alert = $("#alert-box");
|
||||
@ -168,66 +93,23 @@ $('#dismiss-cookie-alert').click(function(event){
|
||||
event.preventDefault();
|
||||
})
|
||||
|
||||
// Script for Result Actions
|
||||
$('.result-action-buttons').click(function(event){
|
||||
|
||||
var id = $(this).data('id');
|
||||
|
||||
if ($(this).data('result-action') == 'generate') {
|
||||
// Create New Dataset
|
||||
$('.create-new-dataset').click(function(event){
|
||||
$.ajax({
|
||||
url: '/admin/certificate/',
|
||||
url: '/api/editor/new/',
|
||||
type: 'POST',
|
||||
data: JSON.stringify({'id': id}),
|
||||
contentType: 'application/json',
|
||||
dataType: 'html',
|
||||
success: function(response) {
|
||||
var display_window = window.open();
|
||||
display_window.document.write(response);
|
||||
data: {
|
||||
time: Date.now()
|
||||
},
|
||||
error: function(response){
|
||||
error_response(response);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
var action = $(this).data('result-action')
|
||||
$.ajax({
|
||||
url: window.location.href,
|
||||
type: 'POST',
|
||||
data: JSON.stringify({'id': id, 'action': action}),
|
||||
contentType: 'application/json',
|
||||
success: function(response) {
|
||||
if (action == 'delete') {
|
||||
window.location.href = '/admin/results/';
|
||||
} else window.location.reload();
|
||||
},
|
||||
error: function(response){
|
||||
error_response(response);
|
||||
},
|
||||
});
|
||||
dataType: 'json',
|
||||
success: function(response){
|
||||
if (response.redirect_to) {
|
||||
window.location.href = response.redirect_to;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
// Script for Deleting Time Adjustment
|
||||
$('.adjustment-delete').click(function(event){
|
||||
|
||||
var user_code = $(this).data('user_code');
|
||||
var location = window.location.href;
|
||||
location = location.replace('#', '')
|
||||
|
||||
$.ajax({
|
||||
url: location + 'delete-adjustment/',
|
||||
type: 'POST',
|
||||
data: JSON.stringify({'user_code': user_code}),
|
||||
contentType: 'application/json',
|
||||
success: function(response) {
|
||||
window.location.reload();
|
||||
},
|
||||
error: function(response){
|
||||
error_response(response);
|
||||
},
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
});
|
||||
console.log(response);
|
||||
}
|
||||
})
|
||||
event.preventDefault()
|
||||
})
|
@ -48,6 +48,7 @@
|
||||
<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/sortablejs@latest/Sortable.min.js"></script>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js"
|
||||
integrity="sha384-7+zCNj/IqJ95wo16oMtfsKbZ9ccEh31eOz1HGyDuCQ6wgnyJNSYdrPa03rtR1zdB"
|
||||
|
@ -79,6 +79,9 @@
|
||||
<li>
|
||||
<a href="{{ url_for('admin._questions') }}" id="link-questions" class="dropdown-item">Question Datasets</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('editor._editor') }}" id="link-editor" class="dropdown-item">Question Editor</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown" id="nav-account">
|
||||
|
147
ref-test/app/editor/templates/editor/console.html
Normal file
147
ref-test/app/editor/templates/editor/console.html
Normal file
@ -0,0 +1,147 @@
|
||||
{% extends "editor/components/base.html" %}
|
||||
|
||||
{% block style %}
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="{{ url_for('.static', filename='css/editor.css') }}"
|
||||
/>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<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.
|
||||
</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>
|
||||
</div>
|
||||
<div class="container info-panel">
|
||||
<h3>
|
||||
About the Editor Console
|
||||
</h3>
|
||||
<p>
|
||||
This console will allow you to edit the question data for the RefTest App.
|
||||
All of the questions will be visually displayed as blocks on the screen that you can minimise, expand, and rearrange.
|
||||
</p>
|
||||
<p>
|
||||
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><strong>Do not use language that will assume the flow of questions, such as saying ‘the previous question’, or ‘the next question’, etc. because of randomisation.</strong></p>
|
||||
<p>
|
||||
Each option will be referenced by an <strong>index number</strong>.
|
||||
Make sure to select which index number represents the <strong>correct option</strong>.
|
||||
</p>
|
||||
<p>
|
||||
You will also be able to define <strong>tags</strong> for each question.
|
||||
Separate multiple tags in <strong>new lines</strong>.
|
||||
Make sure to keep the spelling, capitalisation, and punctuation for tags consistent.
|
||||
</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’, use the placeholder <code><block_remaining_questions></code>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="container editor-panel">
|
||||
<h3>
|
||||
Edit Dataset
|
||||
</h3>
|
||||
<div class="container dataset-metadata">
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Dataset Name</span>
|
||||
<input type="text" class="form-control dataset-name" value="{{ dataset.get_name() }}">
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Author</span>
|
||||
<select class="form-select dataset-creator">
|
||||
{% for user in users %}
|
||||
<option value="{{ user.id }}" {{default if dataset.user == user else None }}>{{ user.get_username() }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</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') }}
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">
|
||||
<input type="checkbox" aria-label="Default" class="dataset-default" {% if dataset.default %}checked{% endif %}>
|
||||
</span>
|
||||
<span class="form-control">
|
||||
Make Dataset the Default
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion" id="editor-root" data-target="{{ url_for('api._editor') }}" data-id="{{ dataset.id }}">
|
||||
</div>
|
||||
{% include "editor/components/client-alerts.html" %}
|
||||
<div class="editor-controls container">
|
||||
<a href="javascript:void(0);" class="btn btn-primary" data-action="add-block" title="Add Block" aria-title="Add Block">
|
||||
<i class="bi bi-folder-plus"></i>
|
||||
Add Block
|
||||
</a>
|
||||
<a href="javascript:void(0);" class="btn btn-primary" data-action="add-question" title="Add Question" aria-title="Add Question">
|
||||
<i class="bi bi-file-plus-fill"></i>
|
||||
Add Question
|
||||
</a>
|
||||
</div>
|
||||
<div class="editor-controls container">
|
||||
<a href="javascript:void(0);" class="btn btn-warning" data-action="discard" title="Discard Changes" aria-title="Discard Changes">
|
||||
<i class="bi bi-x-circle-fill"></i>
|
||||
Discard Changes
|
||||
</a>
|
||||
<a href="javascript:void(0);" class="btn btn-danger {% if datasets <=1 or dataset.default or dataset.tests|length > 0 %}disabled{% endif %}" data-action="delete" title="Delete" aria-title="Delete">
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
Delete
|
||||
</a>
|
||||
<a href="javascript:void(0);" class="btn btn-success" data-action="save" title="Save" aria-title="Save">
|
||||
<i class="bi bi-cloud-arrow-up-fill"></i>
|
||||
Save Changes
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="{{ url_for('.static', filename='js/editor.js') }}"
|
||||
></script>
|
||||
{% endblock %}
|
@ -1,140 +1,31 @@
|
||||
{% extends "editor/components/base.html" %}
|
||||
|
||||
{% block style %}
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="{{ url_for('.static', filename='css/editor.css') }}"
|
||||
/>
|
||||
{% endblock %}
|
||||
{% extends "editor/components/input-forms.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Editor</h1>
|
||||
<div class="container editor-panel" id="editor" tabindex="-1">
|
||||
<div class="accordion" id="editor-root">
|
||||
<div class="accordion-item" id="i0">
|
||||
<h2 class="accordion-header" id="h0">
|
||||
<div class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#c0" aria-expanded="true" aria-controls="c0">
|
||||
<div class="float-start">Question 1</div>
|
||||
<a href="javascript:void(0)" class="btn btn-success panel-control" data-id="0" data-action="remove">X</a>
|
||||
<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">Dataset Editor</h2>
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="form-select-input">
|
||||
{{ form.dataset(placeholder="Select Question Dataset") }}
|
||||
{{ form.dataset.label }}
|
||||
</div>
|
||||
</h2>
|
||||
<div id="c0" class="accordion-collapse collapse" aria-labelledby="h0" data-bs-parent="#editor-root">
|
||||
<div class="accordion-body">
|
||||
<div class="question-text">
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Question 1</span>
|
||||
<textarea type="text" class="form-control" id="q0-text" aria-describedby="q0-text-caption">Placeholder for Question 1</textarea>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Question Type</span>
|
||||
<select id="q0-type" class="form-select">
|
||||
<option value ="Multiple Choice">Multiple Choice</option>
|
||||
<option value="Yes/No">Yes/No</option>
|
||||
<option value="List">Ordered List</option>
|
||||
</select>
|
||||
</div>
|
||||
<label for="q0-options" class="form-label">Options</label>
|
||||
<ul class="options" id="q0-options">
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text" id="q0-options-0-caption">0</span>
|
||||
<input type="text" class="form-control" value="Text for Option 1" id="q0-options-0" aria-describedby="q0-options-0-caption">
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text" id="q0-options-1-caption">1</span>
|
||||
<input type="text" class="form-control" value="Text for Option 2" id="q0-options-1" aria-describedby="q-options-1-caption">
|
||||
</div>
|
||||
</ul>
|
||||
<div class="editor-controls">
|
||||
<a href="" class="btn btn-danger" data-action="Cancel">Cancel</a>
|
||||
<a href="" class="btn btn-success" data-action="Done">Done</a>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text" id="q0-correct-caption">Correct</span>
|
||||
<input type="text" class="form-control" value="0" id="q0-correct" aria-describedby="q0-correct-caption">
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text" id="q0-tags-caption">Tags</span>
|
||||
<textarea type="text" class="form-control" value="Foo" id="q0-tags" aria-describedby="q0-tags-caption">List of tags
List of tags</textarea>
|
||||
{% 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-pencil-fill button-icon"></i>
|
||||
Edit
|
||||
</button>
|
||||
<button title="New" class="btn btn-md btn-primary create-new-dataset">
|
||||
<i class="bi bi-cloud-plus-fill button-icon"></i>
|
||||
New
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item" id="i1">
|
||||
<h2 class="accordion-header" id="h1">
|
||||
<div class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#c1" aria-expanded="true" aria-controls="c1">
|
||||
<div class="float-start">Block 1</div>
|
||||
<a href="javascript:void(0)" class="btn btn-success panel-control" data-id="0" data-action="remove">X</a>
|
||||
</div>
|
||||
</h2>
|
||||
<div id="c1" class="accordion-collapse collapse" aria-labelledby="h1" data-bs-parent="#editor-root">
|
||||
<div class="accordion-body">
|
||||
<div class="accordion" id="b1">
|
||||
<div class="accordion-item" id="b1-item">
|
||||
<h2 class="accordion-header" id="h2">
|
||||
<div class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#c2" aria-expanded="true" aria-controls="c2">
|
||||
<div class="float-start">Question 1</div>
|
||||
<a href="javascript:void(0)" class="btn btn-success panel-control" data-id="0" data-action="remove">X</a>
|
||||
</div>
|
||||
</h2>
|
||||
<div id="c2" class="accordion-collapse collapse" aria-labelledby="h2" data-bs-parent="#b1">
|
||||
<div class="accordion-body">
|
||||
<div class="question-text">
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Question 1</span>
|
||||
<textarea type="text" class="form-control" id="q0-text" aria-describedby="q0-text-caption">Placeholder for Question 1</textarea>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Question Type</span>
|
||||
<select id="q0-type" class="form-select">
|
||||
<option value ="Multiple Choice">Multiple Choice</option>
|
||||
<option value="Yes/No">Yes/No</option>
|
||||
<option value="List">Ordered List</option>
|
||||
</select>
|
||||
</div>
|
||||
<label for="q1-options" class="form-label">Options</label>
|
||||
<ul class="options" id="q1-options">
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text" id="q0-options-0-caption">0</span>
|
||||
<input type="text" class="form-control" value="Text for Option 1" id="q0-options-0" aria-describedby="q0-options-0-caption">
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text" id="q0-options-1-caption">1</span>
|
||||
<input type="text" class="form-control" value="Text for Option 2" id="q0-options-1" aria-describedby="q-options-1-caption">
|
||||
</div>
|
||||
</ul>
|
||||
<div class="editor-controls">
|
||||
<a href="" class="btn btn-danger" data-action="Cancel">Cancel</a>
|
||||
<a href="" class="btn btn-success" data-action="Done">Done</a>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text" id="q0-correct-caption">Correct</span>
|
||||
<input type="text" class="form-control" value="0" id="q0-correct" aria-describedby="q0-correct-caption">
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text" id="q0-tags-caption">Tags</span>
|
||||
<textarea type="text" class="form-control" value="Foo" id="q0-tags" aria-describedby="q0-tags-caption">List of tags
List of tags</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "editor/components/client-alerts.html" %}
|
||||
<div class="editor-controls">
|
||||
<a href="" class="btn btn-danger" data-action="Cancel">Cancel</a>
|
||||
<a href="" class="btn btn-success" data-action="Done">Done</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</form>
|
||||
|
||||
{% block script %}
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="{{ url_for('.static', filename='js/editor.js') }}"
|
||||
></script>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,4 +1,10 @@
|
||||
from flask import Blueprint, render_template
|
||||
from ..forms.admin import EditDataset
|
||||
from ..models import Dataset, User
|
||||
from ..tools.forms import get_dataset_choices, send_errors_to_client
|
||||
|
||||
from flask import Blueprint, flash, jsonify, redirect, render_template, request
|
||||
from flask.helpers import url_for
|
||||
from flask_login import login_required
|
||||
|
||||
editor = Blueprint(
|
||||
name='editor',
|
||||
@ -7,6 +13,26 @@ editor = Blueprint(
|
||||
static_folder='static'
|
||||
)
|
||||
|
||||
@editor.route('/')
|
||||
@editor.route('/', methods=['GET','POST'])
|
||||
@login_required
|
||||
def _editor():
|
||||
return render_template('/editor/index.html')
|
||||
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('editor._editor_console', id=id)}),200
|
||||
return send_errors_to_client(form=form)
|
||||
form.process()
|
||||
return render_template('/editor/index.html', form=form)
|
||||
|
||||
@editor.route('/<string:id>/')
|
||||
@login_required
|
||||
def _editor_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('/editor/console.html', dataset=dataset, datasets=datasets, users=users)
|
@ -57,8 +57,12 @@ class CreateTest(FlaskForm):
|
||||
dataset = SelectField('Question Dataset')
|
||||
|
||||
class UploadData(FlaskForm):
|
||||
name = StringField('Name', validators=[InputRequired()])
|
||||
data_file = FileField('Data File', validators=[FileRequired(), FileAllowed(['json'])])
|
||||
default = BooleanField('Make Default', render_kw={'checked': True})
|
||||
|
||||
class AddTimeAdjustment(FlaskForm):
|
||||
time = IntegerField('Extra Time (Minutes)', validators=[InputRequired(), value(max=60)])
|
||||
|
||||
class EditDataset(FlaskForm):
|
||||
dataset = SelectField('Question Dataset')
|
@ -1,4 +1,5 @@
|
||||
from ..extensions import db
|
||||
from ..tools.encryption import decrypt, encrypt
|
||||
from ..tools.logs import write
|
||||
|
||||
from flask import flash
|
||||
@ -7,7 +8,7 @@ from flask_login import current_user
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from datetime import datetime
|
||||
from json import dump, loads
|
||||
from json import dump
|
||||
from os import path, remove
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
@ -15,13 +16,16 @@ from uuid import uuid4
|
||||
class Dataset(db.Model):
|
||||
|
||||
id = db.Column(db.String(36), primary_key=True)
|
||||
name = db.Column(db.String(128), nullable=False)
|
||||
tests = db.relationship('Test', backref='dataset')
|
||||
creator_id = db.Column(db.String(36), db.ForeignKey('user.id'))
|
||||
date = db.Column(db.DateTime, nullable=False)
|
||||
default = db.Column(db.Boolean, default=False, nullable=True)
|
||||
accessed = db.Column(db.DateTime, nullable=True)
|
||||
locked = db.Column(db.Boolean, default=False, nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Dataset {self.id}> was added.'
|
||||
return f'<Dataset {self.id}>.'
|
||||
|
||||
@property
|
||||
def generate_id(self): raise AttributeError('generate_id is not a readable attribute.')
|
||||
@ -29,6 +33,14 @@ class Dataset(db.Model):
|
||||
generate_id.setter
|
||||
def generate_id(self): self.id = uuid4().hex
|
||||
|
||||
@property
|
||||
def set_name(self): raise AttributeError('set_name is not a readable attribute.')
|
||||
|
||||
set_name.setter
|
||||
def set_name(self, name:str): self.name = encrypt(name)
|
||||
|
||||
def get_name(self): return decrypt(self.name)
|
||||
|
||||
def make_default(self):
|
||||
for dataset in Dataset.query.all():
|
||||
dataset.default = False
|
||||
@ -43,7 +55,7 @@ class Dataset(db.Model):
|
||||
message = 'Cannot delete the default dataset.'
|
||||
flash(message, 'error')
|
||||
return False, message
|
||||
if Dataset.query.all().count() == 1:
|
||||
if Dataset.query.count() == 1:
|
||||
message = 'Cannot delete the only dataset.'
|
||||
flash(message, 'error')
|
||||
return False, message
|
||||
@ -56,23 +68,19 @@ class Dataset(db.Model):
|
||||
db.session.commit()
|
||||
return True, 'Dataset deleted.'
|
||||
|
||||
def create(self, upload, default:bool=False):
|
||||
def create(self, data:list, default:bool=False):
|
||||
self.generate_id()
|
||||
timestamp = datetime.now()
|
||||
filename = secure_filename('.'.join([self.id,'json']))
|
||||
data = Path(app.config.get('DATA'))
|
||||
file_path = path.join(data, 'questions', filename)
|
||||
upload.stream.seek(0)
|
||||
questions = loads(upload.read())
|
||||
file_path = self.get_file()
|
||||
with open(file_path, 'w') as file:
|
||||
dump(questions, file, indent=2)
|
||||
dump(data, file, indent=2)
|
||||
self.date = timestamp
|
||||
self.creator = current_user
|
||||
if default: self.make_default()
|
||||
write('system.log', f'New dataset {self.id} added by {current_user.get_username()}.')
|
||||
write('system.log', f'New dataset {self.get_name()} added by {current_user.get_username()}.')
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
return True, 'Dataset uploaded.'
|
||||
return True, 'Dataset created.'
|
||||
|
||||
def check_file(self):
|
||||
filename = secure_filename('.'.join([self.id,'json']))
|
||||
@ -86,3 +94,15 @@ class Dataset(db.Model):
|
||||
data = Path(app.config.get('DATA'))
|
||||
file_path = path.join(data, 'questions', filename)
|
||||
return file_path
|
||||
|
||||
def update(self, data:list=None, default:bool=False):
|
||||
self.date = datetime.now()
|
||||
if default: self.make_default()
|
||||
file_path = self.get_file()
|
||||
with open(file_path, 'w') as file:
|
||||
dump(data, file, indent=2)
|
||||
write('system.log', f'Dataset {self.id} edited by {current_user.get_username()}.')
|
||||
flash(f'Dataset {self.name} successfully edited.', 'success')
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
return True, 'Dataset successfully edited.'
|
@ -1,5 +1,4 @@
|
||||
from ..extensions import db
|
||||
from ..tools.encryption import decrypt, encrypt
|
||||
from ..tools.forms import JsonEncodedDict
|
||||
from ..tools.logs import write
|
||||
|
||||
|
@ -18,12 +18,10 @@ def check_is_json(file):
|
||||
if not '.' in file.filename or not file.filename.rsplit('.',1)[-1] == 'json': return False
|
||||
return True
|
||||
|
||||
def validate_json(file):
|
||||
file.stream.seek(0)
|
||||
data = json.loads(file.read())
|
||||
def validate_json(data):
|
||||
if not isinstance(data, list): return False
|
||||
for block in data:
|
||||
block_type = block.pop('type', None)
|
||||
block_type = block.get('type', None)
|
||||
if block_type not in ['block', 'question']: return False
|
||||
if block_type == 'question':
|
||||
if not all (key in block for key in ['q_no', 'text', 'options', 'correct', 'q_type', 'tags']): return False
|
||||
|
@ -50,7 +50,7 @@ def get_dataset_choices():
|
||||
datasets = Dataset.query.all()
|
||||
dataset_choices = []
|
||||
for dataset in datasets:
|
||||
label = dataset.date.strftime('%Y%m%d%H%M%S')
|
||||
label = dataset.get_name()
|
||||
label = f'{label} (Default)' if dataset.default else label
|
||||
choice = (dataset.id, label)
|
||||
dataset_choices.append(choice)
|
||||
|
@ -22,7 +22,7 @@ def _cookie_consent():
|
||||
value='true',
|
||||
max_age = timedelta(days=14) if request.cookies.get('remember') == 'True' else None,
|
||||
path = '/',
|
||||
expires = datetime.utcnow() + timedelta(days=14) if request.cookies.get('remember') else None,
|
||||
expires = datetime.now() + timedelta(days=14) if request.cookies.get('remember') else None,
|
||||
domain = f'{app.config.get("SERVER_NAME")}',
|
||||
secure = True
|
||||
)
|
||||
|
Reference in New Issue
Block a user