8 Commits

22 changed files with 975 additions and 11 deletions

View File

@ -2,12 +2,7 @@
# Source https://pentacent.medium.com/nginx-and-lets-encrypt-with-docker-in-less-than-5-minutes-b4b8a60d3a71 # Source https://pentacent.medium.com/nginx-and-lets-encrypt-with-docker-in-less-than-5-minutes-b4b8a60d3a71
if ! [ -x "$(command -v docker)" ]; then if ! [ -x "$(command -v docker compose)" ]; then
echo 'Error: docker is not installed.' >&2
exit 1
fi
if ! [ -x "$(command -v compose)" ]; then
echo 'Error: docker compose is not installed.' >&2 echo 'Error: docker compose is not installed.' >&2
exit 1 exit 1
fi fi

View File

@ -1,7 +1,9 @@
from .config import Production as Config from .config import Development as Config
from .install import install_app from .install import install_app
from .models import User from .models import Entry, Dataset, Test, User
from .extensions import bootstrap, csrf, db, login_manager, mail from .extensions import bootstrap, csrf, db, login_manager, mail
from .tools.data import save
from .tools.logs import write
from flask import flash, Flask, render_template, request from flask import flash, Flask, render_template, request
from flask.helpers import url_for from flask.helpers import url_for
@ -9,6 +11,7 @@ from flask.json import jsonify
from flask_wtf.csrf import CSRFError from flask_wtf.csrf import CSRFError
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
from cryptography.fernet import Fernet
from datetime import datetime from datetime import datetime
def create_app(): def create_app():

View File

@ -1,5 +1,7 @@
import os import os
from pathlib import Path from pathlib import Path
if not os.getenv('DATA'):
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv('../.env') load_dotenv('../.env')

View File

View File

