Finished making dashboards
This commit is contained in:
parent
488389057c
commit
66d8fb7d93
@ -42,10 +42,10 @@
|
|||||||
aria-labelledby="dropdown-account"
|
aria-labelledby="dropdown-account"
|
||||||
>
|
>
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for('admin_views.users') }}" id="link-users" class="dropdown-item">Manage Users</a>
|
<a href="{{ url_for('admin_views.users') }}" id="link-users" class="dropdown-item">Users</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for('admin_views.questions') }}" id="link-questions" class="dropdown-item">Manage Questions</a>
|
<a href="{{ url_for('admin_views.questions') }}" id="link-questions" class="dropdown-item">Question Datasets</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
@ -2,4 +2,147 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Dashboard</h1>
|
<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_views.view_test', _id=test._id) }}">{{ '—'.join([test.test_code[:4], test.test_code[4:8], test.test_code[8:]]) }}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ test.expiry_date.strftime('%d %b %Y') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('admin_views.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_views.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_views.view_entry', _id=result._id) }}">{{ result.name.surname }}, {{ result.name.first_name }}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ result.submission_time.strftime('%d %b %Y %H:%M') }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ result.percent }}% ({{ result.results.grade }})
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('admin_views.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_views.view_test', _id=test._id) }}">{{ '—'.join([test.test_code[:4], test.test_code[4:8], test.test_code[8:]]) }}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ test.expiry_date.strftime('%d %b %Y') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('admin_views.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_views.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 anything you are unsure of, you can get in touch with Vivek via email.</p>
|
||||||
|
<a href="mailto:me@viveksantayana.co.uk" class="btn btn-primary">Email</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -1 +1,93 @@
|
|||||||
{% extends "admin/components/base.html" %}
|
{% extends "admin/components/base.html" %}
|
||||||
|
{% block title %}Settings — SKA Referee Test | Admin Console{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>
|
||||||
|
Settings
|
||||||
|
</h1>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="card m-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Admin Users</h5>
|
||||||
|
<div class="card-text">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
Username
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Email Address
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in users %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="
|
||||||
|
{% if user._id == get_id_from_cookie() %}
|
||||||
|
{{ url_for('admin_auth.account') }}
|
||||||
|
{% else %}
|
||||||
|
{{ url_for('admin_views.update_user', _id=user._id) }}
|
||||||
|
{% endif%}
|
||||||
|
">{{ user.username }}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="mailto:{{ user.email }}">{{ user.email }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('admin_views.users') }}" class="btn btn-primary">Manage Users</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="card m-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Question Datasets</h5>
|
||||||
|
{% if datasets %}
|
||||||
|
<div class="card-text">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
File Name
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Exams
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for dataset in datasets %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{ dataset.filename }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ dataset.use }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('admin_views.questions') }}" class="btn btn-primary">Manage Datasets</a>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-primary">
|
||||||
|
There are currently no question datasets uploaded.
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('admin_views.questions') }}" class="btn btn-primary">Upload Dataset</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -16,7 +16,7 @@ import secrets
|
|||||||
from main import mail
|
from main import mail
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
from .models.tests import Test
|
from .models.tests import Test
|
||||||
from common.data_tools import get_default_dataset, get_time_options, available_datasets
|
from common.data_tools import get_default_dataset, get_time_options, available_datasets, get_datasets
|
||||||
|
|
||||||
views = Blueprint(
|
views = Blueprint(
|
||||||
'admin_views',
|
'admin_views',
|
||||||
@ -76,13 +76,26 @@ def disable_if_logged_in(function):
|
|||||||
@admin_account_required
|
@admin_account_required
|
||||||
@login_required
|
@login_required
|
||||||
def home():
|
def home():
|
||||||
return render_template('/admin/index.html')
|
tests = db.tests.find()
|
||||||
|
results = decrypt_find(db.entries, {})
|
||||||
|
current_tests = [ test for test in tests if test['expiry_date'].date() >= date.today() and test['start_date'].date() <= date.today() ]
|
||||||
|
current_tests.sort(key= lambda x: x['expiry_date'], reverse=True)
|
||||||
|
upcoming_tests = [ test for test in tests if test['start_date'].date() > date.today()]
|
||||||
|
upcoming_tests.sort(key= lambda x: x['start_date'])
|
||||||
|
recent_results = [result for result in results if 'submission_time' in result ]
|
||||||
|
recent_results.sort(key= lambda x: x['submission_time'], reverse=True)
|
||||||
|
for result in recent_results:
|
||||||
|
result['percent'] = round(100*result['results']['score']/result['results']['max'])
|
||||||
|
return render_template('/admin/index.html', current_tests = current_tests[:5], upcomimg_tests = upcoming_tests[:5], recent_results = recent_results[:5])
|
||||||
|
|
||||||
@views.route('/settings/')
|
@views.route('/settings/')
|
||||||
@admin_account_required
|
@admin_account_required
|
||||||
@login_required
|
@login_required
|
||||||
def settings():
|
def settings():
|
||||||
return render_template('/admin/settings/index.html')
|
users = decrypt_find(db.users, {})
|
||||||
|
users.sort(key= lambda x: x['username'])
|
||||||
|
datasets = get_datasets()
|
||||||
|
return render_template('/admin/settings/index.html', users=users[:5], datasets=datasets[:5])
|
||||||
|
|
||||||
@views.route('/settings/users/', methods=['GET','POST'])
|
@views.route('/settings/users/', methods=['GET','POST'])
|
||||||
@admin_account_required
|
@admin_account_required
|
||||||
@ -245,22 +258,7 @@ def questions():
|
|||||||
from common.data_tools import check_json_format, validate_json_contents, store_data_file
|
from common.data_tools import check_json_format, validate_json_contents, store_data_file
|
||||||
form = UploadDataForm()
|
form = UploadDataForm()
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
files = glob(os.path.join(app.config["DATA_FILE_DIRECTORY"],'*.json'))
|
data = get_datasets()
|
||||||
data = []
|
|
||||||
if files:
|
|
||||||
for file in files:
|
|
||||||
filename = file.rsplit('/')[-1]
|
|
||||||
with open(file) as _file:
|
|
||||||
load = loads(_file.read())
|
|
||||||
_author = load['meta']['author']
|
|
||||||
author = decrypt_find_one(db.users, {'_id': _author})['username']
|
|
||||||
data_element = {
|
|
||||||
'filename': filename,
|
|
||||||
'timestamp': datetime.strptime(load['meta']['timestamp'], '%Y-%m-%d %H%M%S'),
|
|
||||||
'author': author,
|
|
||||||
'use': len(load['meta']['tests'])
|
|
||||||
}
|
|
||||||
data.append(data_element)
|
|
||||||
default = get_default_dataset()
|
default = get_default_dataset()
|
||||||
return render_template('/admin/settings/questions.html', form=form, data=data, default=default)
|
return render_template('/admin/settings/questions.html', form=form, data=data, default=default)
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
@ -3,13 +3,12 @@ import pathlib
|
|||||||
from json import dump, loads
|
from json import dump, loads
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from glob import glob
|
from glob import glob
|
||||||
|
|
||||||
from flask.json import jsonify
|
|
||||||
from main import app
|
|
||||||
from random import shuffle
|
from random import shuffle
|
||||||
|
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
|
from main import app, db
|
||||||
|
from .security.database import decrypt_find_one
|
||||||
|
|
||||||
def check_data_folder_exists():
|
def check_data_folder_exists():
|
||||||
if not os.path.exists(app.config['DATA_FILE_DIRECTORY']):
|
if not os.path.exists(app.config['DATA_FILE_DIRECTORY']):
|
||||||
pathlib.Path(app.config['DATA_FILE_DIRECTORY']).mkdir(parents='True', exist_ok='True')
|
pathlib.Path(app.config['DATA_FILE_DIRECTORY']).mkdir(parents='True', exist_ok='True')
|
||||||
@ -199,4 +198,23 @@ def get_time_options():
|
|||||||
('90', '1 hour 30 minutes'),
|
('90', '1 hour 30 minutes'),
|
||||||
('120', '2 hours')
|
('120', '2 hours')
|
||||||
]
|
]
|
||||||
return time_options
|
return time_options
|
||||||
|
|
||||||
|
def get_datasets():
|
||||||
|
files = glob(os.path.join(app.config["DATA_FILE_DIRECTORY"],'*.json'))
|
||||||
|
data = []
|
||||||
|
if files:
|
||||||
|
for file in files:
|
||||||
|
filename = file.rsplit('/')[-1]
|
||||||
|
with open(file) as _file:
|
||||||
|
load = loads(_file.read())
|
||||||
|
_author = load['meta']['author']
|
||||||
|
author = decrypt_find_one(db.users, {'_id': _author})['username']
|
||||||
|
data_element = {
|
||||||
|
'filename': filename,
|
||||||
|
'timestamp': datetime.strptime(load['meta']['timestamp'], '%Y-%m-%d %H%M%S'),
|
||||||
|
'author': author,
|
||||||
|
'use': len(load['meta']['tests'])
|
||||||
|
}
|
||||||
|
data.append(data_element)
|
||||||
|
return data
|
Loading…
Reference in New Issue
Block a user