Building new test form

Added CRUD for tests
This commit is contained in:
Vivek Santayana 2021-11-24 17:17:56 +00:00
parent 9f198ed133
commit a862a0f03a
28 changed files with 727 additions and 113 deletions

3
.gitignore vendored
View File

@ -147,3 +147,6 @@ ref-test/testing.py
# Ignore Encryption Keyfile # Ignore Encryption Keyfile
.encryption.key .encryption.key
# Ignore Font Binaries
**/fonts/

View File

@ -1,6 +1,7 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField from wtforms import StringField, PasswordField, BooleanField, DateField, SelectField
from wtforms.validators import InputRequired, Email, Length, EqualTo, Optional from wtforms.validators import InputRequired, Email, Length, EqualTo, Optional, ValidationError
from datetime import date, timedelta
class LoginForm(FlaskForm): class LoginForm(FlaskForm):
username = StringField('Username', validators=[InputRequired(), Length(min=4, max=15)]) username = StringField('Username', validators=[InputRequired(), Length(min=4, max=15)])
@ -42,3 +43,13 @@ class UpdateAccountForm(FlaskForm):
email = StringField('Email Address', validators=[Optional(), Email(message='You must enter a valid email address.'), Length(max=50)]) email = StringField('Email Address', validators=[Optional(), Email(message='You must enter a valid email address.'), Length(max=50)])
password = PasswordField('Change Password', validators=[Optional(),Length(min=6, max=30, message='The password must be between 6 and 20 characters long.')]) password = PasswordField('Change Password', validators=[Optional(),Length(min=6, max=30, message='The password must be between 6 and 20 characters long.')])
password_reenter = PasswordField('Re-Enter New Password', validators=[EqualTo('password', message='Passwords do not match.')]) password_reenter = PasswordField('Re-Enter New Password', validators=[EqualTo('password', message='Passwords do not match.')])
class CreateTest(FlaskForm):
time_options = [
('none', 'None'),
('60', '1 hour'),
('90', '1 hour 30 minutes'),
('120', '2 hours')
]
expiry = DateField('Expiry Date', format="%Y-%m-%d", validators=[InputRequired()], default = date.today() + timedelta(days=1) )
time_limit = SelectField('Time Limit', choices=time_options)

45
ref-test/admin/models.py Normal file
View File

@ -0,0 +1,45 @@
import secrets
from datetime import datetime
from uuid import uuid4
from flask import flash, jsonify
from main import db
from security import encrypt, decrypt
class Test:
def __init__(self, _id=None, expiry=None, time_limit=None, creator=None):
self._id = _id
self.expiry = expiry
self.time_limit = None if time_limit == 'none' or time_limit == '' else time_limit
self.creator = creator
def create(self):
test = {
'_id': self._id,
'date': datetime.today(),
'expiry': self.expiry,
'time_limit': self.time_limit,
'creator': encrypt(self.creator),
'test_code': secrets.token_hex(6).upper()
}
if db.tests.insert_one(test):
flash(f'Created a new exam with Exam Code <strong>{self.render_test_code(test["test_code"])}</strong>.', 'success')
return jsonify({'success': test}), 200
return jsonify({'error': f'Could not create exam. An error occurred.'}), 400
def add_user_code(self, user_code, time_adjustment):
code = {
'_id': uuid4().hex,
'user_code': user_code,
'time_adjustment': time_adjustment
}
if db.tests.find_one_and_update({'_id': self._id}, {'$push': {'time_adjustments': code}},upsert=False):
return jsonify({'success': code})
else:
return jsonify({'error': 'An error occurred.'}), 400
def render_test_code(self, test_code):
return ''.join([test_code[:4], test_code[4:8], test_code[8:]])
def parse_test_code(self, test_code):
return test_code.replace('', '')

View File

