Finished data upload
Refactored to move security package inside common Moved data folder to process root.
This commit is contained in:
parent
3bde83cf92
commit
f361d25728
@ -77,3 +77,7 @@ Uses SQL rather than MongoDB.
|
|||||||
### Flask techniques
|
### Flask techniques
|
||||||
|
|
||||||
- [Create a `config.py` file](https://www.youtube.com/watch?v=GW_2O9CrnSU)
|
- [Create a `config.py` file](https://www.youtube.com/watch?v=GW_2O9CrnSU)
|
||||||
|
|
||||||
|
### Flask handling file uploads
|
||||||
|
|
||||||
|
- [Handlin File Uploads](https://blog.miguelgrinberg.com/post/handling-file-uploads-with-flask)
|
@ -3,7 +3,7 @@ from flask.helpers import flash, url_for
|
|||||||
from flask.json import jsonify
|
from flask.json import jsonify
|
||||||
from .models.users import User
|
from .models.users import User
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from security.database import decrypt_find_one, encrypted_update
|
from common.security.database import decrypt_find_one, encrypted_update
|
||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
|
|
||||||
from main import db
|
from main import db
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
|
from flask_wtf.file import FileField, FileRequired, FileAllowed
|
||||||
from wtforms import StringField, PasswordField, BooleanField, DateField, SelectField
|
from wtforms import StringField, PasswordField, BooleanField, DateField, SelectField
|
||||||
from wtforms.validators import InputRequired, Email, Length, EqualTo, Optional
|
from wtforms.validators import InputRequired, Email, Length, EqualTo, Optional
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
@ -54,3 +55,6 @@ class CreateTest(FlaskForm):
|
|||||||
]
|
]
|
||||||
expiry_date = DateField('Expiry Date', format="%Y-%m-%d", validators=[InputRequired()], default = date.today() + timedelta(days=1) )
|
expiry_date = DateField('Expiry Date', format="%Y-%m-%d", validators=[InputRequired()], default = date.today() + timedelta(days=1) )
|
||||||
time_limit = SelectField('Time Limit', choices=time_options)
|
time_limit = SelectField('Time Limit', choices=time_options)
|
||||||
|
|
||||||
|
class UploadDataForm(FlaskForm):
|
||||||
|
data_file = FileField('Data File', validators=[FileRequired(), FileAllowed(['json'])])
|
@ -5,7 +5,7 @@ from flask import flash, jsonify
|
|||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
from main import db
|
from main import db
|
||||||
from security import encrypt
|
from common.security import encrypt
|
||||||
|
|
||||||
class Test:
|
class Test:
|
||||||
def __init__(self, _id=None, start_date=None, expiry_date=None, time_limit=None, creator=None):
|
def __init__(self, _id=None, start_date=None, expiry_date=None, time_limit=None, creator=None):
|
||||||
|
@ -350,13 +350,18 @@ $('form[name=form-update-account]').submit(function(event) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.delete-test').click(function(event) {
|
$('form[name=form-create-test]').submit(function(event) {
|
||||||
|
|
||||||
_id = $(this).data('_id')
|
var $form = $(this);
|
||||||
|
var alert = document.getElementById('alert-box');
|
||||||
|
var data = $form.serialize();
|
||||||
|
alert.innerHTML = ''
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: `/admin/tests/delete/${_id}`,
|
url: window.location.pathname,
|
||||||
type: 'GET',
|
type: 'POST',
|
||||||
|
data: data,
|
||||||
|
dataType: 'json',
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
window.location.href = '/admin/tests/';
|
window.location.href = '/admin/tests/';
|
||||||
},
|
},
|
||||||
@ -386,20 +391,78 @@ $('.delete-test').click(function(event) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Edit and Delete Test Button Handlers
|
$('form[name=form-upload-questions]').submit(function(event) {
|
||||||
|
|
||||||
$('form[name=form-create-test]').submit(function(event) {
|
|
||||||
|
|
||||||
var $form = $(this);
|
var $form = $(this);
|
||||||
var alert = document.getElementById('alert-box');
|
var alert = document.getElementById('alert-box');
|
||||||
var data = $form.serialize();
|
var data = new FormData($form[0]);
|
||||||
|
var file = $('input[name=data_file]')[0].files[0]
|
||||||
|
data.append('file', file)
|
||||||
alert.innerHTML = ''
|
alert.innerHTML = ''
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: window.location.pathname,
|
url: window.location.pathname,
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
data: data,
|
data: data,
|
||||||
dataType: 'json',
|
processData: false,
|
||||||
|
contentType: false,
|
||||||
|
success: function(response) {
|
||||||
|
if (typeof response.success === 'string' || response.success instanceof String) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.success}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (response.success instanceof Array) {
|
||||||
|
for (var i = 0; i < response.success.length; i ++) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.success[i]}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(response) {
|
||||||
|
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (response.responseJSON.error instanceof Array) {
|
||||||
|
for (var i = 0; i < response.responseJSON.error.length; i ++) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error[i]}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Edit and Delete Test Button Handlers
|
||||||
|
|
||||||
|
$('.delete-test').click(function(event) {
|
||||||
|
|
||||||
|
_id = $(this).data('_id')
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: `/admin/tests/delete/${_id}`,
|
||||||
|
type: 'GET',
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
window.location.href = '/admin/tests/';
|
window.location.href = '/admin/tests/';
|
||||||
},
|
},
|
||||||
|
@ -1 +1,23 @@
|
|||||||
{% extends "admin/components/base.html" %}
|
{% extends "admin/components/base.html" %}
|
||||||
|
{% block title %} SKA Referee Test | Upload Questions {% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<!-- <h1>Upload Question Dataset</h1> -->
|
||||||
|
<div class="form-container">
|
||||||
|
<form name="form-upload-questions" method="post" action="#" class="form-signin" enctype="multipart/form-data">
|
||||||
|
<h2 class="form-signin-heading">Upload Question Dataset</h2>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{{ form.data_file() }}
|
||||||
|
{% include "admin/components/client-alerts.html" %}
|
||||||
|
<div class="container form-submission-button">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col text-center">
|
||||||
|
<button title="Create User" class="btn btn-md btn-success btn-block" type="submit">
|
||||||
|
<i class="bi bi-file-earmark-arrow-up-fill button-icon"></i>
|
||||||
|
Upload Dataset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -1,9 +1,10 @@
|
|||||||
from flask import Blueprint, render_template, flash, redirect, request, jsonify, abort
|
from flask import Blueprint, render_template, flash, redirect, request, jsonify, abort
|
||||||
from flask.helpers import url_for
|
from flask.helpers import url_for
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
from security.database import decrypt_find, decrypt_find_one
|
from common.security.database import decrypt_find, decrypt_find_one
|
||||||
from .models.users import User
|
from .models.users import User
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
from main import db
|
from main import db
|
||||||
@ -231,11 +232,27 @@ def update_user(_id:str):
|
|||||||
errors = [*form.user_password.errors, *form.email.errors, *form.password.errors, *form.password_reenter.errors]
|
errors = [*form.user_password.errors, *form.email.errors, *form.password.errors, *form.password_reenter.errors]
|
||||||
return jsonify({ 'error': errors}), 400
|
return jsonify({ 'error': errors}), 400
|
||||||
|
|
||||||
@views.route('/settings/questions/')
|
@views.route('/settings/questions/', methods=['GET', 'POST'])
|
||||||
@admin_account_required
|
@admin_account_required
|
||||||
@login_required
|
@login_required
|
||||||
def questions():
|
def questions():
|
||||||
return render_template('/admin/settings/questions.html')
|
from main import app
|
||||||
|
from .models.forms import UploadDataForm
|
||||||
|
from common.data_tools import check_json_format, validate_json_contents, store_data_file
|
||||||
|
form = UploadDataForm()
|
||||||
|
if request.method == 'GET':
|
||||||
|
return render_template('/admin/settings/questions.html', form=form)
|
||||||
|
if request.method == 'POST':
|
||||||
|
if form.validate_on_submit():
|
||||||
|
upload = form.data_file.data
|
||||||
|
if not check_json_format(upload):
|
||||||
|
return jsonify({ 'error': 'Invalid file selected. Please upload a JSON file.'}), 400
|
||||||
|
if not validate_json_contents(upload):
|
||||||
|
return jsonify({'error': 'The data in the file is invalid.'}), 400
|
||||||
|
store_data_file(upload)
|
||||||
|
return jsonify({ 'success': 'File uploaded.'}), 200
|
||||||
|
errors = [*form.errors]
|
||||||
|
return jsonify({ 'error': errors}), 400
|
||||||
|
|
||||||
@views.route('/settings/questions/upload/')
|
@views.route('/settings/questions/upload/')
|
||||||
@admin_account_required
|
@admin_account_required
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
from datetime import datetime, timedelta
|
|
||||||
from flask import Blueprint, redirect, request
|
|
||||||
|
|
||||||
cookie_consent = Blueprint(
|
|
||||||
'cookie_consent',
|
|
||||||
__name__
|
|
||||||
)
|
|
||||||
@cookie_consent.route('/')
|
|
||||||
def _cookies():
|
|
||||||
resp = redirect('/')
|
|
||||||
resp.set_cookie(
|
|
||||||
key = 'cookie_consent',
|
|
||||||
value = 'True',
|
|
||||||
max_age = timedelta(days=14) if request.cookies.get('remember') == 'True' else 'Session',
|
|
||||||
path = '/',
|
|
||||||
expires = datetime.utcnow() + timedelta(days=14) if request.cookies.get('remember') else 'Session'
|
|
||||||
)
|
|
||||||
return resp
|
|
18
ref-test/common/blueprints.py
Normal file
18
ref-test/common/blueprints.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from flask import Blueprint, redirect, request
|
||||||
|
|
||||||
|
cookie_consent = Blueprint(
|
||||||
|
'cookie_consent',
|
||||||
|
__name__
|
||||||
|
)
|
||||||
|
@cookie_consent.route('/')
|
||||||
|
def _cookies():
|
||||||
|
resp = redirect('/')
|
||||||
|
resp.set_cookie(
|
||||||
|
key = 'cookie_consent',
|
||||||
|
value = 'True',
|
||||||
|
max_age = timedelta(days=14) if request.cookies.get('remember') == 'True' else 'Session',
|
||||||
|
path = '/',
|
||||||
|
expires = datetime.utcnow() + timedelta(days=14) if request.cookies.get('remember') else 'Session'
|
||||||
|
)
|
||||||
|
return resp
|
59
ref-test/common/data_tools.py
Normal file
59
ref-test/common/data_tools.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import os
|
||||||
|
from shutil import rmtree
|
||||||
|
import pathlib
|
||||||
|
from json import dump, loads
|
||||||
|
from datetime import datetime
|
||||||
|
from main import app
|
||||||
|
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
|
def check_data_folder_exists():
|
||||||
|
if not os.path.exists(app.config['DATA_FILE_DIRECTORY']):
|
||||||
|
pathlib.Path(app.config['DATA_FILE_DIRECTORY']).mkdir(parents='True', exist_ok='True')
|
||||||
|
|
||||||
|
def check_current_indicator():
|
||||||
|
if not os.path.isfile(os.path.join(app.config['DATA_FILE_DIRECTORY'], '.current.txt')):
|
||||||
|
open(os.path.join(app.config['DATA_FILE_DIRECTORY'], '.current.txt'),'w').close()
|
||||||
|
|
||||||
|
def make_temp_dir(file):
|
||||||
|
if not os.path.isdir('tmp'):
|
||||||
|
os.mkdir('tmp')
|
||||||
|
if os.path.isfile(f'tmp/{file.filename}'):
|
||||||
|
os.remove(f'tmp/{file.filename}')
|
||||||
|
file.save(f'tmp/{file.filename}')
|
||||||
|
|
||||||
|
def check_json_format(file):
|
||||||
|
if not '.' in file.filename:
|
||||||
|
return False
|
||||||
|
if not file.filename.rsplit('.', 1)[-1] == 'json':
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def validate_json_contents(file):
|
||||||
|
file.stream.seek(0)
|
||||||
|
data = loads(file.read())
|
||||||
|
if not type(data) is dict:
|
||||||
|
return False
|
||||||
|
elif not all( key in data for key in ['meta', 'questions']):
|
||||||
|
return False
|
||||||
|
elif not type(data['meta']) is dict:
|
||||||
|
return False
|
||||||
|
elif not type(data['questions']) is list:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def store_data_file(file):
|
||||||
|
from admin.views import get_id_from_cookie
|
||||||
|
check_current_indicator()
|
||||||
|
timestamp = datetime.utcnow()
|
||||||
|
filename = '.'.join([timestamp.strftime('%Y%m%d%H%M%S'),'json'])
|
||||||
|
filename = secure_filename(filename)
|
||||||
|
file_path = os.path.join(app.config['DATA_FILE_DIRECTORY'], filename)
|
||||||
|
file.stream.seek(0)
|
||||||
|
data = loads(file.read())
|
||||||
|
data['meta']['timestamp'] = timestamp.strftime('%Y-%m-%d %H%M%S')
|
||||||
|
data['meta']['author'] = get_id_from_cookie()
|
||||||
|
with open(file_path, 'w') as _file:
|
||||||
|
dump(data, _file, indent=4)
|
||||||
|
with open(os.path.join(app.config['DATA_FILE_DIRECTORY'], '.current.txt'), 'w') as _file:
|
||||||
|
_file.write(filename)
|
@ -2,17 +2,17 @@ from os import environ, path
|
|||||||
from cryptography.fernet import Fernet
|
from cryptography.fernet import Fernet
|
||||||
|
|
||||||
def generate_keyfile():
|
def generate_keyfile():
|
||||||
with open('./security/.encryption.key', 'wb') as keyfile:
|
with open('./common/security/.encryption.key', 'wb') as keyfile:
|
||||||
key = Fernet.generate_key()
|
key = Fernet.generate_key()
|
||||||
keyfile.write(key)
|
keyfile.write(key)
|
||||||
|
|
||||||
def load_key():
|
def load_key():
|
||||||
with open('./security/.encryption.key', 'rb') as keyfile:
|
with open('./common/security/.encryption.key', 'rb') as keyfile:
|
||||||
key = keyfile.read()
|
key = keyfile.read()
|
||||||
return key
|
return key
|
||||||
|
|
||||||
def check_keyfile_exists():
|
def check_keyfile_exists():
|
||||||
return path.isfile('./security/.encryption.key')
|
return path.isfile('./common/security/.encryption.key')
|
||||||
|
|
||||||
def encrypt(input):
|
def encrypt(input):
|
||||||
if not check_keyfile_exists():
|
if not check_keyfile_exists():
|
@ -26,6 +26,7 @@ class Config(object):
|
|||||||
MAIL_MAX_EMAILS = int(os.getenv("MAIL_MAX_EMAILS"))
|
MAIL_MAX_EMAILS = int(os.getenv("MAIL_MAX_EMAILS"))
|
||||||
MAIL_SUPPRESS_SEND = False
|
MAIL_SUPPRESS_SEND = False
|
||||||
MAIL_ASCII_ATTACHMENTS = bool(os.getenv("MAIL_ASCII_ATTACHMENTS"))
|
MAIL_ASCII_ATTACHMENTS = bool(os.getenv("MAIL_ASCII_ATTACHMENTS"))
|
||||||
|
DATA_FILE_DIRECTORY = os.getenv("DATA_FILE_DIRECTORY")
|
||||||
|
|
||||||
class ProductionConfig(Config):
|
class ProductionConfig(Config):
|
||||||
pass
|
pass
|
||||||
|
2
ref-test/data/.gitignore
vendored
Normal file
2
ref-test/data/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
@ -9,7 +9,7 @@ from pymongo.errors import ConnectionFailure
|
|||||||
from flask_wtf.csrf import CSRFProtect, CSRFError
|
from flask_wtf.csrf import CSRFProtect, CSRFError
|
||||||
from flask_mail import Mail
|
from flask_mail import Mail
|
||||||
|
|
||||||
from security import check_keyfile_exists, generate_keyfile
|
from common.security import check_keyfile_exists, generate_keyfile
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_object('config.DevelopmentConfig')
|
app.config.from_object('config.DevelopmentConfig')
|
||||||
@ -37,7 +37,7 @@ if __name__ == '__main__':
|
|||||||
if not check_keyfile_exists():
|
if not check_keyfile_exists():
|
||||||
generate_keyfile()
|
generate_keyfile()
|
||||||
|
|
||||||
from common import cookie_consent
|
from common.blueprints import cookie_consent
|
||||||
|
|
||||||
from admin.views import views as admin_views
|
from admin.views import views as admin_views
|
||||||
from admin.auth import auth as admin_auth
|
from admin.auth import auth as admin_auth
|
||||||
|
@ -3,7 +3,7 @@ from datetime import datetime
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from main import db
|
from main import db
|
||||||
from security import encrypt
|
from common.security import encrypt
|
||||||
|
|
||||||
views = Blueprint(
|
views = Blueprint(
|
||||||
'quiz_views',
|
'quiz_views',
|
||||||
|
Loading…
Reference in New Issue
Block a user