Building new test form

Added CRUD for tests
This commit is contained in:
2021-11-24 17:17:56 +00:00
parent f2943e4bc1
commit 610b6a5766
28 changed files with 727 additions and 113 deletions

View File

@ -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)])
@ -41,4 +42,14 @@ class UpdateAccountForm(FlaskForm):
password_confirm = PasswordField('Current Password', validators=[InputRequired(), Length(min=6, max=30, message='The password must be between 6 and 20 characters long.')])
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.')])
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;
}
.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) {

View File

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

View File

@ -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") }}

View File

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

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 %}
{% 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>

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 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({

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