Finished admin console

This commit is contained in:
Vivek Santayana 2022-06-15 23:54:44 +01:00
parent 62160beab2
commit 2ea778143e
33 changed files with 198 additions and 206 deletions

View File

@ -70,14 +70,14 @@ $('form[name=form-upload-questions]').submit(function(event) {
// Edit and Delete Test Button Handlers // Edit and Delete Test Button Handlers
$('.test-action').click(function(event) { $('.test-action').click(function(event) {
let _id = $(this).data('_id'); let id = $(this).data('id');
let action = $(this).data('action'); let action = $(this).data('action');
if (action == 'delete') { if (action == 'delete' || action == 'start' || action == 'end') {
$.ajax({ $.ajax({
url: `/admin/tests/delete/`, url: `/admin/tests/edit/`,
type: 'POST', type: 'POST',
data: JSON.stringify({'_id': _id}), data: JSON.stringify({'id': id, 'action': action}), // TODO Change how CRUD operations work
contentType: 'application/json', contentType: 'application/json',
success: function(response) { success: function(response) {
window.location.href = '/admin/tests/'; window.location.href = '/admin/tests/';
@ -87,21 +87,7 @@ $('.test-action').click(function(event) {
}, },
}); });
} else if (action == 'edit') { } else if (action == 'edit') {
window.location.href = `/admin/test/${_id}/` window.location.href = `/admin/test/${id}/`
} else if (action == 'close'){
$.ajax({
url: `/admin/tests/close/`,
type: 'POST',
data: JSON.stringify({'_id': _id}),
contentType: 'application/json',
success: function(response) {
$(window).scrollTop(0);
window.location.reload();
},
error: function(response){
error_response(response);
},
});
} }
event.preventDefault(); event.preventDefault();
@ -185,13 +171,13 @@ $('#dismiss-cookie-alert').click(function(event){
// Script for Result Actions // Script for Result Actions
$('.result-action-buttons').click(function(event){ $('.result-action-buttons').click(function(event){
var _id = $(this).data('_id'); var id = $(this).data('id');
if ($(this).data('result-action') == 'generate') { if ($(this).data('result-action') == 'generate') {
$.ajax({ $.ajax({
url: '/admin/certificate/', url: '/admin/certificate/',
type: 'POST', type: 'POST',
data: JSON.stringify({'_id': _id}), data: JSON.stringify({'id': id}),
contentType: 'application/json', contentType: 'application/json',
dataType: 'html', dataType: 'html',
success: function(response) { success: function(response) {
@ -207,7 +193,7 @@ $('.result-action-buttons').click(function(event){
$.ajax({ $.ajax({
url: window.location.href, url: window.location.href,
type: 'POST', type: 'POST',
data: JSON.stringify({'_id': _id, 'action': action}), data: JSON.stringify({'id': id, 'action': action}),
contentType: 'application/json', contentType: 'application/json',
success: function(response) { success: function(response) {
if (action == 'delete') { if (action == 'delete') {

View File

@ -2,7 +2,7 @@
{% block content %} {% block content %}
<div class="form-container"> <div class="form-container">
<form name="form-update-account" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin_views.home') }}"> <form name="form-update-account" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin._home') }}">
{% include "admin/components/server-alerts.html" %} {% include "admin/components/server-alerts.html" %}
<h2 class="form-heading">Update Your Account</h2> <h2 class="form-heading">Update Your Account</h2>
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
@ -32,7 +32,7 @@
<div class="container form-submission-button"> <div class="container form-submission-button">
<div class="row"> <div class="row">
<div class="col text-center"> <div class="col text-center">
<a href="{{ url_for('admin_views.users') }}" class="btn btn-md btn-danger btn-block" type="button"> <a href="{{ url_for('admin._users') }}" class="btn btn-md btn-danger btn-block" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/> <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
</svg> </svg>

View File

@ -2,7 +2,7 @@
{% block content %} {% block content %}
<div class="form-container"> <div class="form-container">
<form name="form-login" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin_views.home') }}"> <form name="form-login" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin._home') }}">
{% include "admin/components/server-alerts.html" %} {% include "admin/components/server-alerts.html" %}
<h2 class="form">Log In</h2> <h2 class="form">Log In</h2>
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
@ -26,7 +26,7 @@
</div> </div>
</div> </div>
</div> </div>
<a href="{{ url_for('admin_auth.reset') }}" class="signin-forgot-password">Reset Password</a> <a href="{{ url_for('admin._reset') }}" class="signin-forgot-password">Reset Password</a>
</form> </form>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -3,14 +3,14 @@
{% block navbar %} {% block navbar %}
<nav class="navbar fixed-top navbar-expand-md navbar-dark bg-dark"> <nav class="navbar fixed-top navbar-expand-md navbar-dark bg-dark">
<div class="container"> <div class="container">
<a href="{{ url_for('admin_views.home') }}" class="navbar-brand mb-0 h1">RefTest | Admin</a> <a href="{{ url_for('admin._home') }}" class="navbar-brand mb-0 h1">RefTest | Admin</a>
</div> </div>
</nav> </nav>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="form-container"> <div class="form-container">
<form name="form-register" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin_auth.login') }}"> <form name="form-register" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin._login') }}">
{% include "admin/components/server-alerts.html" %} {% include "admin/components/server-alerts.html" %}
<h2 class="form-heading">Register an Account</h2> <h2 class="form-heading">Register an Account</h2>
{{ form.hidden_tag() }} {{ form.hidden_tag() }}

View File

@ -2,7 +2,7 @@
{% block content %} {% block content %}
<div class="form-container"> <div class="form-container">
<form name="form-reset" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin_auth.login') }}"> <form name="form-reset" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin._login') }}">
{% include "admin/components/server-alerts.html" %} {% include "admin/components/server-alerts.html" %}
<h2 class="form-heading">Reset Password</h2> <h2 class="form-heading">Reset Password</h2>
{{ form.hidden_tag() }} {{ form.hidden_tag() }}

View File

@ -2,7 +2,7 @@
{% block content %} {% block content %}
<div class="form-container"> <div class="form-container">
<form name="form-update-password" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin_auth.login') }}"> <form name="form-update-password" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin._login') }}">
{% include "admin/components/server-alerts.html" %} {% include "admin/components/server-alerts.html" %}
<h2 class="form-heading">Update Password</h2> <h2 class="form-heading">Update Password</h2>
{{ form.hidden_tag() }} {{ form.hidden_tag() }}

View File

@ -1,6 +1,6 @@
<nav class="navbar fixed-top navbar-expand-md navbar-dark bg-dark"> <nav class="navbar fixed-top navbar-expand-md navbar-dark bg-dark">
<div class="container"> <div class="container">
<a href="{{ url_for('admin_views.home') }}" class="navbar-brand mb-0 h1">RefTest (Beta) | Admin</a> <a href="{{ url_for('admin._home') }}" class="navbar-brand mb-0 h1">RefTest (Beta) | Admin</a>
<button <button
class="navbar-toggler" class="navbar-toggler"
type="button" type="button"
@ -14,24 +14,24 @@
</button> </button>
<div class="collapse navbar-collapse justify-content-end" id="navbar"> <div class="collapse navbar-collapse justify-content-end" id="navbar">
<ul class="navbar-nav"> <ul class="navbar-nav">
{% if not check_login() %} {% if not current_user.is_authenticated %}
<li class="nav-item" id="nav-login"> <li class="nav-item" id="nav-login">
<a href="{{ url_for('admin_auth.login') }}" id="link-login" class="nav-link">Log In</a> <a href="{{ url_for('admin._login') }}" id="link-login" class="nav-link">Log In</a>
</li> </li>
{% endif %} {% endif %}
{% if check_login() %} {% if current_user.is_authenticated %}
<li class="nav-item" id="nav-results"> <li class="nav-item" id="nav-results">
<a href="{{ url_for('results._results') }}" id="link-results" class="nav-link">View Results</a> <a href="{{ url_for('admin._view_entries') }}" id="link-results" class="nav-link">View Results</a>
</li> </li>
<li class="nav-item" id="nav-tests"> <li class="nav-item" id="nav-tests">
<a href="{{ url_for('admin_views.tests') }}" id="link-tests" class="nav-link">Manage Exams</a> <a href="{{ url_for('admin._tests') }}" id="link-tests" class="nav-link">Manage Exams</a>
</li> </li>
<li class="nav-item dropdown" id="nav-settings"> <li class="nav-item dropdown" id="nav-settings">
<a <a
class="nav-link dropdown-toggle" class="nav-link dropdown-toggle"
id="dropdown-account" id="dropdown-account"
role="button" role="button"
href="{{ url_for('admin_views.settings') }}" href="{{ url_for('admin._settings') }}"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-expanded="false" aria-expanded="false"
> >
@ -39,13 +39,16 @@
</a> </a>
<ul <ul
class="dropdown-menu" class="dropdown-menu"
aria-labelledby="dropdown-account" aria-labelledby="dropdown-settings"
> >
<li> <li>
<a href="{{ url_for('admin_views.users') }}" id="link-users" class="dropdown-item">Users</a> <a href="{{ url_for('admin._settings') }}" id="link-settings" class="dropdown-item">Settings</a>
</li> </li>
<li> <li>
<a href="{{ url_for('admin_views.questions') }}" id="link-questions" class="dropdown-item">Question Datasets</a> <a href="{{ url_for('admin._users') }}" id="link-users" class="dropdown-item">Users</a>
</li>
<li>
<a href="{{ url_for('admin._questions') }}" id="link-questions" class="dropdown-item">Question Datasets</a>
</li> </li>
</ul> </ul>
</li> </li>
@ -54,7 +57,7 @@
class="nav-link dropdown-toggle" class="nav-link dropdown-toggle"
id="dropdown-account" id="dropdown-account"
role="button" role="button"
href="{{ url_for('admin_auth.account') }}" href="{{ url_for('admin._update_user', id=current_user.id) }}"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-expanded="false" aria-expanded="false"
> >
@ -65,10 +68,10 @@
aria-labelledby="dropdown-account" aria-labelledby="dropdown-account"
> >
<li> <li>
<a href="{{ url_for('admin_auth.account') }}" id="link-account" class="dropdown-item">Account Settings</a> <a href="{{ url_for('admin._update_user', id=current_user.id) }}" id="link-account" class="dropdown-item">Account Settings</a>
</li> </li>
<li> <li>
<a href="{{ url_for('admin_auth.logout') }}" id="link-logout" class="dropdown-item">Log Out</a> <a href="{{ url_for('admin._logout') }}" id="link-logout" class="dropdown-item">Log Out</a>
</li> </li>
</ul> </ul>
</li> </li>

View File

@ -3,19 +3,19 @@
<div class="expand navbar-expand justify-content-center" id="navbar_secondary"> <div class="expand navbar-expand justify-content-center" id="navbar_secondary">
<ul class="nav"> <ul class="nav">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('admin_views.tests', filter='active') }}">Active</a> <a class="nav-link" href="{{ url_for('admin._tests', filter='active') }}">Active</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('admin_views.tests', filter='scheduled') }}">Scheduled</a> <a class="nav-link" href="{{ url_for('admin._tests', filter='scheduled') }}">Scheduled</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('admin_views.tests', filter='expired') }}">Expired</a> <a class="nav-link" href="{{ url_for('admin._tests', filter='expired') }}">Expired</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('admin_views.tests', filter='all') }}">All</a> <a class="nav-link" href="{{ url_for('admin._tests', filter='all') }}">All</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('admin_views.tests', filter='create') }}">Create</a> <a class="nav-link" href="{{ url_for('admin._tests', filter='create') }}">Create</a>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -25,22 +25,22 @@
{% for test in current_tests %} {% for test in current_tests %}
<tr> <tr>
<td> <td>
<a href="{{ url_for('admin_views.view_test', _id=test._id) }}">{{ '—'.join([test.test_code[:4], test.test_code[4:8], test.test_code[8:]]) }}</a> <a href="{{ url_for('admin._view_test', id=test.id) }}">{{ test.get_code() }}</a>
</td> </td>
<td> <td>
{{ test.expiry_date.strftime('%d %b %Y') }} {{ test.end_date.strftime('%d %b %Y') }}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<a href="{{ url_for('admin_views.tests', filter='active') }}" class="btn btn-primary">View Exams</a> <a href="{{ url_for('admin._tests', filter='active') }}" class="btn btn-primary">View Exams</a>
{% else %} {% else %}
<div class="alert alert-primary"> <div class="alert alert-primary">
There are currently no active exams. There are currently no active exams.
</div> </div>
<a href="{{ url_for('admin_views.tests', filter='create') }}" class="btn btn-primary">Create Exam</a> <a href="{{ url_for('admin._tests', filter='create') }}" class="btn btn-primary">Create Exam</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -69,7 +69,7 @@
{% for result in recent_results %} {% for result in recent_results %}
<tr> <tr>
<td> <td>
<a href="{{ url_for('admin_views.view_entry', _id=result._id) }}">{{ result.name.surname }}, {{ result.name.first_name }}</a> <a href="{{ url_for('admin._view_entry', id=result.id) }}">{{ result.name.surname }}, {{ result.name.first_name }}</a>
</td> </td>
<td> <td>
{{ result.submission_time.strftime('%d %b %Y %H:%M') }} {{ result.submission_time.strftime('%d %b %Y %H:%M') }}
@ -82,7 +82,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<a href="{{ url_for('admin_views.view_entries') }}" class="btn btn-primary">View Results</a> <a href="{{ url_for('admin._view_entries') }}" class="btn btn-primary">View Results</a>
{% else %} {% else %}
<div class="alert alert-primary"> <div class="alert alert-primary">
There are currently no exam results to preview. There are currently no exam results to preview.
@ -114,22 +114,22 @@
{% for test in upcoming_tests %} {% for test in upcoming_tests %}
<tr> <tr>
<td> <td>
<a href="{{ url_for('admin_views.view_test', _id=test._id) }}">{{ '—'.join([test.test_code[:4], test.test_code[4:8], test.test_code[8:]]) }}</a> <a href="{{ url_for('admin._view_test', id=test.id) }}">{{ test.get_code() }}</a>
</td> </td>
<td> <td>
{{ test.expiry_date.strftime('%d %b %Y') }} {{ test.end_date.strftime('%d %b %Y') }}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<a href="{{ url_for('admin_views.tests', filter='scheduled') }}" class="btn btn-primary">View Exams</a> <a href="{{ url_for('admin._tests', filter='scheduled') }}" class="btn btn-primary">View Exams</a>
{% else %} {% else %}
<div class="alert alert-primary"> <div class="alert alert-primary">
There are currently no upcoming exams. There are currently no upcoming exams.
</div> </div>
<a href="{{ url_for('admin_views.tests', filter='create') }}" class="btn btn-primary">Create Exam</a> <a href="{{ url_for('admin._tests', filter='create') }}" class="btn btn-primary">Create Exam</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -164,19 +164,19 @@
{% endif %} {% endif %}
<div class="container justify-content-center"> <div class="container justify-content-center">
<div class="row"> <div class="row">
<a href="#" class="btn btn-primary result-action-buttons" data-result-action="generate" data-_id="{{ entry._id }}"> <a href="#" class="btn btn-primary result-action-buttons" data-result-action="generate" data-id="{{ entry.id }}">
<i class="bi bi-printer-fill button-icon"></i> <i class="bi bi-printer-fill button-icon"></i>
Printable Version Printable Version
</a> </a>
</div> </div>
<div class="row"> <div class="row">
{% if entry.status == 'late' %} {% if entry.status == 'late' %}
<a href="#" class="btn btn-warning result-action-buttons" data-result-action="override" data-_id="{{ entry._id }}"> <a href="#" class="btn btn-warning result-action-buttons" data-result-action="override" data-id="{{ entry.id }}">
<i class="bi bi-clock-history button-icon"></i> <i class="bi bi-clock-history button-icon"></i>
Allow Late Entry Allow Late Entry
</a> </a>
{% endif %} {% endif %}
<a href="#" class="btn btn-danger result-action-buttons" data-result-action="delete" data-_id="{{ entry._id }}"> <a href="#" class="btn btn-danger result-action-buttons" data-result-action="delete" data-id="{{ entry.id }}">
<i class="bi bi-trash-fill button-icon"></i> <i class="bi bi-trash-fill button-icon"></i>
Delete Result Delete Result
</a> </a>

View File

@ -69,9 +69,9 @@
</td> </td>
<td class="row-actions"> <td class="row-actions">
<a <a
href="{{ url_for('admin_views.view_entry', _id = entry._id ) }}" href="{{ url_for('admin._view_entry', id = entry.id ) }}"
class="btn btn-primary entry-details" class="btn btn-primary entry-details"
data-_id="{{entry._id}}" data-id="{{entry.id}}"
title="View Details" title="View Details"
> >
<i class="bi bi-file-medical-fill button-icon"></i> <i class="bi bi-file-medical-fill button-icon"></i>

View File

@ -2,11 +2,11 @@
{% block content %} {% block content %}
<div class="form-container"> <div class="form-container">
<form name="form-delete-user" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin_views.users') }}"> <form name="form-delete-user" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin._users') }}">
{% include "admin/components/server-alerts.html" %} {% include "admin/components/server-alerts.html" %}
<h2 class="form-heading">Delete User &lsquo;{{ user.username }}&rsquo;?</h2> <h2 class="form-heading">Delete User &lsquo;{{ user.get_username() }}&rsquo;?</h2>
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
<p>This action cannot be undone. Deleting an account will mean {{ user.username }} will no longer be able to log in to the admin console.</p> <p>This action cannot be undone. Deleting an account will mean {{ user.get_username() }} will no longer be able to log in to the admin console.</p>
<p>Are you sure you want to proceed?</p> <p>Are you sure you want to proceed?</p>
<div class="form-label-group"> <div class="form-label-group">
{{ form.password(class_="form-control", placeholder="Confirm Your Password", autofocus=true) }} {{ form.password(class_="form-control", placeholder="Confirm Your Password", autofocus=true) }}
@ -20,7 +20,7 @@
<div class="container form-submission-button"> <div class="container form-submission-button">
<div class="row"> <div class="row">
<div class="col text-center"> <div class="col text-center">
<a href="{{ url_for('admin_views.users') }}" autofocus="true" class="btn btn-md btn-primary btn-block" type="button"> <a href="{{ url_for('admin._users') }}" autofocus="true" class="btn btn-md btn-primary btn-block" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/> <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
</svg> </svg>

View File

@ -28,22 +28,22 @@
<tr> <tr>
<td> <td>
<a href=" <a href="
{% if user._id == get_id_from_cookie() %} {% if user == current_user %}
{{ url_for('admin_auth.account') }} {{ url_for('admin._update_user', id=current_user.id) }}
{% else %} {% else %}
{{ url_for('admin_views.update_user', _id=user._id) }} {{ url_for('admin._update_user', id=user.id) }}
{% endif%} {% endif%}
">{{ user.username }}</a> ">{{ user.get_username() }}</a>
</td> </td>
<td> <td>
<a href="mailto:{{ user.email }}">{{ user.email }}</a> <a href="mailto:{{ user.get_email() }}">{{ user.get_email() }}</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<a href="{{ url_for('admin_views.users') }}" class="btn btn-primary">Manage Users</a> <a href="{{ url_for('admin._users') }}" class="btn btn-primary">Manage Users</a>
</div> </div>
</div> </div>
</div> </div>
@ -57,7 +57,7 @@
<thead> <thead>
<tr> <tr>
<th> <th>
File Name Uploaded
</th> </th>
<th> <th>
Exams Exams
@ -68,22 +68,22 @@
{% for dataset in datasets %} {% for dataset in datasets %}
<tr> <tr>
<td> <td>
{{ dataset.filename }} {{ dataset.date.strftime('%d %b %Y %H:%M') }}
</td> </td>
<td> <td>
{{ dataset.use }} {{ dataset.tests|length }}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<a href="{{ url_for('admin_views.questions') }}" class="btn btn-primary">Manage Datasets</a> <a href="{{ url_for('admin._questions') }}" class="btn btn-primary">Manage Datasets</a>
{% else %} {% else %}
<div class="alert alert-primary"> <div class="alert alert-primary">
There are currently no question datasets uploaded. There are currently no question datasets uploaded.
</div> </div>
<a href="{{ url_for('admin_views.questions') }}" class="btn btn-primary">Upload Dataset</a> <a href="{{ url_for('admin._questions') }}" class="btn btn-primary">Upload Dataset</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -9,9 +9,6 @@
<tr> <tr>
<th> <th>
</th>
<th data-priority="1">
File Name
</th> </th>
<th data-priority="2"> <th data-priority="2">
Uploaded Uploaded
@ -31,7 +28,7 @@
{% for element in data %} {% for element in data %}
<tr class="table-row"> <tr class="table-row">
<td> <td>
{% if element.filename == default %} {% if element.default %}
<div class="text-success" title="Default Dataset"> <div class="text-success" title="Default Dataset">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi success bi-caret-right-fill" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi success bi-caret-right-fill" viewBox="0 0 16 16">
<path d="m12.14 8.753-5.482 4.796c-.646.566-1.658.106-1.658-.753V3.204a1 1 0 0 1 1.659-.753l5.48 4.796a1 1 0 0 1 0 1.506z"/> <path d="m12.14 8.753-5.482 4.796c-.646.566-1.658.106-1.658-.753V3.204a1 1 0 0 1 1.659-.753l5.48 4.796a1 1 0 0 1 0 1.506z"/>
@ -40,16 +37,13 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
{{ element.filename }} {{ element.date.strftime('%d %b %Y %H:%M') }}
</td> </td>
<td> <td>
{{ element.timestamp.strftime('%d %b %Y') }} {{ element.creator.get_username() }}
</td> </td>
<td> <td>
{{ element.author }} {{ element.tests|length }}
</td>
<td>
{{ element.use }}
</td> </td>
<td class="row-actions"> <td class="row-actions">
<a <a
@ -112,10 +106,10 @@
$(document).ready(function() { $(document).ready(function() {
$('#question-datasets-table').DataTable({ $('#question-datasets-table').DataTable({
'columnDefs': [ 'columnDefs': [
{'sortable': false, 'targets': [0,5]}, {'sortable': false, 'targets': [0,4]},
{'searchable': false, 'targets': [0,4,5]} {'searchable': false, 'targets': [0,3,4]}
], ],
'order': [[2, 'desc'], [3, 'asc']], 'order': [[1, 'desc'], [2, 'asc']],
'responsive': 'true', 'responsive': 'true',
'fixedHeader': 'true', 'fixedHeader': 'true',
}); });

View File

@ -2,12 +2,12 @@
{% block content %} {% block content %}
<div class="form-container"> <div class="form-container">
<form name="form-update-user" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin_views.users') }}"> <form name="form-update-user" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin._users') }}">
{% include "admin/components/server-alerts.html" %} {% include "admin/components/server-alerts.html" %}
<h2 class="form-heading">Update User &lsquo;{{ user.username }}&rsquo;</h2> <h2 class="form-heading">Update User &lsquo;{{ user.get_username() }}&rsquo;</h2>
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
<div class="form-label-group"> <div class="form-label-group">
{{ form.email(class_="form-control", placeholder="Email Address", value = user.email) }} {{ form.email(class_="form-control", placeholder="Email Address", value = user.get_email()) }}
{{ form.email.label }} {{ form.email.label }}
</div> </div>
<div class="form-label-group"> <div class="form-label-group">
@ -23,17 +23,17 @@
{{ form.notify.label }} {{ form.notify.label }}
</div> </div>
<div class="form-label-group"> <div class="form-label-group">
Please confirm <strong>your password</strong> before committing any changes to a user account. Please confirm <strong>your current password</strong> before committing any changes to a user account.
</div> </div>
<div class="form-label-group"> <div class="form-label-group">
{{ form.user_password(class_="form-control", placeholder="Your Password", value = user.email) }} {{ form.confirm_password(class_="form-control", placeholder="Your Password", value = user.email) }}
{{ form.user_password.label }} {{ form.confirm_password.label }}
</div> </div>
{% include "admin/components/client-alerts.html" %} {% include "admin/components/client-alerts.html" %}
<div class="container form-submission-button"> <div class="container form-submission-button">
<div class="row"> <div class="row">
<div class="col text-center"> <div class="col text-center">
<a href="{{ url_for('admin_views.users') }}" class="btn btn-md btn-danger btn-block" type="button"> <a href="{{ url_for('admin._users') }}" class="btn btn-md btn-danger btn-block" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/> <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
</svg> </svg>

View File

@ -23,7 +23,7 @@
{% for user in users %} {% for user in users %}
<tr class="table-row"> <tr class="table-row">
<td> <td>
{% if user._id == get_id_from_cookie() %} {% if user == current_user %}
<div class="text-success" title="Current User"> <div class="text-success" title="Current User">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi success bi-caret-right-fill" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi success bi-caret-right-fill" viewBox="0 0 16 16">
<path d="m12.14 8.753-5.482 4.796c-.646.566-1.658.106-1.658-.753V3.204a1 1 0 0 1 1.659-.753l5.48 4.796a1 1 0 0 1 0 1.506z"/> <path d="m12.14 8.753-5.482 4.796c-.646.566-1.658.106-1.658-.753V3.204a1 1 0 0 1 1.659-.753l5.48 4.796a1 1 0 0 1 0 1.506z"/>
@ -32,18 +32,18 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
{{ user.username }} {{ user.get_username() }}
</td> </td>
<td> <td>
{{ user.email }} {{ user.get_email() }}
</td> </td>
<td class="row-actions"> <td class="row-actions">
<a <a
href=" href="
{% if not user._id == get_id_from_cookie() %} {% if not user == current_user %}
{{ url_for('admin_views.update_user', _id = user._id ) }} {{ url_for('admin._update_user', id = user.id ) }}
{% else %} {% else %}
{{ url_for('admin_auth.account') }} {{ url_for('admin._update_user', id=current_user.id) }}
{% endif %} {% endif %}
" "
class="btn btn-primary" class="btn btn-primary"
@ -53,15 +53,15 @@
</a> </a>
<a <a
href=" href="
{% if not user._id == get_id_from_cookie() %} {% if not user == current_user %}
{{ url_for('admin_views.delete_user', _id = user._id ) }} {{ url_for('admin._delete_user', id = user.id ) }}
{% else %} {% else %}
# #
{% endif %} {% endif %}
" "
class="btn btn-danger {% if user._id == get_id_from_cookie() %} disabled {% endif %}" class="btn btn-danger {% if user == current_user %} disabled {% endif %}"
title="Delete User" title="Delete User"
{% if user._id == get_id_from_cookie() %} onclick="return false" {% endif %} {% if user == current_user %} onclick="return false" {% endif %}
> >
<i class="bi bi-person-x-fill button-icon"></i> <i class="bi bi-person-x-fill button-icon"></i>
</button> </button>

View File

@ -12,38 +12,33 @@
<h5 class="mb-1">Exam Code</h5> <h5 class="mb-1">Exam Code</h5>
</div> </div>
<h2> <h2>
{{ '—'.join([test.test_code[:4], test.test_code[4:8], test.test_code[8:]]) }} {{ test.get_code() }}
</h2> </h2>
</li> </li>
<li class="list-group-item list-group-item-action"> <li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Dataset</h5> <h5 class="mb-1">Dataset</h5>
</div> </div>
{{ test.dataset }} {{ test.dataset.date.strftime('%Y%m%d%H%M%S') }}
</li> </li>
<li class="list-group-item list-group-item-action"> <li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Created By</h5> <h5 class="mb-1">Created By</h5>
</div> </div>
{{ test.creator }} {{ test.creator.get_username() }}
</li>
<li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Date Created</h5>
</div>
{{ test.date_created.strftime('%d %b %Y') }}
</li> </li>
<li class="list-group-item list-group-item-action"> <li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Start Date</h5> <h5 class="mb-1">Start Date</h5>
</div> </div>
{{ test.start_date.strftime('%d %b %Y') }} {{ test.start_date.strftime('%d %b %Y %H:%M') }}
</li> </li>
<li class="list-group-item list-group-item-action"> <li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Expiry Date</h5> <h5 class="mb-1">Expiry Date</h5>
</div> </div>
{{ test.expiry_date.strftime('%d %b %Y') }} {{ test.end_date.strftime('%d %b %Y %H:%M') }}
</li> </li>
<li class="list-group-item list-group-item-action"> <li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
@ -62,7 +57,7 @@
{% endif %} {% endif %}
</li> </li>
<div class="accordion" id="test-info-detail"> <div class="accordion" id="test-info-detail">
{% if 'entries' in test and test.entries|length > 0 %} {% if test.entries|length > 0 %}
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header" id="test-entries"> <h2 class="accordion-header" id="test-entries">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#test-entries-list" aria-expanded="false" aria-controls="test-entries-list"> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#test-entries-list" aria-expanded="false" aria-controls="test-entries-list">
@ -76,7 +71,7 @@
{% for entry in test.entries %} {% for entry in test.entries %}
<tr> <tr>
<td> <td>
<a href="{{ url_for('admin_views.view_entry', _id=entry) }}" >Entry {{ loop.index }}</a> <a href="{{ url_for('admin._view_entry', id=entry) }}" >Entry {{ loop.index }}</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -86,7 +81,7 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if 'time_adjustments' in test and test.time_adjustments|length > 0 %} {% if test.adjustments %}
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header" id="test-adjustments"> <h2 class="accordion-header" id="test-adjustments">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#test-adjustments-list" aria-expanded="false" aria-controls="test-adjustments-list"> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#test-adjustments-list" aria-expanded="false" aria-controls="test-adjustments-list">
@ -110,10 +105,10 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for key, value in test.time_adjustments.items() %} {% for key, value in test.adjustments.items() %}
<tr> <tr>
<td> <td>
{{ key }} {{ key.upper() }}
</td> </td>
<td> <td>
{{ value }} {{ value }}
@ -143,7 +138,7 @@
<form name="form-add-adjustment" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success=""> <form name="form-add-adjustment" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
<div class="form-label-group"> <div class="form-label-group">
{{ form.time(class_="form-control", placeholder="Enter Username") }} {{ form.time(class_="form-control", placeholder="Enter Time") }}
{{ form.time.label }} {{ form.time.label }}
</div> </div>
<div class="container form-submission-button"> <div class="container form-submission-button">
@ -168,11 +163,18 @@
</div> </div>
<div class="container justify-content-center"> <div class="container justify-content-center">
<div class="row"> <div class="row">
<a href="#" class="btn btn-warning test-action" data-action="close" data-_id="{{ test._id }}"> {% if test.start_date <= now %}
<i class="bi bi-hourglass button-icon"></i> <a href="#" class="btn btn-warning test-action {% if test.end_date < now %}disabled{% endif %}" data-action="end" data-id="{{ test.id }}">
Close Exam <i class="bi bi-hourglass-bottom button-icon"></i>
</a> Close Exam
<a href="#" class="btn btn-danger test-action" data-action="delete" data-_id="{{ test._id }}"> </a>
{% else %}
<a href="#" class="btn btn-success test-action {% if test.start_date < now %}disabled{% endif %}" data-action="start" data-id="{{ test.id }}">
<i class="bi bi-hourglass-top button-icon"></i>
Start Exam
</a>
{% endif %}
<a href="#" class="btn btn-danger test-action" data-action="delete" data-id="{{ test.id }}">
<i class="bi bi-file-earmark-excel-fill button-icon"></i> <i class="bi bi-file-earmark-excel-fill button-icon"></i>
Delete Exam Delete Exam
</a> </a>

View File

@ -33,13 +33,13 @@
{% for test in tests %} {% for test in tests %}
<tr class="table-row"> <tr class="table-row">
<td> <td>
{{ test.start_date.strftime('%d %b %Y') }} {{ test.start_date.strftime('%d %b %y %H:%M') }}
</td> </td>
<td> <td>
{{ '—'.join([test.test_code[:4], test.test_code[4:8], test.test_code[8:]]) }} {{ test.get_code() }}
</td> </td>
<td> <td>
{{ test.expiry_date.strftime('%d %b %Y') }} {{ test.end_date.strftime('%d %b %Y %H:%M') }}
</td> </td>
<td> <td>
{% if test.time_limit == None -%} {% if test.time_limit == None -%}
@ -61,7 +61,7 @@
<a <a
href="#" href="#"
class="btn btn-primary test-action" class="btn btn-primary test-action"
data-_id="{{test._id}}" data-id="{{test.id}}"
title="Edit Exam" title="Edit Exam"
data-action="edit" data-action="edit"
> >
@ -70,7 +70,7 @@
<a <a
href="#" href="#"
class="btn btn-danger test-action" class="btn btn-danger test-action"
data-_id="{{test._id}}" data-id="{{test.id}}"
title="Delete Exam" title="Delete Exam"
data-action="delete" data-action="delete"
> >

View File

@ -9,7 +9,7 @@ from flask import Blueprint, jsonify, render_template, redirect, request, sessio
from flask.helpers import flash, url_for from flask.helpers import flash, url_for
from flask_login import current_user, login_required from flask_login import current_user, login_required
from datetime import date, datetime from datetime import date, datetime, timedelta
from json import loads from json import loads
import secrets import secrets
@ -27,12 +27,12 @@ admin = Blueprint(
def _home(): def _home():
tests = Test.query.all() tests = Test.query.all()
results = Entry.query.all() results = Entry.query.all()
current_tests = [ test for test in tests if test['expiry_date'].date() >= datetime.now().date() and test['start_date'].date() <= date.today() ] current_tests = [ test for test in tests if test.end_date >= datetime.now() and test.start_date.date() <= date.today() ]
current_tests.sort(key= lambda x: x['expiry_date'], reverse=True) current_tests.sort(key= lambda x: x.end_date, reverse=True)
upcoming_tests = [ test for test in tests if test['start_date'].date() > datetime.now().date()] upcoming_tests = [ test for test in tests if test.start_date.date() > datetime.now().date()]
upcoming_tests.sort(key= lambda x: x['start_date']) upcoming_tests.sort(key= lambda x: x.start_date)
recent_results = [result for result in results if not result['status'] == 'started' ] recent_results = [result for result in results if not result.status == 'started' ]
recent_results.sort(key= lambda x: x['end_time'], reverse=True) recent_results.sort(key= lambda x: x.end_time, reverse=True)
for result in recent_results: for result in recent_results:
result['percent'] = round(100*result['result']['score']/result['result']['max']) result['percent'] = round(100*result['result']['score']/result['result']['max'])
return render_template('/admin/index.html', current_tests = current_tests, upcomimg_tests = upcoming_tests, recent_results = recent_results) return render_template('/admin/index.html', current_tests = current_tests, upcomimg_tests = upcoming_tests, recent_results = recent_results)
@ -83,9 +83,9 @@ def _register():
if request.method == 'POST': if request.method == 'POST':
if form.validate_on_submit(): if form.validate_on_submit():
new_user = User() new_user = User()
new_user.set_username = request.form.get('username').lower() new_user.set_username(request.form.get('username').lower())
new_user.set_email = request.form.get('email').lower() new_user.set_email(request.form.get('email').lower())
new_user.set_password = request.form.get('password').lower() new_user.set_password(request.form.get('password'))
success, message = new_user.register() success, message = new_user.register()
if success: if success:
flash(message=f'{message} Please log in to continue.', category='success') flash(message=f'{message} Please log in to continue.', category='success')
@ -95,7 +95,7 @@ def _register():
return jsonify({'error': message}), 401 return jsonify({'error': message}), 401
errors = [*form.username.errors, *form.email.errors, *form.password.errors, *form.password_reenter.errors] errors = [*form.username.errors, *form.email.errors, *form.password.errors, *form.password_reenter.errors]
return jsonify({ 'error': errors}), 400 return jsonify({ 'error': errors}), 400
return render_template('admin/auth/register.html') return render_template('admin/auth/register.html', form=form)
@admin.route('/reset/') @admin.route('/reset/')
def _reset(): def _reset():
@ -149,9 +149,9 @@ def _users():
if form.validate_on_submit(): if form.validate_on_submit():
password = request.form.get('password') password = request.form.get('password')
new_user = User() new_user = User()
new_user.set_username = request.form.get('username').lower() new_user.set_username(request.form.get('username').lower())
new_user.set_password = secrets.token_hex(12) if not password else password new_user.set_password(secrets.token_hex(12)) if not password else password
new_user.set_email = request.form.get('email') new_user.set_email(request.form.get('email'))
success, message = new_user.register(notify=request.form.get('notify')) success, message = new_user.register(notify=request.form.get('notify'))
if success: return jsonify({'success': message}), 200 if success: return jsonify({'success': message}), 200
return jsonify({'error': message}), 401 return jsonify({'error': message}), 401
@ -192,23 +192,26 @@ def _update_user(id:str):
if request.method == 'POST': if request.method == 'POST':
if not user: return jsonify({'error': 'User does not exist.'}), 400 if not user: return jsonify({'error': 'User does not exist.'}), 400
if form.validate_on_submit(): if form.validate_on_submit():
if not user.verify_password(request.form.get('confirm_password')): return jsonify({'error': 'Invalid password for your account.'}), 401
success, message = user.update( success, message = user.update(
password = request.form.get('password'), password = request.form.get('password'),
email = request.form.get('email'), email = request.form.get('email'),
notify = request.form.get('notify') notify = request.form.get('notify')
) )
if success: return jsonify({'success': message}), 200 if success:
flash(message, 'success')
return jsonify({'success': message}), 200
return jsonify({'error': message}), 400 return jsonify({'error': message}), 400
errors = [*form.confirm_password.errors, *form.email.errors, *form.password.errors, *form.password_reenter.errors] errors = [*form.confirm_password.errors, *form.email.errors, *form.password.errors, *form.password_reenter.errors]
return jsonify({ 'error': errors}), 400 return jsonify({ 'error': errors}), 400
if not user: if not user:
flash('User not found.', 'error') flash('User not found.', 'error')
return redirect(url_for('admin._users')) return redirect(url_for('admin._users'))
return render_template('/admin/settings/delete_user.html', form=form, id = id, user = user) return render_template('/admin/settings/update_user.html', form=form, id = id, user = user)
@admin.route('/settings/questions/', methods=['GET', 'POST']) @admin.route('/settings/questions/', methods=['GET', 'POST'])
@login_required @login_required
def _quesitons(): def _questions():
form = UploadData() form = UploadData()
if request.method == 'POST': if request.method == 'POST':
if form.validate_on_submit(): if form.validate_on_submit():
@ -226,7 +229,7 @@ def _quesitons():
return jsonify({ 'error': errors}), 400 return jsonify({ 'error': errors}), 400
data = Dataset.query.all() data = Dataset.query.all()
return render_template('/admin/settings/questions.html', data=data) return render_template('/admin/settings/questions.html', form=form, data=data)
@admin.route('/settings/questions/edit/', methods=['POST']) @admin.route('/settings/questions/edit/', methods=['POST'])
@login_required @login_required
@ -248,6 +251,7 @@ def _tests(filter:str=None):
tests = None tests = None
_tests = Test.query.all() _tests = Test.query.all()
form = None form = None
now = datetime.now()
if not datasets: if not datasets:
flash('There are no available question datasets. Please upload a question dataset in order to set up an exam.', 'error') flash('There are no available question datasets. Please upload a question dataset in order to set up an exam.', 'error')
return redirect(url_for('admin._questions')) return redirect(url_for('admin._questions'))
@ -255,21 +259,21 @@ def _tests(filter:str=None):
if filter == 'create': if filter == 'create':
form = CreateTest() form = CreateTest()
form.time_limit.choices = get_time_options() form.time_limit.choices = get_time_options()
form.dataset.choices = get_dataset_choices form.dataset.choices = get_dataset_choices()
form.time_limit.default='none' form.time_limit.default='none'
form.process() form.process()
display_title = '' display_title = ''
error_none = '' error_none = ''
if filter in [None, '', 'active']: if filter in [None, '', 'active']:
tests = [ test for test in _tests if test['expiry_date'].date() >= date.today() and test['start_date'].date() <= date.today() ] tests = [ test for test in _tests if test.end_date >= now and test.start_date <= now ]
display_title = 'Active Exams' display_title = 'Active Exams'
error_none = 'There are no exams that are currently active. You can create one using the Creat Exam form.' error_none = 'There are no exams that are currently active. You can create one using the Creat Exam form.'
if filter == 'expired': if filter == 'expired':
tests = [ test for test in _tests if test['expiry_date'].date() < date.today() ] tests = [ test for test in _tests if test.end_date < now ]
display_title = 'Expired Exams' display_title = 'Expired Exams'
error_none = 'There are no expired exams. Exams will appear in this category after their expiration date has passed.' error_none = 'There are no expired exams. Exams will appear in this category after their expiration date has passed.'
if filter == 'scheduled': if filter == 'scheduled':
tests = [ test for test in _tests if test['start_date'].date() > date.today()] tests = [ test for test in _tests if test.start_date > now]
display_title = 'Scheduled Exams' display_title = 'Scheduled Exams'
error_none = 'There are no scheduled exams pending. You can schedule an exam for the future using the Create Exam form.' error_none = 'There are no scheduled exams pending. You can schedule an exam for the future using the Create Exam form.'
if filter == 'all': if filter == 'all':
@ -287,11 +291,12 @@ def _create_test():
if form.validate_on_submit(): if form.validate_on_submit():
new_test = Test() new_test = Test()
new_test.start_date = request.form.get('start_date') new_test.start_date = request.form.get('start_date')
new_test.start_date = datetime.strptime(new_test.start_date, '%Y-%m-%d') new_test.start_date = datetime.strptime(new_test.start_date, '%Y-%m-%dT%H:%M')
new_test.end_date = request.form.get('expiry_date') new_test.end_date = request.form.get('expiry_date')
new_test.end_date = datetime.strptime(new_test.end_date, '%Y-%m-%d') new_test.end_date = datetime.strptime(new_test.end_date, '%Y-%m-%dT%H:%M')
new_test.time_limit = request.form.get('time_limit')
dataset = request.form.get('dataset') dataset = request.form.get('dataset')
new_test.dataset = Dataset.query.filter_by(id=dataset) new_test.dataset = Dataset.query.filter_by(id=dataset).first()
success, message = new_test.create() success, message = new_test.create()
if success: if success:
flash(message=message, category='success') flash(message=message, category='success')
@ -371,7 +376,7 @@ def _view_entry(id:str=None):
flash('Invalid entry ID.', 'error') flash('Invalid entry ID.', 'error')
return redirect(url_for('admin._view_entries')) return redirect(url_for('admin._view_entries'))
test = entry['test'] test = entry['test']
dataset = test['dataset'] dataset = test.dataset
dataset_path = dataset.get_file() dataset_path = dataset.get_file()
with open(dataset_path, 'r') as _dataset: with open(dataset_path, 'r') as _dataset:
data = loads(_dataset.read()) data = loads(_dataset.read())

View File

@ -16,21 +16,21 @@ def _fetch_questions():
id = request.get_json()['id'] id = request.get_json()['id']
entry = Entry.query.filter_by(id=id).first() entry = Entry.query.filter_by(id=id).first()
if not entry: return jsonify({'error': 'Invalid entry ID.'}), 400 if not entry: return jsonify({'error': 'Invalid entry ID.'}), 400
test = entry['test'] test = entry.test
user_code = entry['user_code'] user_code = entry.user_code
time_limit = test['time_limit'] time_limit = test.time_limit
time_adjustment = 0 time_adjustment = 0
if time_limit: if time_limit:
_time_limit = int(time_limit) _time_limit = int(time_limit)
if user_code: if user_code:
time_adjustment = test['time_adjustments'][user_code] time_adjustment = test.time_adjustments[user_code]
_time_limit += time_adjustment _time_limit += time_adjustment
end_delta = timedelta(minutes=_time_limit) end_delta = timedelta(minutes=_time_limit)
end_time = datetime.utcnow() + end_delta end_time = datetime.utcnow() + end_delta
else: else:
end_time = None end_time = None
entry.start() entry.start()
dataset = test['dataset'] dataset = test.dataset
success, message = dataset.check_file() success, message = dataset.check_file()
if not success: return jsonify({'error': message}), 500 if not success: return jsonify({'error': message}), 500
data_path = dataset.get_file() data_path = dataset.get_file()
@ -51,7 +51,7 @@ def _submit_quiz():
entry = Entry.query.filter_by(id=id).first() entry = Entry.query.filter_by(id=id).first()
if not entry: return jsonify({'error': 'Unrecognised ID.'}), 400 if not entry: return jsonify({'error': 'Unrecognised ID.'}), 400
test = entry['test'] test = entry['test']
dataset = test['dataset'] dataset = test.dataset
success, message = dataset.check_file() success, message = dataset.check_file()
if not success: return jsonify({'error': message}), 500 if not success: return jsonify({'error': message}), 500
data_path = dataset.get_file() data_path = dataset.get_file()

View File

@ -2,10 +2,11 @@ from ..tools.forms import value
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_wtf.file import FileAllowed, FileField, FileRequired from flask_wtf.file import FileAllowed, FileField, FileRequired
from wtforms import BooleanField, DateField, IntegerField, PasswordField, SelectField, StringField from wtforms import BooleanField, IntegerField, PasswordField, SelectField, StringField
from wtforms.fields import DateTimeLocalField
from wtforms.validators import InputRequired, Email, EqualTo, Length, Optional from wtforms.validators import InputRequired, Email, EqualTo, Length, Optional
from datetime import date, timedelta from datetime import date, datetime, timedelta
class Login(FlaskForm): class Login(FlaskForm):
username = StringField('Username', validators=[InputRequired(), Length(min=4, max=15)]) username = StringField('Username', validators=[InputRequired(), Length(min=4, max=15)])
@ -50,8 +51,8 @@ class UpdateAccount(FlaskForm):
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): class CreateTest(FlaskForm):
start_date = DateField('Start Date', format="%Y-%m-%d", validators=[InputRequired()], default = date.today() ) start_date = DateTimeLocalField('Start Date', format='%Y-%m-%dT%H:%M', validators=[InputRequired()], default = datetime.now() )
expiry_date = DateField('Expiry Date', format="%Y-%m-%d", validators=[InputRequired()], default = date.today() + timedelta(days=1) ) expiry_date = DateTimeLocalField('Expiry Date', format='%Y-%m-%dT%H:%M', validators=[InputRequired()], default = date.today() + timedelta(days=1) )
time_limit = SelectField('Time Limit') time_limit = SelectField('Time Limit')
dataset = SelectField('Question Dataset') dataset = SelectField('Question Dataset')

View File

@ -26,7 +26,7 @@ class Dataset(db.Model):
def generate_id(self): raise AttributeError('generate_id is not a readable attribute.') def generate_id(self): raise AttributeError('generate_id is not a readable attribute.')
generate_id.setter generate_id.setter
def generate_id(self): self.id = uuid4.hex() def generate_id(self): self.id = uuid4().hex
def make_default(self): def make_default(self):
for dataset in Dataset.query.all(): for dataset in Dataset.query.all():

View File

@ -32,7 +32,7 @@ class Entry(db.Model):
def generate_id(self): raise AttributeError('generate_id is not a readable attribute.') def generate_id(self): raise AttributeError('generate_id is not a readable attribute.')
generate_id.setter generate_id.setter
def generate_id(self): self.id = uuid4.hex() def generate_id(self): self.id = uuid4().hex
@property @property
def set_first_name(self): raise AttributeError('set_first_name is not a readable attribute.') def set_first_name(self): raise AttributeError('set_first_name is not a readable attribute.')

View File

@ -6,8 +6,6 @@ from ..tools.logs import write
from flask_login import current_user from flask_login import current_user
from datetime import date, datetime from datetime import date, datetime
from json import dump, loads
import os
import secrets import secrets
from uuid import uuid4 from uuid import uuid4
@ -24,13 +22,13 @@ class Test(db.Model):
entries = db.relationship('Entry', backref='test') entries = db.relationship('Entry', backref='test')
def __repr__(self): def __repr__(self):
return f'<test with code {self.code} was created by {current_user.get_username()}.>' return f'<Test with code {self.get_code()} was created by {current_user.get_username()}.>'
@property @property
def generate_id(self): raise AttributeError('generate_id is not a readable attribute.') def generate_id(self): raise AttributeError('generate_id is not a readable attribute.')
generate_id.setter generate_id.setter
def generate_id(self): self.id = uuid4.hex() def generate_id(self): self.id = uuid4().hex
@property @property
def generate_code(self): raise AttributeError('generate_code is not a readable attribute.') def generate_code(self): raise AttributeError('generate_code is not a readable attribute.')
@ -65,26 +63,26 @@ class Test(db.Model):
if self.entries: return False, f'Cannot delete a test with submitted entries.' if self.entries: return False, f'Cannot delete a test with submitted entries.'
db.session.delete(self) db.session.delete(self)
db.session.commit() db.session.commit()
write('system.log', f'Test with code {code} has been deleted by {current_user.get_username()}.') write('system.log', f'Test with code {self.get_code()} has been deleted by {current_user.get_username()}.')
return True, f'Test with code {code} has been deleted.' return True, f'Test with code {self.get_code()} has been deleted.'
def start(self): def start(self):
now = datetime.now() now = datetime.now()
if self.start_date.date() > now.date(): if self.start_date.date() > now.date():
self.start_date = now self.start_date = now
db.session.commit() db.session.commit()
write('system.log', f'Test with code {self.code} has been started by {current_user.get_username()}.') write('system.log', f'Test with code {self.get_code()} has been started by {current_user.get_username()}.')
return True, f'Test with code {self.code} has been started.' return True, f'Test with code {self.get_code()} has been started.'
return False, f'Test with code {self.code} has already started.' return False, f'Test with code {self.get_code()} has already started.'
def end(self): def end(self):
now = datetime.now() now = datetime.now()
if self.end_date.date() > now.date(): if self.end_date >= now:
self.end_date = now self.end_date = now
db.session.commit() db.session.commit()
write('system.log', f'Test with code {self.code} ended by {current_user.get_username()}.') write('system.log', f'Test with code {self.get_code()} ended by {current_user.get_username()}.')
return True, f'Test with code {self.code} has been ended.' return True, f'Test with code {self.get_code()} has been ended.'
return False, f'Test with code {self.code} has already ended.' return False, f'Test with code {self.get_code()} has already ended.'
def add_adjustment(self, time:int): def add_adjustment(self, time:int):
adjustments = self.adjustments if self.adjustments is not None else {} adjustments = self.adjustments if self.adjustments is not None else {}

View File

@ -26,7 +26,7 @@ class User(UserMixin, db.Model):
def generate_id(self): raise AttributeError('generate_id is not a readable attribute.') def generate_id(self): raise AttributeError('generate_id is not a readable attribute.')
generate_id.setter generate_id.setter
def generate_id(self): self.id = uuid4.hex() def generate_id(self): self.id = uuid4().hex
@property @property
def set_username(self): raise AttributeError('set_username is not a readable attribute.') def set_username(self): raise AttributeError('set_username is not a readable attribute.')
@ -83,7 +83,7 @@ class User(UserMixin, db.Model):
print('Password', new_password) print('Password', new_password)
print('Reset Token', self.reset_token) print('Reset Token', self.reset_token)
print('Verification Token', self.verification_token) print('Verification Token', self.verification_token)
print('Reset Link', f'{url_for("auth._reset", token=self.reset_token, verification=self.verification_token, _external=True)}') print('Reset Link', f'{url_for("admin._reset", token=self.reset_token, verification=self.verification_token, _external=True)}')
return jsonify({'success': 'Your password reset link has been generated.'}), 200 return jsonify({'success': 'Your password reset link has been generated.'}), 200
def clear_reset_tokens(self): def clear_reset_tokens(self):
@ -103,6 +103,5 @@ class User(UserMixin, db.Model):
if password: self.set_password(password) if password: self.set_password(password)
if email: self.set_email(email) if email: self.set_email(email)
db.session.commit() db.session.commit()
message = f'Information for user {self.get_username()} has been updated by {current_user.get_username()}.' write('system.log', f'Information for user {self.get_username()} has been updated by {current_user.get_username()}.')
write('system.log', message) return True, f'Account {self.get_username()} has been updated.'
return True, message

View File

@ -143,7 +143,7 @@ $("#btn-start-quiz").click(function(event){
$.ajax({ $.ajax({
url: `/api/questions/`, url: `/api/questions/`,
type: 'POST', type: 'POST',
data: JSON.stringify({'_id': _id}), data: JSON.stringify({'id': id}),
contentType: "application/json", contentType: "application/json",
success: function(response) { success: function(response) {
$(this).fadeOut(); $(this).fadeOut();
@ -223,7 +223,7 @@ $("#q-review-answers").click(function(event){
$(".quiz-button-submit").click(function(event){ $(".quiz-button-submit").click(function(event){
let submission = { let submission = {
'_id': _id, 'id': id,
'answers': answers 'answers': answers
} }
@ -607,7 +607,7 @@ function count_questions(status) {
// Variable Definitions // Variable Definitions
const _id = window.localStorage.getItem('_id'); const id = window.localStorage.getItem('id');
var current_question = 0; var current_question = 0;
var total_questions = 0; var total_questions = 0;

View File

@ -23,8 +23,8 @@ $('form[name=form-quiz-start]').submit(function(event) {
data: data, data: data,
dataType: 'json', dataType: 'json',
success: function(response) { success: function(response) {
var _id = response._id var id = response.id
window.localStorage.setItem('_id', _id); window.localStorage.setItem('id', id);
window.location.href = `/test/`; window.location.href = `/test/`;
}, },
error: function(response) { error: function(response) {

View File

@ -2,7 +2,7 @@ from .data import load
from ..models import User from ..models import User
from flask import abort, redirect from flask import abort, redirect
from flask.helpers import url_for from flask.helpers import flash, url_for
from flask_login import current_user from flask_login import current_user
from functools import wraps from functools import wraps
@ -10,7 +10,9 @@ from functools import wraps
def require_account_creation(function): def require_account_creation(function):
@wraps(function) @wraps(function)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
if User.query.count() == 0: return redirect(url_for('views._register')) if User.query.count() == 0:
flash('Please register a user account.', 'alert')
return redirect(url_for('admin._register'))
return function(*args, **kwargs) return function(*args, **kwargs)
return wrapper return wrapper

View File

@ -18,7 +18,8 @@ def check_is_json(file):
def validate_json(file): def validate_json(file):
file.stream.seek(0) file.stream.seek(0)
data = json.loads(file.read()) data = json.loads(file.read())
if not type(data) is list: return False if not isinstance(data, list): return False
return True
def randomise_list(list:list): def randomise_list(list:list):
_list = list.copy() _list = list.copy()

View File

@ -1,5 +1,4 @@
from ..models import Dataset
from ..modules import db from ..modules import db
from wtforms.validators import ValidationError from wtforms.validators import ValidationError
@ -46,11 +45,12 @@ def get_time_options():
return time_options return time_options
def get_dataset_choices(): def get_dataset_choices():
from ..models import Dataset
datasets = Dataset.query.all() datasets = Dataset.query.all()
dataset_choices = [] dataset_choices = []
for dataset in datasets: for dataset in datasets:
label = dataset['date'].strftime('%Y%m%d%H%M%S') label = dataset.date.strftime('%Y%m%d%H%M%S')
label = f'{label} (Default)' if dataset.default else label label = f'{label} (Default)' if dataset.default else label
choice = (dataset['id'], label) choice = (dataset.id, label)
dataset_choices.append(choice) dataset_choices.append(choice)
return dataset_choices return dataset_choices

View File

@ -1,4 +1,4 @@
from ..config import Config from .config import Config
from flask import Blueprint, redirect, request, render_template from flask import Blueprint, redirect, request, render_template

View File

@ -1,3 +1,4 @@
from app.models import User
from app.modules import bootstrap, csrf, db, login_manager, mail from app.modules import bootstrap, csrf, db, login_manager, mail
from config import Config from config import Config
@ -21,8 +22,8 @@ def create_app():
login_manager.login_view = 'admin._login' login_manager.login_view = 'admin._login'
@login_manager.user_loader @login_manager.user_loader
def _load_user(user_id): def _load_user(id):
pass return User.query.filter_by(id=id).first()
@app.errorhandler(404) @app.errorhandler(404)
def _404_handler(error): def _404_handler(error):
@ -32,7 +33,7 @@ def create_app():
return jsonify({'error':'Could not validate a secure connection.'}), 403 return jsonify({'error':'Could not validate a secure connection.'}), 403
@app.context_processor @app.context_processor
def _now(): def _now():
return {'now': datetime.utcnow()} return {'now': datetime.now()}
from app.admin.views import admin from app.admin.views import admin
from app.api.views import api from app.api.views import api