@ -27,7 +27,7 @@ body {
margin: auto; margin: auto;
} }
.form-signin-heading { .form-heading {
margin-bottom: 2rem; margin-bottom: 2rem;
} }
@ -65,6 +65,10 @@ body {
border-bottom: 2px solid #585858; 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 { .form-label-group input::-webkit-input-placeholder {
color: transparent; color: transparent;
} }
@ -149,6 +153,50 @@ table.dataTable {
width: 100%; width: 100%;
} }
.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;
}
/* Fallback for Edge /* Fallback for Edge
-------------------------------------------------- */ -------------------------------------------------- */
@supports (-ms-ime-align: auto) { @supports (-ms-ime-align: auto) {

View File

@ -350,6 +350,49 @@ $('form[name=form-update-account]').submit(function(event) {
event.preventDefault(); event.preventDefault();
}); });
$('form[name=form-create-test]').submit(function(event) {
var $form = $(this);
var alert = document.getElementById('alert-box');
var data = $form.serialize();
console.log(data)
alert.innerHTML = ''
$.ajax({
url: window.location.pathname,
type: 'POST',
data: data,
dataType: 'json',
success: function(response) {
window.location.reload();
},
error: function(response) {
console.log(response.responseJSON)
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
alert.innerHTML = alert.innerHTML + `
<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) {
for (var i = 0; i < response.responseJSON.error.length; i ++) {
alert.innerHTML = alert.innerHTML + `
<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>
`;
}
}
}
});
event.preventDefault();
});
// Dismiss Cookie Alert // Dismiss Cookie Alert
$('#dismiss-cookie-alert').click(function(event){ $('#dismiss-cookie-alert').click(function(event){

View File

@ -4,7 +4,7 @@
<div class="form-container"> <div class="form-container">
<form name="form-login" class="form-signin"> <form name="form-login" class="form-signin">
{% include "admin/components/server-alerts.html" %} {% include "admin/components/server-alerts.html" %}
<h2 class="form-signin-heading">Log In</h2> <h2 class="form">Log In</h2>
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
<div class="form-label-group"> <div class="form-label-group">
{{ form.username(class_="form-control", autofocus=true, placeholder="Enter Username") }} {{ form.username(class_="form-control", autofocus=true, placeholder="Enter Username") }}

View File

@ -42,9 +42,6 @@
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous"> crossorigin="anonymous">
</script> </script>
<script>
window.jQuery || document.write('<script src="js/jquery-3.6.0.min.js"><\/script>')
</script>
<script <script
src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js" src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js"
integrity="sha384-7+zCNj/IqJ95wo16oMtfsKbZ9ccEh31eOz1HGyDuCQ6wgnyJNSYdrPa03rtR1zdB" integrity="sha384-7+zCNj/IqJ95wo16oMtfsKbZ9ccEh31eOz1HGyDuCQ6wgnyJNSYdrPa03rtR1zdB"

View File

@ -0,0 +1,27 @@
{% extends "admin/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"/>
{% 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>
{% block custom_data_script %}{% endblock %}
{% endblock %}

View File

@ -4,26 +4,26 @@
{% for category, message in messages %} {% for category, message in messages %}
{% if category == "error" %} {% if category == "error" %}
<div class="alert alert-danger alert-dismissible fade show" role="alert"> <div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i> <i class="bi bi-exclamation-triangle-fill" title="Error" aria-title="Error"></i>
{{ message|safe }} {{ message|safe }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% elif category == "success" %} {% elif category == "success" %}
<div class="alert alert-success alert-dismissible fade show" role="alert"> <div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check2-circle" title="Success"></i> <i class="bi bi-check2-circle" title="Success" aria-title="Success"></i>
{{ message|safe }} {{ message|safe }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% elif category == "warning" %} {% elif category == "warning" %}
<div class="alert alert-warning alert-dismissible fade show" role="alert"> <div class="alert alert-warning alert-dismissible fade show" role="alert">
<i class="bi bi-info-circle-fill" title="Warning"></i> <i class="bi bi-info-circle-fill" aria-title="Warning" title="Warning"></i>
{{ message|safe }} {{ message|safe }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% elif category == "cookie_alert" %} {% elif category == "cookie_alert" %}
{% if not cookie_flash_flag.value %} {% if not cookie_flash_flag.value %}
<div class="alert alert-primary alert-dismissible fade show" id="cookie-alert" role="alert"> <div class="alert alert-primary alert-dismissible fade show" id="cookie-alert" role="alert">
<i class="bi bi-info-circle-fill" title="Alert"></i> <i class="bi bi-info-circle-fill" title="Cookie Alert" aria-title="Cookie Alert"></i>
{{ message|safe }} {{ message|safe }}
<button type="button" id="dismiss-cookie-alert" class="btn btn-success" data-bs-dismiss="alert" aria-label="Close">Accept</button> <button type="button" id="dismiss-cookie-alert" class="btn btn-success" data-bs-dismiss="alert" aria-label="Close">Accept</button>
</div> </div>

View File

@ -1,13 +1,5 @@
{% extends "admin/components/base.html" %} {% extends "admin/components/datatable.html" %}
{% block title %} SKA Referee Test | Manage Users {% endblock %} {% block title %} SKA Referee Test | Manage Users {% endblock %}
{% 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"/>
{% endblock %}
{% block content %} {% block content %}
<h1>Manage Users</h1> <h1>Manage Users</h1>
@ -120,22 +112,7 @@
</form> </form>
</div> </div>
{% endblock %} {% endblock %}
{% block datatable_scripts %} {% block custom_data_script %}
<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> <script>
$(document).ready(function() { $(document).ready(function() {
$('#user-table').DataTable({ $('#user-table').DataTable({

View File

@ -1 +1,138 @@
{% extends "admin/components/base.html" %} {% extends "admin/components/datatable.html" %}
{% block title %} SKA Referee Test | Manage Exams {% endblock %}
{% block content %}
<h1>Manage Exams</h1>
{% if tests %}
<table id="test-table" class="table table-striped" style="width:100%">
<thead>
<tr>
<th data-proority="1">
Date Created
</th>
<th data-priority="2">
Exam Code
</th>
<th data-priority="1">
Expiry Date
</th>
<th data-proority="4">
Time Limit
</th>
<th data-priority="3">
Created By
</th>
<th data-priority="1">
Actions
</th>
</tr>
</thead>
<tbody>
{% for test in tests %}
<tr class="user-table-row">
<td>
{{ test.date.date() }}
</td>
<td>
{{ '—'.join([test.test_code[:4], test.test_code[4:8], test.test_code[8:]]) }}
</td>
<td>
{{ test.expiry }}
</td>
<td>
{% if test.time_limit == None -%}
None
{% elif test.time_limit == '60' -%}
1 hour
{% elif test.time_limit == '90' -%}
1 hour 30 min
{% elif test.time_limit == '120' -%}
2 hours
{% else -%}
{{ test.time_limit }}
{% endif %}
</td>
<td>
{{ test.creator }}
</td>
<td class="test-row-actions">
<a
href="#"
class="btn btn-primary"
title="Edit Exam"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-person-lines-fill" viewBox="0 0 16 16">
<path d="M6 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-5 6s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zM11 3.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5zm.5 2.5a.5.5 0 0 0 0 1h4a.5.5 0 0 0 0-1h-4zm2 3a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1h-2zm0 3a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1h-2z"/>
</svg>
</a>
<a
href="#"
class="btn btn-danger"
title="Delete Exam"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-person-x-fill" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm6.146-2.854a.5.5 0 0 1 .708 0L14 6.293l1.146-1.147a.5.5 0 0 1 .708.708L14.707 7l1.147 1.146a.5.5 0 0 1-.708.708L14 7.707l-1.146 1.147a.5.5 0 0 1-.708-.708L13.293 7l-1.147-1.146a.5.5 0 0 1 0-.708z"></path>
</svg>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% block custom_data_script %}
<script>
$(document).ready(function() {
$('#test-table').DataTable({
'columnDefs': [
{'sortable': false, 'targets': [1,3,5]},
{'searchable': false, 'targets': [3,5]}
],
'order': [[0, 'desc'], [2, 'asc']],
'buttons': [
'copy', 'excel', 'pdf'
],
'responsive': 'true',
'colReorder': 'true',
'fixedHeader': 'true'
});
} );
$('#test-table').show();
$(window).trigger('resize');
</script>
{% endblock %}
{% else %}
<div class="alert alert-primary alert-db-empty">
<i class="bi bi-info-circle-fill" aria-title="Alert" title="Alert"></i>
No exams have been created. Use the form below to create an exam.
</div>
{% endif %}
<div class="form-container">
<form name="form-create-test" class="form-signin">
<h2 class="form-signin-heading">Create Exam</h2>
{{ form.hidden_tag() }}
<div class="form-date-input">
{{ form.expiry(placeholder="Enter Expiry Date", class_ = "datepicker") }}
{{ form.expiry.label }}
</div>
<div class="form-select-input">
{{ form.time_limit(placeholder="Select Time Limit") }}
{{ form.time_limit.label }}
</div>
{% include "admin/components/client-alerts.html" %}
<div class="container form-submission-button">
<div class="row">
<div class="col text-center">
<button title="Create Exam" class="btn btn-md btn-success btn-block" type="submit">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-person-plus-fill" viewBox="0 0 20 20">
<path d="M1 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
<path fill-rule="evenodd" d="M13.5 5a.5.5 0 0 1 .5.5V7h1.5a.5.5 0 0 1 0 1H14v1.5a.5.5 0 0 1-1 0V8h-1.5a.5.5 0 0 1 0-1H13V5.5a.5.5 0 0 1 .5-.5z"/>
</svg>
Create Exam
</button>
</div>
</div>
</div>
</form>
</div>
{% endblock %}

View File

@ -10,25 +10,8 @@ from main import db
from uuid import uuid4 from uuid import uuid4
import secrets import secrets
from main import mail from main import mail
from datetime import datetime, timedelta from datetime import datetime, date, timedelta
from .models import Test
from .forms import CreateUserForm
cookie_consent = Blueprint(
'cookie_consent',
__name__
)
@cookie_consent.route('/')
def _cookies():
resp = redirect('/')
resp.set_cookie(
key = 'cookie_consent',
value = 'True',
max_age = timedelta(days=14) if request.cookies.get('remember') == 'True' else 'Session',
path = '/',
expires = datetime.utcnow() + timedelta(days=14) if request.cookies.get('remember') else 'Session'
)
return resp
views = Blueprint( views = Blueprint(
'admin_views', 'admin_views',
@ -99,6 +82,7 @@ def settings():
@admin_account_required @admin_account_required
@login_required @login_required
def users(): def users():
from .forms import CreateUserForm
form = CreateUserForm() form = CreateUserForm()
if request.method == 'GET': if request.method == 'GET':
users_list = decrypt_find(db.users, {}) users_list = decrypt_find(db.users, {})
@ -259,8 +243,32 @@ def questions():
def upload_questions(): def upload_questions():
return render_template('/admin/settings/upload-questions.html') return render_template('/admin/settings/upload-questions.html')
@views.route('/tests/') @views.route('/tests/', methods=['GET','POST'])
@admin_account_required @admin_account_required
@login_required @login_required
def tests(): def tests():
return render_template('/admin/tests.html') from .forms import CreateTest
form = CreateTest()
form.time_limit.default='none'
form.process()
if request.method == 'GET':
tests = decrypt_find(db.tests, {})
return render_template('/admin/tests.html', tests = tests, form = form)
if request.method == 'POST':
if form.validate_on_submit():
expiry = request.form.get('expiry')
if datetime.strptime(expiry, '%Y-%m-%d').date() < date.today():
return jsonify({'error': 'The expiry date cannot be in the past.'}), 400
creator_id = get_id_from_cookie()
creator = decrypt_find_one(db.users, { '_id': creator_id } )['username']
test = Test(
_id = uuid4().hex,
expiry = expiry,
time_limit = request.form.get('time_limit'),
creator = creator
)
test.create()
return jsonify({'success': 'New exam created.'}), 200
else:
errors = [*form.expiry.errors, *form.time_limit.errors]
return jsonify({ 'error': errors}), 400

View File

@ -0,0 +1,18 @@
from datetime import datetime, timedelta
from flask import Blueprint, redirect, request
cookie_consent = Blueprint(
'cookie_consent',
__name__
)
@cookie_consent.route('/')
def _cookies():
resp = redirect('/')
resp.set_cookie(
key = 'cookie_consent',
value = 'True',
max_age = timedelta(days=14) if request.cookies.get('remember') == 'True' else 'Session',
path = '/',
expires = datetime.utcnow() + timedelta(days=14) if request.cookies.get('remember') else 'Session'
)
return resp

View File

@ -37,7 +37,9 @@ if __name__ == '__main__':
if not check_keyfile_exists(): if not check_keyfile_exists():
generate_keyfile() generate_keyfile()
from admin.views import views as admin_views, cookie_consent from common import cookie_consent
from admin.views import views as admin_views
from admin.auth import auth as admin_auth from admin.auth import auth as admin_auth
from admin.results import results from admin.results import results
from quiz.views import views as quiz_views from quiz.views import views as quiz_views

View File

@ -3,6 +3,4 @@ from flask import Blueprint
auth = Blueprint( auth = Blueprint(
'quiz_auth', 'quiz_auth',
__name__, __name__,
template_folder='templates',
static_folder='static'
) )

View File

@ -1,9 +1,11 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField from wtforms import StringField, PasswordField, BooleanField
from wtforms.validators import InputRequired, Email, Length from wtforms.validators import InputRequired, Email, Length, Optional
class StartQuiz(FlaskForm): class StartQuiz(FlaskForm):
given_name = StringField('Given Name', validators=[InputRequired(), Length(max=15)]) first_name = StringField('First Name(s)', validators=[InputRequired(), Length(max=30)])
surname = StringField('Surname', validators=[InputRequired(), Length(max=15)]) surname = StringField('Surname', validators=[InputRequired(), Length(max=30)])
email = StringField('Email Address', validators=[InputRequired(), Email(message='You must enter a valid email address.'), Length(max=50)]) email = StringField('Email Address', validators=[InputRequired(), Email(message='You must enter a valid email address.'), Length(max=50)])
club = StringField('Affiliated Club', validators=[InputRequired(), Length(max=50)]) club = StringField('Affiliated Club (Optional)', validators=[Optional(), Length(max=50)])
auth_code = StringField('Exam Code', validators=[InputRequired(), Length(min=14, max=14)])
user_code = StringField('User Code (Optional)', validators=[Optional(), Length(min=6, max=6)])

View File

@ -0,0 +1,190 @@
.bg-light {
background-color: #EBE3E1!important;
}
body {
padding: 80px 0;
line-height: 1.5;
font-size: 14pt;
}
.site-footer {
background-color: lightgray;
font-size: small;
}
.site-footer p {
margin: 0;
}
.quiz-container {
max-width: 720px;
}
.form-container {
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
}
.form-quiz-start {
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;
}
/*! Generated by Font Squirrel (https://www.fontsquirrel.com) on November 23, 2021 */
@font-face {
font-family: 'opendyslexic3bold';
src: url('../fonts/opendyslexic3-bold-webfont.woff2') format('woff2'),
url('../fonts/opendyslexic3-bold-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'opendyslexic3regular';
src: url('../fonts/opendyslexic3-regular-webfont.woff2') format('woff2'),
url('../fonts/opendyslexic3-regular-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'opendyslexicmonoregular';
src: url('../fonts/opendyslexicmono-regular-webfont.woff2') format('woff2'),
url('../fonts/opendyslexicmono-regular-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
/* Fallback for Edge
-------------------------------------------------- */
@supports (-ms-ime-align: auto) {
.form-label-group label {
display: none;
}
.form-label-group input::-ms-input-placeholder {
color: #777;
}
}
/* Fallback for IE
-------------------------------------------------- */
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.form-label-group label {
display: none;
}
.form-label-group input:-ms-input-placeholder {
color: #777;
}
}

View File

@ -0,0 +1,13 @@
$(document).ready(function() {
$("#od-font-test").click(function(){
$("body").css("font-family", "opendyslexic3regular")
});
$('.auth-code-input').keyup(function() {
var input = $(this).val().split("-").join("").split("—").join(""); // remove hyphens and mdashes
if (input.length > 0) {
input = input.match(new RegExp('.{1,4}', 'g')).join("—");
}
$(this).val(input);
});
});

View File

@ -1,43 +0,0 @@
<!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.0.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
crossorigin="anonymous">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.15.4/css/fontawesome.min.css"
crossorigin="anonymous"
/>
<title>{% block title %} Site Title {% endblock %}</title>
</head>
<body>
<nav class="navbar fixed-top navbar-expand-md navbar-dark bg-dark">
<div class="container">
<a href="/" class="navbar-brand mb-0 h1">SKA Refereeing Test </a>
</div>
</nav>
<!-- JQuery, Popper, and Bootstrap js dependencies -->
<script
src="https://code.jquery.com/jquery-3.6.0.slim.min.js"
integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI="
crossorigin="anonymous">
</script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
crossorigin="anonymous">
</script>
<!-- Custom js -->
<script
type="text/javascript"
src="{{ url_for('.static', filename='js/script.js') }}"
></script>
</body>
</html>

View File

@ -0,0 +1,59 @@
<!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.0.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
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') }}"
/>
<title>{% block title %} SKA Referee Test {% endblock %}</title>
</head>
<body class="bg-light">
{% block navbar %}
{% include "quiz/components/navbar.html" %}
{% endblock %}
<div class="container quiz-container">
{% block top_alerts %}
{% include "quiz/components/server-alerts.html" %}
{% endblock %}
{% block content %}{% endblock %}
</div>
<footer class="container site-footer">
{% include "quiz/components/footer.html" %}
</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
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"
src="{{ url_for('.static', filename='js/script.js') }}"
></script>
</body>
</html>

View File

@ -0,0 +1,5 @@
<nav class="navbar fixed-top navbar-expand-md navbar-dark bg-dark">
<div class="container">
<a href="/" class="navbar-brand mb-0 h1">SKA Refereeing Test </a>
</div>
</nav>

View File

@ -0,0 +1,24 @@
{% extends "quiz/components/base.html" %}
{% block content %}
<h1>SKA Refereeing Theory Exam</h1>
<p>
This app will allow you to take the Exam on-line. This app should also allow you to adjust the way the quiz is rendered to suit your access needs. This could include using a screen reader, changing the display font size or typeface, or navigating questions and answers via the keyboard.
</p>
<p>
Instructions
</p>
<p>
Other Info
</p>
<p>
When you are ready to begin the quiz, click the following button.
</p>
<a href="{{ url_for('quiz_views.start') }}" class="btn btn-success">Take the Quiz</a>
{% endblock %}

View File

@ -0,0 +1,43 @@
{% extends "quiz/components/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block content %}
<div class="form-container">
<form name="form-quiz-start" class="form-quiz-start">
<h2 class="form-heading">Take the Exam</h2>
{{ form.hidden_tag() }}
<div class="form-label-group">
{{ form.first_name(class_="form-control", autofocus=true, placeholder="Enter First Name(s)") }}
{{ form.first_name.label }}
</div>
<div class="form-label-group">
{{ form.surname(class_="form-control", placeholder="Enter Surname") }}
{{ form.surname.label }}
</div>
<div class="form-label-group">
{{ form.email(class_="form-control", placeholder="Enter Email Address") }}
{{ form.email.label }}
</div>
<div class="form-label-group">
{{ form.club(class_="form-control", placeholder="Enter Affiliated Club") }}
{{ form.club.label }}
</div>
<div class="form-label-group">
{{ form.auth_code(class_="form-control auth-code-input", placeholder="Enter Exam Code") }}
{{ form.auth_code.label }}
</div>
<div class="form-label-group">
{{ form.user_code(class_="form-control", placeholder="Enter User Code") }}
{{ form.user_code.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">Start Quiz</button>
</div>
</div>
</div>
</form>
</div>
{% endblock %}

View File

@ -1,8 +1,9 @@
from flask import Blueprint from flask import Blueprint, render_template
views = Blueprint( views = Blueprint(
'quiz_views', 'quiz_views',
__name__, __name__,
static_url_path='',
template_folder='templates', template_folder='templates',
static_folder='static' static_folder='static'
) )
@ -10,7 +11,13 @@ views = Blueprint(
@views.route('/') @views.route('/')
@views.route('/home/') @views.route('/home/')
def home(): def home():
return f'<h1>Ref Test Home Page</h1>' return render_template('/quiz/index.html')
@views.route('/start/', methods = ['GET', 'POST'])
def start():
from .forms import StartQuiz
form = StartQuiz()
return render_template('/quiz/start-quiz.html', form=form)
@views.route('/privacy/') @views.route('/privacy/')
def privacy(): def privacy():

View File

@ -1,6 +1,6 @@
from pymongo import collection from pymongo import collection
from . import encrypt, decrypt from . import encrypt, decrypt
encrypted_parameters = ['username', 'email', 'name', 'club'] encrypted_parameters = ['username', 'email', 'name', 'club', 'creator']
def decrypt_find(collection:collection, query:dict): def decrypt_find(collection:collection, query:dict):
cursor = collection.find({}) cursor = collection.find({})