Building new test form
Added CRUD for tests
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -147,3 +147,6 @@ ref-test/testing.py
 | 
			
		||||
 | 
			
		||||
# Ignore Encryption Keyfile
 | 
			
		||||
.encryption.key
 | 
			
		||||
 | 
			
		||||
# Ignore Font Binaries
 | 
			
		||||
**/fonts/
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
from flask_wtf import FlaskForm
 | 
			
		||||
from wtforms import StringField, PasswordField, BooleanField
 | 
			
		||||
from wtforms.validators import InputRequired, Email, Length, EqualTo, Optional
 | 
			
		||||
from wtforms import StringField, PasswordField, BooleanField, DateField, SelectField
 | 
			
		||||
from wtforms.validators import InputRequired, Email, Length, EqualTo, Optional, ValidationError
 | 
			
		||||
from datetime import date, timedelta
 | 
			
		||||
 | 
			
		||||
class LoginForm(FlaskForm):
 | 
			
		||||
    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)])
 | 
			
		||||
    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.')])
 | 
			
		||||
 | 
			
		||||
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
									
								
							
							
						
						
									
										45
									
								
								ref-test/admin/models.py
									
									
									
									
									
										Normal 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('—', '')
 | 
			
		||||
@@ -27,7 +27,7 @@ body {
 | 
			
		||||
    margin: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.form-signin-heading {
 | 
			
		||||
.form-heading {
 | 
			
		||||
    margin-bottom: 2rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -65,6 +65,10 @@ body {
 | 
			
		||||
    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;
 | 
			
		||||
}
 | 
			
		||||
@@ -149,6 +153,50 @@ table.dataTable {
 | 
			
		||||
    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
 | 
			
		||||
-------------------------------------------------- */
 | 
			
		||||
@supports (-ms-ime-align: auto) {
 | 
			
		||||
 
 | 
			
		||||
@@ -350,6 +350,49 @@ $('form[name=form-update-account]').submit(function(event) {
 | 
			
		||||
    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').click(function(event){
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
    <div class="form-container">
 | 
			
		||||
        <form name="form-login" class="form-signin">
 | 
			
		||||
            {% include "admin/components/server-alerts.html" %}
 | 
			
		||||
            <h2 class="form-signin-heading">Log In</h2>
 | 
			
		||||
            <h2 class="form">Log In</h2>
 | 
			
		||||
            {{ form.hidden_tag() }}
 | 
			
		||||
            <div class="form-label-group">
 | 
			
		||||
                {{ form.username(class_="form-control", autofocus=true, placeholder="Enter Username") }}
 | 
			
		||||
 
 | 
			
		||||
@@ -42,9 +42,6 @@
 | 
			
		||||
            integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
 | 
			
		||||
            crossorigin="anonymous">
 | 
			
		||||
        </script>
 | 
			
		||||
        <script>
 | 
			
		||||
            window.jQuery || document.write('<script src="js/jquery-3.6.0.min.js"><\/script>')
 | 
			
		||||
        </script>
 | 
			
		||||
        <script
 | 
			
		||||
            src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js"
 | 
			
		||||
            integrity="sha384-7+zCNj/IqJ95wo16oMtfsKbZ9ccEh31eOz1HGyDuCQ6wgnyJNSYdrPa03rtR1zdB"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								ref-test/admin/templates/admin/components/datatable.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								ref-test/admin/templates/admin/components/datatable.html
									
									
									
									
									
										Normal 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 %}
 | 
			
		||||
@@ -4,26 +4,26 @@
 | 
			
		||||
        {% for category, message in messages %}
 | 
			
		||||
            {% if category == "error" %}
 | 
			
		||||
                <div class="alert alert-danger alert-dismissible fade show" role="alert">
 | 
			
		||||
                    <i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
 | 
			
		||||
                    <i class="bi bi-exclamation-triangle-fill" title="Error" aria-title="Error"></i>
 | 
			
		||||
                    {{ message|safe }}
 | 
			
		||||
                    <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
 | 
			
		||||
                </div>
 | 
			
		||||
            {% elif category == "success" %}
 | 
			
		||||
                <div class="alert alert-success alert-dismissible fade show" role="alert">
 | 
			
		||||
                    <i class="bi bi-check2-circle" title="Success"></i>
 | 
			
		||||
                    <i class="bi bi-check2-circle" title="Success" aria-title="Success"></i>
 | 
			
		||||
                    {{ message|safe }}
 | 
			
		||||
                    <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
 | 
			
		||||
                </div>
 | 
			
		||||
            {% elif category == "warning" %}
 | 
			
		||||
                <div class="alert alert-warning alert-dismissible fade show" role="alert">
 | 
			
		||||
                    <i class="bi bi-info-circle-fill" title="Warning"></i>
 | 
			
		||||
                    <i class="bi bi-info-circle-fill" aria-title="Warning" title="Warning"></i>
 | 
			
		||||
                    {{ message|safe }}
 | 
			
		||||
                    <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
 | 
			
		||||
                </div>
 | 
			
		||||
            {% elif category == "cookie_alert" %}
 | 
			
		||||
                {% if not cookie_flash_flag.value %}
 | 
			
		||||
                    <div class="alert alert-primary alert-dismissible fade show" id="cookie-alert" role="alert">
 | 
			
		||||
                        <i class="bi bi-info-circle-fill" title="Alert"></i>
 | 
			
		||||
                        <i class="bi bi-info-circle-fill" title="Cookie Alert" aria-title="Cookie Alert"></i>
 | 
			
		||||
                        {{ message|safe }}
 | 
			
		||||
                        <button type="button" id="dismiss-cookie-alert" class="btn btn-success" data-bs-dismiss="alert" aria-label="Close">Accept</button>
 | 
			
		||||
                    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,5 @@
 | 
			
		||||
{% extends "admin/components/base.html" %}
 | 
			
		||||
{% extends "admin/components/datatable.html" %}
 | 
			
		||||
{% 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 %}
 | 
			
		||||
    <h1>Manage Users</h1>
 | 
			
		||||
 | 
			
		||||
@@ -120,22 +112,7 @@
 | 
			
		||||
        </form>
 | 
			
		||||
    </div>
 | 
			
		||||
{% 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 %}
 | 
			
		||||
    <script>
 | 
			
		||||
        $(document).ready(function() {
 | 
			
		||||
            $('#user-table').DataTable({
 | 
			
		||||
 
 | 
			
		||||
@@ -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 %}
 | 
			
		||||
@@ -10,25 +10,8 @@ from main import db
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
import secrets
 | 
			
		||||
from main import mail
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
from datetime import datetime, date, timedelta
 | 
			
		||||
from .models import Test
 | 
			
		||||
 | 
			
		||||
views = Blueprint(
 | 
			
		||||
    'admin_views',
 | 
			
		||||
@@ -99,6 +82,7 @@ def settings():
 | 
			
		||||
@admin_account_required
 | 
			
		||||
@login_required
 | 
			
		||||
def users():
 | 
			
		||||
    from .forms import CreateUserForm
 | 
			
		||||
    form = CreateUserForm()
 | 
			
		||||
    if request.method == 'GET':
 | 
			
		||||
        users_list = decrypt_find(db.users, {})
 | 
			
		||||
@@ -259,8 +243,32 @@ def questions():
 | 
			
		||||
def upload_questions():
 | 
			
		||||
    return render_template('/admin/settings/upload-questions.html')
 | 
			
		||||
 | 
			
		||||
@views.route('/tests/')
 | 
			
		||||
@views.route('/tests/', methods=['GET','POST'])
 | 
			
		||||
@admin_account_required
 | 
			
		||||
@login_required
 | 
			
		||||
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
 | 
			
		||||
							
								
								
									
										18
									
								
								ref-test/common/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								ref-test/common/__init__.py
									
									
									
									
									
										Normal 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
 | 
			
		||||
@@ -37,7 +37,9 @@ if __name__ == '__main__':
 | 
			
		||||
    if not check_keyfile_exists():
 | 
			
		||||
        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.results import results
 | 
			
		||||
    from quiz.views import views as quiz_views
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,4 @@ from flask import Blueprint
 | 
			
		||||
auth = Blueprint(
 | 
			
		||||
    'quiz_auth',
 | 
			
		||||
    __name__,
 | 
			
		||||
    template_folder='templates',
 | 
			
		||||
    static_folder='static'
 | 
			
		||||
)
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
from flask_wtf import FlaskForm
 | 
			
		||||
from wtforms import StringField, PasswordField, BooleanField
 | 
			
		||||
from wtforms.validators import InputRequired, Email, Length
 | 
			
		||||
from wtforms.validators import InputRequired, Email, Length, Optional
 | 
			
		||||
 | 
			
		||||
class StartQuiz(FlaskForm):
 | 
			
		||||
    given_name = StringField('Given Name', validators=[InputRequired(), Length(max=15)])
 | 
			
		||||
    surname = StringField('Surname', validators=[InputRequired(), Length(max=15)])
 | 
			
		||||
    first_name = StringField('First Name(s)', validators=[InputRequired(), Length(max=30)])
 | 
			
		||||
    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)])
 | 
			
		||||
    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)])
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
  
 | 
			
		||||
@@ -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);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -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>
 | 
			
		||||
							
								
								
									
										59
									
								
								ref-test/quiz/templates/quiz/components/base.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								ref-test/quiz/templates/quiz/components/base.html
									
									
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										0
									
								
								ref-test/quiz/templates/quiz/components/footer.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								ref-test/quiz/templates/quiz/components/footer.html
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										5
									
								
								ref-test/quiz/templates/quiz/components/navbar.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								ref-test/quiz/templates/quiz/components/navbar.html
									
									
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										24
									
								
								ref-test/quiz/templates/quiz/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								ref-test/quiz/templates/quiz/index.html
									
									
									
									
									
										Normal 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 %}
 | 
			
		||||
							
								
								
									
										0
									
								
								ref-test/quiz/templates/quiz/privacy.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								ref-test/quiz/templates/quiz/privacy.html
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										43
									
								
								ref-test/quiz/templates/quiz/start-quiz.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								ref-test/quiz/templates/quiz/start-quiz.html
									
									
									
									
									
										Normal 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 %}
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
from flask import Blueprint
 | 
			
		||||
from flask import Blueprint, render_template
 | 
			
		||||
 | 
			
		||||
views = Blueprint(
 | 
			
		||||
    'quiz_views',
 | 
			
		||||
    __name__,
 | 
			
		||||
    static_url_path='',
 | 
			
		||||
    template_folder='templates',
 | 
			
		||||
    static_folder='static'
 | 
			
		||||
)
 | 
			
		||||
@@ -10,7 +11,13 @@ views = Blueprint(
 | 
			
		||||
@views.route('/')
 | 
			
		||||
@views.route('/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/')
 | 
			
		||||
def privacy():
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
from pymongo import collection
 | 
			
		||||
from . import encrypt, decrypt
 | 
			
		||||
encrypted_parameters = ['username', 'email', 'name', 'club']
 | 
			
		||||
encrypted_parameters = ['username', 'email', 'name', 'club', 'creator']
 | 
			
		||||
 | 
			
		||||
def decrypt_find(collection:collection, query:dict):
 | 
			
		||||
    cursor = collection.find({})
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user