@ -0,0 +1,260 @@
body {
padding: 80px 0;
}
.site-footer {
background-color: lightgray;
font-size: small;
}
.site-footer p {
margin: 0;
}
.form-container {
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
}
.form-display {
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;
}
table.dataTable {
border-collapse: collapse;
width: 100%;
}
.table-row {
vertical-align: middle;
}
.row-actions {
text-align: center;
white-space: nowrap;
}
.dataTables_wrapper .dt-buttons {
left: 50%;
transform: translateX(-50%);
float:none;
text-align:center;
}
.row-actions button, .row-actions a {
margin: 0px 5px;
}
#cookie-alert {
padding-right: 16px;
}
#dismiss-cookie-alert {
margin-top: 16px;
width: fit-content;
}
.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;
}
.button-icon {
font-size: 20px;
}
.form-upload {
margin: 2rem 0;
font-size: 14pt;
}
.result-action-buttons, .test-action {
margin: 5px auto;
width: fit-content;
}
.accordion-item {
background-color: unset;
}
/* Change Autocomplete styles in Chrome*/
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
transition: background-color 5000s ease-in-out 0s;
}
/* 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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,233 @@
// Menu Highlight Scripts
const menuItems = document.getElementsByClassName('nav-link');
for(let i = 0; i < menuItems.length; i++) {
if(menuItems[i].pathname == window.location.pathname) {
menuItems[i].classList.add('active');
}
}
const dropdownItems = document.getElementsByClassName('dropdown-item');
for(let i = 0; i< dropdownItems.length; i++) {
if(dropdownItems[i].pathname == window.location.pathname) {
dropdownItems[i].classList.add('active');
$( "#" + dropdownItems[i].id ).closest( '.dropdown' ).find('.dropdown-toggle').addClass('active');
}
}
// General Post Method Form Processing Script
$('form.form-post').submit(function(event) {
var $form = $(this);
var data = $form.serialize();
var url = $(this).prop('action');
var rel_success = $(this).data('rel-success');
$.ajax({
url: url,
type: 'POST',
data: data,
dataType: 'json',
success: function(response) {
if (response.redirect_to) {
window.location.href = response.redirect_to;
}
else {
window.location.href = rel_success;
}
},
error: function(response) {
error_response(response);
}
});
event.preventDefault();
});
// Form Upload Questions - Special case, needs to handle files.
$('form[name=form-upload-questions]').submit(function(event) {
var $form = $(this);
var data = new FormData($form[0]);
var file = $('input[name=data_file]')[0].files[0]
data.append('file', file)
$.ajax({
url: window.location.pathname,
type: 'POST',
data: data,
processData: false,
contentType: false,
success: function(response) {
window.location.reload();
},
error: function(response) {
error_response(response);
}
});
event.preventDefault();
});
// Edit and Delete Test Button Handlers
$('.test-action').click(function(event) {
let id = $(this).data('id');
let action = $(this).data('action');
if (action == 'delete' || action == 'start' || action == 'end') {
$.ajax({
url: `/admin/tests/edit/`,
type: 'POST',
data: JSON.stringify({'id': id, 'action': action}), // TODO Change how CRUD operations work
contentType: 'application/json',
success: function(response) {
window.location.href = '/admin/tests/';
},
error: function(response){
error_response(response);
},
});
} else if (action == 'edit') {
window.location.href = `/admin/test/${id}/`
}
event.preventDefault();
});
// Edit Dataset Button Handlers
$('.edit-question-dataset').click(function(event) {
var filename = $(this).data('filename');
var action = $(this).data('action');
var disabled = $(this).hasClass('disabled');
if ( !disabled ) {
$.ajax({
url: `/admin/settings/questions/${action}/`,
type: 'POST',
data: JSON.stringify({'filename': filename}),
contentType: 'application/json',
success: function(response) {
window.location.reload();
},
error: function(response){
error_response(response);
},
});
};
event.preventDefault();
});
function error_response(response) {
const $alert = $("#alert-box");
$alert.html('');
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
$alert.html(`
<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) {
var output = ''
for (var i = 0; i < response.responseJSON.error.length; i ++) {
output += `
<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>
`;
$alert.html(output);
}
}
$alert.focus()
}
// Dismiss Cookie Alert
$('#dismiss-cookie-alert').click(function(event){
$.ajax({
url: '/cookies/',
type: 'POST',
data: {
time: Date.now()
},
dataType: 'json',
success: function(response){
console.log(response);
},
error: function(response){
console.log(response);
}
})
event.preventDefault();
})
// Script for Result Actions
$('.result-action-buttons').click(function(event){
var id = $(this).data('id');
if ($(this).data('result-action') == 'generate') {
$.ajax({
url: '/admin/certificate/',
type: 'POST',
data: JSON.stringify({'id': id}),
contentType: 'application/json',
dataType: 'html',
success: function(response) {
var display_window = window.open();
display_window.document.write(response);
},
error: function(response){
error_response(response);
},
});
} else {
var action = $(this).data('result-action')
$.ajax({
url: window.location.href,
type: 'POST',
data: JSON.stringify({'id': id, 'action': action}),
contentType: 'application/json',
success: function(response) {
if (action == 'delete') {
window.location.href = '/admin/results/';
} else window.location.reload();
},
error: function(response){
error_response(response);
},
});
}
event.preventDefault();
});
// Script for Deleting Time Adjustment
$('.adjustment-delete').click(function(event){
var user_code = $(this).data('user_code');
var location = window.location.href;
location = location.replace('#', '')
$.ajax({
url: location + 'delete-adjustment/',
type: 'POST',
data: JSON.stringify({'user_code': user_code}),
contentType: 'application/json',
success: function(response) {
window.location.reload();
},
error: function(response){
error_response(response);
},
});
event.preventDefault();
});

View File

@ -0,0 +1,80 @@
<!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.1.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
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') }}"
/>
{% block style %}
{% endblock %}
<title>{% block title %} SKA Referee Test | Admin Console {% endblock %}</title>
{% include "editor/components/og-meta.html" %}
</head>
<body class="bg-light">
{% block navbar %}
{% include "editor/components/navbar.html" %}
{% endblock %}
<div class="container">
{% block top_alerts %}
{% include "editor/components/server-alerts.html" %}
{% endblock %}
{% block content %}{% endblock %}
</div>
<footer class="container site-footer mt-5">
{% block footer %}
{% include "editor/components/footer.html" %}
{% endblock %}
</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>
window.jQuery || document.write(`<script src="{{ url_for('.static', filename='js/jquery-3.6.0.min.js') }}"><\/script>`)
</script>
<script
src="https://cdn.jsdelivr.net/npm/@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">
var csrf_token = "{{ csrf_token() }}";
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrf_token);
}
}
});
</script>
<script
type="text/javascript"
src="{{ url_for('.static', filename='js/script.js') }}"
></script>
{% block script %}
{% endblock %}
</body>
</html>

View File

@ -0,0 +1 @@
<div id="alert-box" tabindex="-1"></div>

View File

@ -0,0 +1,28 @@
{% extends "editor/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"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/searchbuilder/1.3.0/css/searchBuilder.dataTables.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>
<script type="text/javascript" src="https://cdn.datatables.net/searchbuilder/1.3.0/js/dataTables.searchBuilder.min.js"></script>
{% endblock %}

View File

@ -0,0 +1,2 @@
<p>This web app was developed by Vivek Santayana. The source code for the web app, excluding any data pertaining to the questions in the quiz, is freely available at <a href="https://git.vsnt.uk/viveksantayana/ska-referee-test">Vivek&rsquo;s personal GIT repository</a> under an MIT License.</p>
<p>All questions in the test are &copy; The Scottish Korfball Association {{ now.year }}. All rights are reserved.</p>

View File

@ -0,0 +1,4 @@
{% extends "admin/components/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block top_alerts %}
{% endblock %}

View File

@ -0,0 +1,111 @@
<nav class="navbar fixed-top navbar-expand-md navbar-dark bg-dark">
<div class="container">
<a href="{{ url_for('admin._home') }}" class="navbar-brand mb-0 h1">RefTest (Beta) | Admin</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbar"
aria-controls="navbar"
aria-expanded="false"
aria-label="Toggle Navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse justify-content-end" id="navbar">
<ul class="navbar-nav">
{% if not current_user.is_authenticated %}
<li class="nav-item" id="nav-login">
<a href="{{ url_for('admin._login') }}" id="link-login" class="nav-link">Log In</a>
</li>
{% endif %}
{% if current_user.is_authenticated %}
<li class="nav-item" id="nav-results">
<a href="{{ url_for('admin._view_entries') }}" id="link-results" class="nav-link">View Results</a>
</li>
<li class="nav-item dropdown" id="nav-tests">
<a
class="nav-link dropdown-toggle"
id="dropdown-tests"
role="button"
href="{{ url_for('admin._tests') }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Exams
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-settings"
>
<li>
<a href="{{ url_for('admin._tests', filter='active') }}" id="link-active" class="dropdown-item">Active</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='scheduled') }}" id="link-scheduled" class="dropdown-item">Scheduled</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='expired') }}" id="link-expired" class="dropdown-item">Expired</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='all') }}" id="link-all" class="dropdown-item">All</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='create') }}" id="link-create" class="dropdown-item">Create</a>
</li>
</ul>
</li>
<li class="nav-item dropdown" id="nav-settings">
<a
class="nav-link dropdown-toggle"
id="dropdown-account"
role="button"
href="{{ url_for('admin._settings') }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Settings
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-settings"
>
<li>
<a href="{{ url_for('admin._settings') }}" id="link-settings" class="dropdown-item">View Settings</a>
</li>
<li>
<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>
</ul>
</li>
<li class="nav-item dropdown" id="nav-account">
<a
class="nav-link dropdown-toggle"
id="dropdown-account"
role="button"
href="{{ url_for('admin._update_user', id=current_user.id) }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Account
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-account"
>
<li>
<a href="{{ url_for('admin._update_user', id=current_user.id) }}" id="link-account" class="dropdown-item">Account Settings</a>
</li>
<li>
<a href="{{ url_for('admin._logout') }}" id="link-logout" class="dropdown-item">Log Out</a>
</li>
</ul>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>

View File

@ -0,0 +1,18 @@
<meta name="description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta property="og:locale" content="en_UK" />
<meta property="og:type" content="website" />
<meta property="og:description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta property="og:url" content="{{ url_for(request.endpoint, _external = True, **(request.view_args or {})) }}" />
<meta property="og:site_name" content="Scottish Korfball Association Referee Theory Exam" />
<meta property="og:image" content="{{ url_for('.static', filename='favicon.png', _external = True) }}" />
<meta property="og:image:alt" content="Logo of the SKA Refereeing Exam App" />
<meta property="og:image:width" content="512" />
<meta property="og:image:height" content="512" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta name="twitter:image" content="{{ url_for('.static', filename='favicon.png', _external = True) }}" />
<meta name="twitter:image:alt" content="Logo of the SKA Refereeing Exam App" />
<meta name="twitter:creator" content="@viveksantayana" />
<meta name="twitter:site" content="@viveksantayana" />
<meta name="theme-color" content="#343a40" />
<link rel="shortcut icon" href="{{ url_for('.static', filename='favicon.ico') }}">

View File

@ -0,0 +1,23 @@
<div class="navbar navbar-expand-sm navbar-light bg-light">
<div class="container-fluid">
<div class="expand navbar-expand justify-content-center" id="navbar_secondary">
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='active') }}">Active</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='scheduled') }}">Scheduled</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='expired') }}">Expired</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='all') }}">All</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='create') }}">Create</a>
</li>
</ul>
</div>
</div>
</div>

View File

@ -0,0 +1,43 @@
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% set cookie_flash_flag = namespace(value=False) %}
{% 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="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" 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" 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="Cookie Alert" aria-title="Cookie Alert"></i>
{{ message|safe }}
<div class="d-flex justify-content-center w-100">
<button type="button" id="dismiss-cookie-alert" class="btn btn-success" data-bs-dismiss="alert" aria-label="Close">Accept</button>
</div>
</div>
{% set cookie_flash_flag.value = True %}
{% endif %}
{% else %}
<div class="alert alert-primary alert-dismissible fade show" role="alert">
<i class="bi bi-info-circle-fill" title="Alert"></i>
{{ message|safe }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% endfor %}
{% endif %}
{% endwith %}

View File

@ -0,0 +1,148 @@
{% extends "editor/components/base.html" %}
{% block content %}
<h1>Dashboard</h1>
<div class="container">
<div class="row">
<div class="col-sm">
<div class="card m-3">
<div class="card-body">
<h5 class="card-title">Current Exams</h5>
{% if current_tests %}
<div class="card-text">
<table class="table table-striped">
<thead>
<tr>
<th>
Exam Code
</th>
<th>
Expiry Date
</th>
</tr>
</thead>
<tbody>
{% for test in current_tests %}
<tr>
<td>
<a href="{{ url_for('admin._view_test', id=test.id) }}">{{ test.get_code() }}</a>
</td>
<td>
{{ test.end_date.strftime('%d %b %Y') }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ url_for('admin._tests', filter='active') }}" class="btn btn-primary">View Exams</a>
{% else %}
<div class="alert alert-primary">
There are currently no active exams.
</div>
<a href="{{ url_for('admin._tests', filter='create') }}" class="btn btn-primary">Create Exam</a>
{% endif %}
</div>
</div>
</div>
<div class="col-sm">
<div class="card m-3">
<div class="card-body">
<h5 class="card-title">Recent Results</h5>
{% if recent_results %}
<div class="card-text">
<table class="table table-striped">
<thead>
<tr>
<th>
Name
</th>
<th>
Date Submitted
</th>
<th>
Result
</th>
</tr>
</thead>
<tbody>
{% for result in recent_results %}
<tr>
<td>
<a href="{{ url_for('admin._view_entry', id=result.id) }}">{{ result.get_surname() }}, {{ result.get_first_name() }}</a>
</td>
<td>
{{ result.end_time.strftime('%d %b %Y %H:%M') }}
</td>
<td>
{{ (100*result.result['score']/result.result['max'])|round|int }}&percnt; ({{ result.result.grade }})
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ url_for('admin._view_entries') }}" class="btn btn-primary">View Results</a>
{% else %}
<div class="alert alert-primary">
There are currently no exam results to preview.
</div>
{% endif %}
</div>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-sm">
<div class="card m-3">
<div class="card-body">
<h5 class="card-title">Upcoming Exams</h5>
{% if upcoming_tests %}
<div class="card-text">
<table class="table table-striped">
<thead>
<tr>
<th>
Exam Code
</th>
<th>
Expiry Date
</th>
</tr>
</thead>
<tbody>
{% for test in upcoming_tests %}
<tr>
<td>
<a href="{{ url_for('admin._view_test', id=test.id) }}">{{ test.get_code() }}</a>
</td>
<td>
{{ test.end_date.strftime('%d %b %Y') }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ url_for('admin._tests', filter='scheduled') }}" class="btn btn-primary">View Exams</a>
{% else %}
<div class="alert alert-primary">
There are currently no upcoming exams.
</div>
<a href="{{ url_for('admin._tests', filter='create') }}" class="btn btn-primary">Create Exam</a>
{% endif %}
</div>
</div>
</div>
<div class="col-sm">
<div class="card m-3">
<div class="card-body">
<h5 class="card-title">Help</h5>
<p class="card-text">This web app was developed by Vivek Santayana. If there are any issues with the app, any bugs you need to report, or any features you would like to request, please feel free to <a href="https://git.vsnt.uk/viveksantayana/ska-referee-test/issues">open an issue at the Git Repository</a>.</p>
<a href="https://git.vsnt.uk/viveksantayana/ska-referee-test/issues" class="btn btn-primary">Open an Issue</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,12 @@
from flask import Blueprint, render_template
editor = Blueprint(
name='editor',
import_name=__name__,
template_folder='templates',
static_folder='static'
)
@editor.route('/')
def _editor():
return render_template('/editor/index.html')

View File

@ -24,7 +24,6 @@ def install_app(app):
if not database_exists(database_uri): if not database_exists(database_uri):
create_database(database_uri) create_database(database_uri)
write('system.log', 'No database found. Creating a new database.') write('system.log', 'No database found. Creating a new database.')
from .models import Entry, Dataset, Test, User
db.create_all() db.create_all()
write('system.log', 'Creating database schema.') write('system.log', 'Creating database schema.')
if not path.isfile(f'./{data}/.encryption.key'): if not path.isfile(f'./{data}/.encryption.key'):