From 39e80c64fa585732cd8242d76237cdc7b269bc02 Mon Sep 17 00:00:00 2001 From: viveksantayana Date: Thu, 25 Nov 2021 23:21:48 +0000 Subject: [PATCH 001/250] Refactor to have all models in the models package. --- ref-test/admin/auth.py | 12 +- ref-test/admin/{user => models}/__init__.py | 0 ref-test/admin/{ => models}/forms.py | 0 ref-test/admin/{models.py => models/tests.py} | 0 ref-test/admin/user/models.py | 185 ------------------ ref-test/admin/views.py | 14 +- 6 files changed, 13 insertions(+), 198 deletions(-) rename ref-test/admin/{user => models}/__init__.py (100%) rename ref-test/admin/{ => models}/forms.py (100%) rename ref-test/admin/{models.py => models/tests.py} (100%) delete mode 100644 ref-test/admin/user/models.py diff --git a/ref-test/admin/auth.py b/ref-test/admin/auth.py index fd9e854..2c6e259 100644 --- a/ref-test/admin/auth.py +++ b/ref-test/admin/auth.py @@ -1,7 +1,7 @@ from flask import Blueprint, render_template, request, session, redirect from flask.helpers import flash, url_for from flask.json import jsonify -from .user.models import User +from .models.users import User from uuid import uuid4 from security.database import decrypt_find_one, encrypted_update from werkzeug.security import check_password_hash @@ -21,7 +21,7 @@ auth = Blueprint( @admin_account_required @login_required def account(): - from .forms import UpdateAccountForm + from .models.forms import UpdateAccountForm form = UpdateAccountForm() _id = get_id_from_cookie() user = decrypt_find_one(db.users, {'_id': _id}) @@ -46,7 +46,7 @@ def account(): @admin_account_required @disable_if_logged_in def login(): - from .forms import LoginForm + from .models.forms import LoginForm form = LoginForm() if request.method == 'GET': return render_template('/admin/auth/login.html', form=form) @@ -72,7 +72,7 @@ def logout(): @auth.route('/register/', methods=['GET','POST']) @disable_on_registration def register(): - from .forms import RegistrationForm + from .models.forms import RegistrationForm form = RegistrationForm() if request.method == 'GET': return render_template('/admin/auth/register.html', form=form) @@ -93,7 +93,7 @@ def register(): @admin_account_required @disable_if_logged_in def reset(): - from .forms import ResetPasswordForm + from .models.forms import ResetPasswordForm form = ResetPasswordForm() if request.method == 'GET': return render_template('/admin/auth/reset.html', form=form) @@ -128,7 +128,7 @@ def reset_gateway(token1,token2): @admin_account_required @disable_if_logged_in def update_password_(): - from .forms import UpdatePasswordForm + from .models.forms import UpdatePasswordForm form = UpdatePasswordForm() if request.method == 'GET': if 'reset_validated' not in session: diff --git a/ref-test/admin/user/__init__.py b/ref-test/admin/models/__init__.py similarity index 100% rename from ref-test/admin/user/__init__.py rename to ref-test/admin/models/__init__.py diff --git a/ref-test/admin/forms.py b/ref-test/admin/models/forms.py similarity index 100% rename from ref-test/admin/forms.py rename to ref-test/admin/models/forms.py diff --git a/ref-test/admin/models.py b/ref-test/admin/models/tests.py similarity index 100% rename from ref-test/admin/models.py rename to ref-test/admin/models/tests.py diff --git a/ref-test/admin/user/models.py b/ref-test/admin/user/models.py deleted file mode 100644 index a983975..0000000 --- a/ref-test/admin/user/models.py +++ /dev/null @@ -1,185 +0,0 @@ -from flask import flash, make_response, Response -from flask.helpers import url_for -from flask.json import jsonify -from werkzeug.security import generate_password_hash, check_password_hash -from werkzeug.utils import redirect -from flask_mail import Message -import secrets - -from security import encrypt, decrypt -from security.database import decrypt_find_one, encrypted_update -from datetime import datetime, timedelta -from main import db, mail - -class User: - - def __init__(self, _id=None, username=None, password=None, email=None, remember=False): - self._id = _id - self.username = username - self.email = email - self.password = password - self.remember = remember - - def start_session(self, resp:Response): - resp.set_cookie( - key = '_id', - value = self._id, - max_age = timedelta(days=14) if self.remember else 'Session', - path = '/', - expires = datetime.utcnow() + timedelta(days=14) if self.remember else 'Session' - ) - if self.remember: - resp.set_cookie ( - key = 'remember', - value = 'True', - max_age = timedelta(days=14), - path = '/', - expires = datetime.utcnow() + timedelta(days=14) - ) - - def register(self): - from ..views import get_id_from_cookie - user = { - '_id': self._id, - 'email': encrypt(self.email), - 'password': generate_password_hash(self.password, method='sha256'), - 'username': encrypt(self.username) - } - if decrypt_find_one(db.users, { 'username': self.username }): - return jsonify({ 'error': f'Username {self.username} is not available.' }), 400 - if db.users.insert_one(user): - flash(f'User {self.username} has been created successfully. You may now use it to log in and administer the tests.', 'success') - resp = make_response(jsonify(user), 200) - if not get_id_from_cookie: - self.start_session(resp) - return resp - return jsonify({ 'error': f'Registration failed. An error occurred.' }), 400 - - def login(self): - user = decrypt_find_one( db.users, { 'username': self.username }) - if not user: - return jsonify({ 'error': f'Username {self.username} does not exist.' }), 401 - if not check_password_hash( user['password'], self.password ): - return jsonify({ 'error': f'The password you entered is incorrect.' }), 401 - resp = make_response(jsonify({ 'success': f'Successfully logged in user {self.username}.' }), 200) - self._id = user['_id'] - self.start_session(resp) - return resp - - def logout(self): - resp = make_response(redirect(url_for('admin_auth.login'))) - resp.set_cookie( - key = '_id', - value = '', - max_age = timedelta(days=-1), - path = '/', - expires= datetime.utcnow() + timedelta(days=-1) - ) - resp.set_cookie ( - key = 'cookie_consent', - value = 'True', - max_age = 'Session', - path = '/', - expires = 'Session' - ) - resp.set_cookie ( - key = 'remember', - value = 'True', - max_age = timedelta(days=-1), - path = '/', - expires = datetime.utcnow() + timedelta(days=-1) - ) - flash('You have been logged out. All cookies pertaining to your account have been deleted. Have a nice day.', 'alert') - return resp - - def reset_password(self): - user = decrypt_find_one(db.users, { 'username': self.username }) - if not user: - return jsonify({ 'error': f'Username {self.username} does not exist.' }), 401 - if not decrypt(user['email']) == self.email: - return jsonify({ 'error': f'The email address {self.email} does not match the user account {self.username}.' }), 401 - new_password = secrets.token_hex(12) - reset_token = secrets.token_urlsafe(16) - verification_token = secrets.token_urlsafe(16) - user['password'] = generate_password_hash(new_password, method='sha256') - if encrypted_update( { 'username': self.username }, { '$set': {'password': user['password'], 'reset_token': reset_token, 'verification_token': verification_token } } ): - flash(f'Your password has been reset. Instructions to recover your account have been sent to {self.email}. Please be sure to check your spam folder in case you have not received the email.', 'alert') - email = Message( - subject = 'RefTest | Password Reset', - recipients = [self.email], - body = f""" - Hello {user['username']}, \n\n - This email was generated because we received a request to reset the password for your administrator account for the SKA RefTest app.\n\n - If you did not make this request, please ignore this email.\n\n - If you did make this request, then you have two options to recover your account.\n\n - For the time being, your password has been reset to the following:\n\n - {new_password}\n\n - You may use this to log back in to your account, and subsequently change your password to something more suitable.\n\n - Alternatively, you may visit the following private link using your unique token to override your password. Copy and paste the following link in a web browser. Please note that this token is only valid once:\n\n - {url_for('admin_auth.reset_gateway', token1 = reset_token, token2 = verification_token, _external = True)}\n\n - Have a nice day. - """, - html = f""" -

Hello {user['username']},

-

This email was generated because we received a request to reset the password for your administrator account for the SKA RefTest app.

-

If you did not make this request, please ignore this email.

-

If you did make this request, then you have two options to recover your account.

-

For the time being, your password has been reset to the following:

- {new_password} -

You may use this to log back in to your account, and subsequently change your password to something more suitable.

-

Alternatively, you may visit the following private link using your unique token to override your password. Click on the following link, or copy and paste it into a browser. Please note that this token is only valid once:

-

{url_for('admin_auth.reset_gateway', token1 = reset_token, token2 = verification_token, _external = True)}

-

Have a nice day.

- """ - ) - mail.send(email) - return jsonify({ 'success': 'Password reset request has been processed.'}), 200 - - def update(self): - from ..views import get_id_from_cookie - retrieved_user = decrypt_find_one(db.users, { '_id': self._id }) - if not retrieved_user: - return jsonify({ 'error': f'User {retrieved_user["username"]} does not exist.' }), 401 - user = {} - updated = [] - if not self.email == '' and self.email is not None: - user['email'] = self.email - updated.append('email') - if not self.password == '' and self.password is not None: - user['password'] = generate_password_hash(self.password, method='sha256') - updated.append('password') - output = '' - if len(updated) == 0: - flash(f'There were no changes requested for your account.', 'alert'), 200 - return jsonify({'success': 'There were no changes requested for your account.'}), 200 - elif len(updated) == 1: - output = updated[0] - elif len(updated) == 2: - output = ' and '.join(updated) - elif len(updated) > 2: - output = updated[0] - for index in range(1,len(updated)): - if index < len(updated) - 2: - output = ', '.join([output, updated[index]]) - elif index == len(updated) - 2: - output = ', and '.join([output, updated[index]]) - else: - output = ''.join([output, updated[index]]) - encrypted_update(db.users, {'_id': self._id}, { '$set': user }) - if self._id == get_id_from_cookie(): - _output = 'Your ' - elif retrieved_user['username'][-1] == 's': - _output = '’'.join([retrieved_user['username'], '']) - else: - _output = '’'.join([retrieved_user['username'], 's']) - _output = f'{_output} {output} {"has" if len(updated) == 1 else "have"} been updated.' - flash(_output) - return jsonify({'success': _output}), 200 - - def delete(self): - retrieved_user = decrypt_find_one(db.users, { '_id': self._id }) - if not retrieved_user: - return jsonify({ 'error': f'User does not exist.' }), 401 - db.users.find_one_and_delete({'_id': self._id}) - flash(f'User {retrieved_user["username"]} has been deleted.') - return jsonify({'success': f'User {retrieved_user["username"]} has been deleted.'}), 200 diff --git a/ref-test/admin/views.py b/ref-test/admin/views.py index c6f4fc2..6bc4ce4 100644 --- a/ref-test/admin/views.py +++ b/ref-test/admin/views.py @@ -4,14 +4,14 @@ from functools import wraps from werkzeug.security import check_password_hash from security.database import decrypt_find, decrypt_find_one -from .user.models import User +from .models.users import User from flask_mail import Message from main import db from uuid import uuid4 import secrets from main import mail from datetime import datetime, date, timedelta -from .models import Test +from .models.tests import Test views = Blueprint( 'admin_views', @@ -82,7 +82,7 @@ def settings(): @admin_account_required @login_required def users(): - from .forms import CreateUserForm + from .models.forms import CreateUserForm form = CreateUserForm() if request.method == 'GET': users_list = decrypt_find(db.users, {}) @@ -132,7 +132,7 @@ def delete_user(_id:str): if _id == get_id_from_cookie(): flash('Cannot delete your own user account.', 'error') return redirect(url_for('admin_views.users')) - from .forms import DeleteUserForm + from .models.forms import DeleteUserForm form = DeleteUserForm() user = decrypt_find_one(db.users, {'_id': _id}) if request.method == 'GET': @@ -181,7 +181,7 @@ def update_user(_id:str): if _id == get_id_from_cookie(): flash('Cannot delete your own user account.', 'error') return redirect(url_for('admin_views.users')) - from .forms import UpdateUserForm + from .models.forms import UpdateUserForm form = UpdateUserForm() user = decrypt_find_one( db.users, {'_id': _id}) if request.method == 'GET': @@ -251,7 +251,7 @@ def tests(filter=''): if filter not in ['', 'create', 'active', 'scheduled', 'expired', 'all']: return abort(404) if filter == 'create': - from .forms import CreateTest + from .models.forms import CreateTest form = CreateTest() form.time_limit.default='none' form.process() @@ -281,7 +281,7 @@ def tests(filter=''): @admin_account_required @login_required def _tests(): - from .forms import CreateTest + from .models.forms import CreateTest form = CreateTest() form.time_limit.default='none' form.process() From 5b2e6dda67cb11ed573bd6619f0da05b12d95f0f Mon Sep 17 00:00:00 2001 From: viveksantayana Date: Thu, 25 Nov 2021 23:21:48 +0000 Subject: [PATCH 002/250] Refactor to have all models in the models package. --- ref-test/admin/auth.py | 12 +- ref-test/admin/{user => models}/__init__.py | 0 ref-test/admin/{ => models}/forms.py | 0 ref-test/admin/{models.py => models/tests.py} | 0 ref-test/admin/user/models.py | 185 ------------------ ref-test/admin/views.py | 14 +- 6 files changed, 13 insertions(+), 198 deletions(-) rename ref-test/admin/{user => models}/__init__.py (100%) rename ref-test/admin/{ => models}/forms.py (100%) rename ref-test/admin/{models.py => models/tests.py} (100%) delete mode 100644 ref-test/admin/user/models.py diff --git a/ref-test/admin/auth.py b/ref-test/admin/auth.py index fd9e854..2c6e259 100644 --- a/ref-test/admin/auth.py +++ b/ref-test/admin/auth.py @@ -1,7 +1,7 @@ from flask import Blueprint, render_template, request, session, redirect from flask.helpers import flash, url_for from flask.json import jsonify -from .user.models import User +from .models.users import User from uuid import uuid4 from security.database import decrypt_find_one, encrypted_update from werkzeug.security import check_password_hash @@ -21,7 +21,7 @@ auth = Blueprint( @admin_account_required @login_required def account(): - from .forms import UpdateAccountForm + from .models.forms import UpdateAccountForm form = UpdateAccountForm() _id = get_id_from_cookie() user = decrypt_find_one(db.users, {'_id': _id}) @@ -46,7 +46,7 @@ def account(): @admin_account_required @disable_if_logged_in def login(): - from .forms import LoginForm + from .models.forms import LoginForm form = LoginForm() if request.method == 'GET': return render_template('/admin/auth/login.html', form=form) @@ -72,7 +72,7 @@ def logout(): @auth.route('/register/', methods=['GET','POST']) @disable_on_registration def register(): - from .forms import RegistrationForm + from .models.forms import RegistrationForm form = RegistrationForm() if request.method == 'GET': return render_template('/admin/auth/register.html', form=form) @@ -93,7 +93,7 @@ def register(): @admin_account_required @disable_if_logged_in def reset(): - from .forms import ResetPasswordForm + from .models.forms import ResetPasswordForm form = ResetPasswordForm() if request.method == 'GET': return render_template('/admin/auth/reset.html', form=form) @@ -128,7 +128,7 @@ def reset_gateway(token1,token2): @admin_account_required @disable_if_logged_in def update_password_(): - from .forms import UpdatePasswordForm + from .models.forms import UpdatePasswordForm form = UpdatePasswordForm() if request.method == 'GET': if 'reset_validated' not in session: diff --git a/ref-test/admin/user/__init__.py b/ref-test/admin/models/__init__.py similarity index 100% rename from ref-test/admin/user/__init__.py rename to ref-test/admin/models/__init__.py diff --git a/ref-test/admin/forms.py b/ref-test/admin/models/forms.py similarity index 100% rename from ref-test/admin/forms.py rename to ref-test/admin/models/forms.py diff --git a/ref-test/admin/models.py b/ref-test/admin/models/tests.py similarity index 100% rename from ref-test/admin/models.py rename to ref-test/admin/models/tests.py diff --git a/ref-test/admin/user/models.py b/ref-test/admin/user/models.py deleted file mode 100644 index a983975..0000000 --- a/ref-test/admin/user/models.py +++ /dev/null @@ -1,185 +0,0 @@ -from flask import flash, make_response, Response -from flask.helpers import url_for -from flask.json import jsonify -from werkzeug.security import generate_password_hash, check_password_hash -from werkzeug.utils import redirect -from flask_mail import Message -import secrets - -from security import encrypt, decrypt -from security.database import decrypt_find_one, encrypted_update -from datetime import datetime, timedelta -from main import db, mail - -class User: - - def __init__(self, _id=None, username=None, password=None, email=None, remember=False): - self._id = _id - self.username = username - self.email = email - self.password = password - self.remember = remember - - def start_session(self, resp:Response): - resp.set_cookie( - key = '_id', - value = self._id, - max_age = timedelta(days=14) if self.remember else 'Session', - path = '/', - expires = datetime.utcnow() + timedelta(days=14) if self.remember else 'Session' - ) - if self.remember: - resp.set_cookie ( - key = 'remember', - value = 'True', - max_age = timedelta(days=14), - path = '/', - expires = datetime.utcnow() + timedelta(days=14) - ) - - def register(self): - from ..views import get_id_from_cookie - user = { - '_id': self._id, - 'email': encrypt(self.email), - 'password': generate_password_hash(self.password, method='sha256'), - 'username': encrypt(self.username) - } - if decrypt_find_one(db.users, { 'username': self.username }): - return jsonify({ 'error': f'Username {self.username} is not available.' }), 400 - if db.users.insert_one(user): - flash(f'User {self.username} has been created successfully. You may now use it to log in and administer the tests.', 'success') - resp = make_response(jsonify(user), 200) - if not get_id_from_cookie: - self.start_session(resp) - return resp - return jsonify({ 'error': f'Registration failed. An error occurred.' }), 400 - - def login(self): - user = decrypt_find_one( db.users, { 'username': self.username }) - if not user: - return jsonify({ 'error': f'Username {self.username} does not exist.' }), 401 - if not check_password_hash( user['password'], self.password ): - return jsonify({ 'error': f'The password you entered is incorrect.' }), 401 - resp = make_response(jsonify({ 'success': f'Successfully logged in user {self.username}.' }), 200) - self._id = user['_id'] - self.start_session(resp) - return resp - - def logout(self): - resp = make_response(redirect(url_for('admin_auth.login'))) - resp.set_cookie( - key = '_id', - value = '', - max_age = timedelta(days=-1), - path = '/', - expires= datetime.utcnow() + timedelta(days=-1) - ) - resp.set_cookie ( - key = 'cookie_consent', - value = 'True', - max_age = 'Session', - path = '/', - expires = 'Session' - ) - resp.set_cookie ( - key = 'remember', - value = 'True', - max_age = timedelta(days=-1), - path = '/', - expires = datetime.utcnow() + timedelta(days=-1) - ) - flash('You have been logged out. All cookies pertaining to your account have been deleted. Have a nice day.', 'alert') - return resp - - def reset_password(self): - user = decrypt_find_one(db.users, { 'username': self.username }) - if not user: - return jsonify({ 'error': f'Username {self.username} does not exist.' }), 401 - if not decrypt(user['email']) == self.email: - return jsonify({ 'error': f'The email address {self.email} does not match the user account {self.username}.' }), 401 - new_password = secrets.token_hex(12) - reset_token = secrets.token_urlsafe(16) - verification_token = secrets.token_urlsafe(16) - user['password'] = generate_password_hash(new_password, method='sha256') - if encrypted_update( { 'username': self.username }, { '$set': {'password': user['password'], 'reset_token': reset_token, 'verification_token': verification_token } } ): - flash(f'Your password has been reset. Instructions to recover your account have been sent to {self.email}. Please be sure to check your spam folder in case you have not received the email.', 'alert') - email = Message( - subject = 'RefTest | Password Reset', - recipients = [self.email], - body = f""" - Hello {user['username']}, \n\n - This email was generated because we received a request to reset the password for your administrator account for the SKA RefTest app.\n\n - If you did not make this request, please ignore this email.\n\n - If you did make this request, then you have two options to recover your account.\n\n - For the time being, your password has been reset to the following:\n\n - {new_password}\n\n - You may use this to log back in to your account, and subsequently change your password to something more suitable.\n\n - Alternatively, you may visit the following private link using your unique token to override your password. Copy and paste the following link in a web browser. Please note that this token is only valid once:\n\n - {url_for('admin_auth.reset_gateway', token1 = reset_token, token2 = verification_token, _external = True)}\n\n - Have a nice day. - """, - html = f""" -

Hello {user['username']},

-

This email was generated because we received a request to reset the password for your administrator account for the SKA RefTest app.

-

If you did not make this request, please ignore this email.

-

If you did make this request, then you have two options to recover your account.

-

For the time being, your password has been reset to the following:

- {new_password} -

You may use this to log back in to your account, and subsequently change your password to something more suitable.

-

Alternatively, you may visit the following private link using your unique token to override your password. Click on the following link, or copy and paste it into a browser. Please note that this token is only valid once:

-

{url_for('admin_auth.reset_gateway', token1 = reset_token, token2 = verification_token, _external = True)}

-

Have a nice day.

- """ - ) - mail.send(email) - return jsonify({ 'success': 'Password reset request has been processed.'}), 200 - - def update(self): - from ..views import get_id_from_cookie - retrieved_user = decrypt_find_one(db.users, { '_id': self._id }) - if not retrieved_user: - return jsonify({ 'error': f'User {retrieved_user["username"]} does not exist.' }), 401 - user = {} - updated = [] - if not self.email == '' and self.email is not None: - user['email'] = self.email - updated.append('email') - if not self.password == '' and self.password is not None: - user['password'] = generate_password_hash(self.password, method='sha256') - updated.append('password') - output = '' - if len(updated) == 0: - flash(f'There were no changes requested for your account.', 'alert'), 200 - return jsonify({'success': 'There were no changes requested for your account.'}), 200 - elif len(updated) == 1: - output = updated[0] - elif len(updated) == 2: - output = ' and '.join(updated) - elif len(updated) > 2: - output = updated[0] - for index in range(1,len(updated)): - if index < len(updated) - 2: - output = ', '.join([output, updated[index]]) - elif index == len(updated) - 2: - output = ', and '.join([output, updated[index]]) - else: - output = ''.join([output, updated[index]]) - encrypted_update(db.users, {'_id': self._id}, { '$set': user }) - if self._id == get_id_from_cookie(): - _output = 'Your ' - elif retrieved_user['username'][-1] == 's': - _output = '’'.join([retrieved_user['username'], '']) - else: - _output = '’'.join([retrieved_user['username'], 's']) - _output = f'{_output} {output} {"has" if len(updated) == 1 else "have"} been updated.' - flash(_output) - return jsonify({'success': _output}), 200 - - def delete(self): - retrieved_user = decrypt_find_one(db.users, { '_id': self._id }) - if not retrieved_user: - return jsonify({ 'error': f'User does not exist.' }), 401 - db.users.find_one_and_delete({'_id': self._id}) - flash(f'User {retrieved_user["username"]} has been deleted.') - return jsonify({'success': f'User {retrieved_user["username"]} has been deleted.'}), 200 diff --git a/ref-test/admin/views.py b/ref-test/admin/views.py index c6f4fc2..6bc4ce4 100644 --- a/ref-test/admin/views.py +++ b/ref-test/admin/views.py @@ -4,14 +4,14 @@ from functools import wraps from werkzeug.security import check_password_hash from security.database import decrypt_find, decrypt_find_one -from .user.models import User +from .models.users import User from flask_mail import Message from main import db from uuid import uuid4 import secrets from main import mail from datetime import datetime, date, timedelta -from .models import Test +from .models.tests import Test views = Blueprint( 'admin_views', @@ -82,7 +82,7 @@ def settings(): @admin_account_required @login_required def users(): - from .forms import CreateUserForm + from .models.forms import CreateUserForm form = CreateUserForm() if request.method == 'GET': users_list = decrypt_find(db.users, {}) @@ -132,7 +132,7 @@ def delete_user(_id:str): if _id == get_id_from_cookie(): flash('Cannot delete your own user account.', 'error') return redirect(url_for('admin_views.users')) - from .forms import DeleteUserForm + from .models.forms import DeleteUserForm form = DeleteUserForm() user = decrypt_find_one(db.users, {'_id': _id}) if request.method == 'GET': @@ -181,7 +181,7 @@ def update_user(_id:str): if _id == get_id_from_cookie(): flash('Cannot delete your own user account.', 'error') return redirect(url_for('admin_views.users')) - from .forms import UpdateUserForm + from .models.forms import UpdateUserForm form = UpdateUserForm() user = decrypt_find_one( db.users, {'_id': _id}) if request.method == 'GET': @@ -251,7 +251,7 @@ def tests(filter=''): if filter not in ['', 'create', 'active', 'scheduled', 'expired', 'all']: return abort(404) if filter == 'create': - from .forms import CreateTest + from .models.forms import CreateTest form = CreateTest() form.time_limit.default='none' form.process() @@ -281,7 +281,7 @@ def tests(filter=''): @admin_account_required @login_required def _tests(): - from .forms import CreateTest + from .models.forms import CreateTest form = CreateTest() form.time_limit.default='none' form.process() From 3bde83cf92af5484d30f52658c3350de6756e30c Mon Sep 17 00:00:00 2001 From: viveksantayana Date: Thu, 25 Nov 2021 23:21:48 +0000 Subject: [PATCH 003/250] Refactor to have all models in the models package. --- ref-test/admin/auth.py | 12 +- ref-test/admin/{user => models}/__init__.py | 0 ref-test/admin/{ => models}/forms.py | 0 ref-test/admin/{models.py => models/tests.py} | 0 ref-test/admin/user/models.py | 185 ------------------ ref-test/admin/views.py | 14 +- 6 files changed, 13 insertions(+), 198 deletions(-) rename ref-test/admin/{user => models}/__init__.py (100%) rename ref-test/admin/{ => models}/forms.py (100%) rename ref-test/admin/{models.py => models/tests.py} (100%) delete mode 100644 ref-test/admin/user/models.py diff --git a/ref-test/admin/auth.py b/ref-test/admin/auth.py index fd9e854..2c6e259 100644 --- a/ref-test/admin/auth.py +++ b/ref-test/admin/auth.py @@ -1,7 +1,7 @@ from flask import Blueprint, render_template, request, session, redirect from flask.helpers import flash, url_for from flask.json import jsonify -from .user.models import User +from .models.users import User from uuid import uuid4 from security.database import decrypt_find_one, encrypted_update from werkzeug.security import check_password_hash @@ -21,7 +21,7 @@ auth = Blueprint( @admin_account_required @login_required def account(): - from .forms import UpdateAccountForm + from .models.forms import UpdateAccountForm form = UpdateAccountForm() _id = get_id_from_cookie() user = decrypt_find_one(db.users, {'_id': _id}) @@ -46,7 +46,7 @@ def account(): @admin_account_required @disable_if_logged_in def login(): - from .forms import LoginForm + from .models.forms import LoginForm form = LoginForm() if request.method == 'GET': return render_template('/admin/auth/login.html', form=form) @@ -72,7 +72,7 @@ def logout(): @auth.route('/register/', methods=['GET','POST']) @disable_on_registration def register(): - from .forms import RegistrationForm + from .models.forms import RegistrationForm form = RegistrationForm() if request.method == 'GET': return render_template('/admin/auth/register.html', form=form) @@ -93,7 +93,7 @@ def register(): @admin_account_required @disable_if_logged_in def reset(): - from .forms import ResetPasswordForm + from .models.forms import ResetPasswordForm form = ResetPasswordForm() if request.method == 'GET': return render_template('/admin/auth/reset.html', form=form) @@ -128,7 +128,7 @@ def reset_gateway(token1,token2): @admin_account_required @disable_if_logged_in def update_password_(): - from .forms import UpdatePasswordForm + from .models.forms import UpdatePasswordForm form = UpdatePasswordForm() if request.method == 'GET': if 'reset_validated' not in session: diff --git a/ref-test/admin/user/__init__.py b/ref-test/admin/models/__init__.py similarity index 100% rename from ref-test/admin/user/__init__.py rename to ref-test/admin/models/__init__.py diff --git a/ref-test/admin/forms.py b/ref-test/admin/models/forms.py similarity index 100% rename from ref-test/admin/forms.py rename to ref-test/admin/models/forms.py diff --git a/ref-test/admin/models.py b/ref-test/admin/models/tests.py similarity index 100% rename from ref-test/admin/models.py rename to ref-test/admin/models/tests.py diff --git a/ref-test/admin/user/models.py b/ref-test/admin/user/models.py deleted file mode 100644 index a983975..0000000 --- a/ref-test/admin/user/models.py +++ /dev/null @@ -1,185 +0,0 @@ -from flask import flash, make_response, Response -from flask.helpers import url_for -from flask.json import jsonify -from werkzeug.security import generate_password_hash, check_password_hash -from werkzeug.utils import redirect -from flask_mail import Message -import secrets - -from security import encrypt, decrypt -from security.database import decrypt_find_one, encrypted_update -from datetime import datetime, timedelta -from main import db, mail - -class User: - - def __init__(self, _id=None, username=None, password=None, email=None, remember=False): - self._id = _id - self.username = username - self.email = email - self.password = password - self.remember = remember - - def start_session(self, resp:Response): - resp.set_cookie( - key = '_id', - value = self._id, - max_age = timedelta(days=14) if self.remember else 'Session', - path = '/', - expires = datetime.utcnow() + timedelta(days=14) if self.remember else 'Session' - ) - if self.remember: - resp.set_cookie ( - key = 'remember', - value = 'True', - max_age = timedelta(days=14), - path = '/', - expires = datetime.utcnow() + timedelta(days=14) - ) - - def register(self): - from ..views import get_id_from_cookie - user = { - '_id': self._id, - 'email': encrypt(self.email), - 'password': generate_password_hash(self.password, method='sha256'), - 'username': encrypt(self.username) - } - if decrypt_find_one(db.users, { 'username': self.username }): - return jsonify({ 'error': f'Username {self.username} is not available.' }), 400 - if db.users.insert_one(user): - flash(f'User {self.username} has been created successfully. You may now use it to log in and administer the tests.', 'success') - resp = make_response(jsonify(user), 200) - if not get_id_from_cookie: - self.start_session(resp) - return resp - return jsonify({ 'error': f'Registration failed. An error occurred.' }), 400 - - def login(self): - user = decrypt_find_one( db.users, { 'username': self.username }) - if not user: - return jsonify({ 'error': f'Username {self.username} does not exist.' }), 401 - if not check_password_hash( user['password'], self.password ): - return jsonify({ 'error': f'The password you entered is incorrect.' }), 401 - resp = make_response(jsonify({ 'success': f'Successfully logged in user {self.username}.' }), 200) - self._id = user['_id'] - self.start_session(resp) - return resp - - def logout(self): - resp = make_response(redirect(url_for('admin_auth.login'))) - resp.set_cookie( - key = '_id', - value = '', - max_age = timedelta(days=-1), - path = '/', - expires= datetime.utcnow() + timedelta(days=-1) - ) - resp.set_cookie ( - key = 'cookie_consent', - value = 'True', - max_age = 'Session', - path = '/', - expires = 'Session' - ) - resp.set_cookie ( - key = 'remember', - value = 'True', - max_age = timedelta(days=-1), - path = '/', - expires = datetime.utcnow() + timedelta(days=-1) - ) - flash('You have been logged out. All cookies pertaining to your account have been deleted. Have a nice day.', 'alert') - return resp - - def reset_password(self): - user = decrypt_find_one(db.users, { 'username': self.username }) - if not user: - return jsonify({ 'error': f'Username {self.username} does not exist.' }), 401 - if not decrypt(user['email']) == self.email: - return jsonify({ 'error': f'The email address {self.email} does not match the user account {self.username}.' }), 401 - new_password = secrets.token_hex(12) - reset_token = secrets.token_urlsafe(16) - verification_token = secrets.token_urlsafe(16) - user['password'] = generate_password_hash(new_password, method='sha256') - if encrypted_update( { 'username': self.username }, { '$set': {'password': user['password'], 'reset_token': reset_token, 'verification_token': verification_token } } ): - flash(f'Your password has been reset. Instructions to recover your account have been sent to {self.email}. Please be sure to check your spam folder in case you have not received the email.', 'alert') - email = Message( - subject = 'RefTest | Password Reset', - recipients = [self.email], - body = f""" - Hello {user['username']}, \n\n - This email was generated because we received a request to reset the password for your administrator account for the SKA RefTest app.\n\n - If you did not make this request, please ignore this email.\n\n - If you did make this request, then you have two options to recover your account.\n\n - For the time being, your password has been reset to the following:\n\n - {new_password}\n\n - You may use this to log back in to your account, and subsequently change your password to something more suitable.\n\n - Alternatively, you may visit the following private link using your unique token to override your password. Copy and paste the following link in a web browser. Please note that this token is only valid once:\n\n - {url_for('admin_auth.reset_gateway', token1 = reset_token, token2 = verification_token, _external = True)}\n\n - Have a nice day. - """, - html = f""" -

Hello {user['username']},

-

This email was generated because we received a request to reset the password for your administrator account for the SKA RefTest app.

-

If you did not make this request, please ignore this email.

-

If you did make this request, then you have two options to recover your account.

-

For the time being, your password has been reset to the following:

- {new_password} -

You may use this to log back in to your account, and subsequently change your password to something more suitable.

-

Alternatively, you may visit the following private link using your unique token to override your password. Click on the following link, or copy and paste it into a browser. Please note that this token is only valid once:

-

{url_for('admin_auth.reset_gateway', token1 = reset_token, token2 = verification_token, _external = True)}

-

Have a nice day.

- """ - ) - mail.send(email) - return jsonify({ 'success': 'Password reset request has been processed.'}), 200 - - def update(self): - from ..views import get_id_from_cookie - retrieved_user = decrypt_find_one(db.users, { '_id': self._id }) - if not retrieved_user: - return jsonify({ 'error': f'User {retrieved_user["username"]} does not exist.' }), 401 - user = {} - updated = [] - if not self.email == '' and self.email is not None: - user['email'] = self.email - updated.append('email') - if not self.password == '' and self.password is not None: - user['password'] = generate_password_hash(self.password, method='sha256') - updated.append('password') - output = '' - if len(updated) == 0: - flash(f'There were no changes requested for your account.', 'alert'), 200 - return jsonify({'success': 'There were no changes requested for your account.'}), 200 - elif len(updated) == 1: - output = updated[0] - elif len(updated) == 2: - output = ' and '.join(updated) - elif len(updated) > 2: - output = updated[0] - for index in range(1,len(updated)): - if index < len(updated) - 2: - output = ', '.join([output, updated[index]]) - elif index == len(updated) - 2: - output = ', and '.join([output, updated[index]]) - else: - output = ''.join([output, updated[index]]) - encrypted_update(db.users, {'_id': self._id}, { '$set': user }) - if self._id == get_id_from_cookie(): - _output = 'Your ' - elif retrieved_user['username'][-1] == 's': - _output = '’'.join([retrieved_user['username'], '']) - else: - _output = '’'.join([retrieved_user['username'], 's']) - _output = f'{_output} {output} {"has" if len(updated) == 1 else "have"} been updated.' - flash(_output) - return jsonify({'success': _output}), 200 - - def delete(self): - retrieved_user = decrypt_find_one(db.users, { '_id': self._id }) - if not retrieved_user: - return jsonify({ 'error': f'User does not exist.' }), 401 - db.users.find_one_and_delete({'_id': self._id}) - flash(f'User {retrieved_user["username"]} has been deleted.') - return jsonify({'success': f'User {retrieved_user["username"]} has been deleted.'}), 200 diff --git a/ref-test/admin/views.py b/ref-test/admin/views.py index c6f4fc2..6bc4ce4 100644 --- a/ref-test/admin/views.py +++ b/ref-test/admin/views.py @@ -4,14 +4,14 @@ from functools import wraps from werkzeug.security import check_password_hash from security.database import decrypt_find, decrypt_find_one -from .user.models import User +from .models.users import User from flask_mail import Message from main import db from uuid import uuid4 import secrets from main import mail from datetime import datetime, date, timedelta -from .models import Test +from .models.tests import Test views = Blueprint( 'admin_views', @@ -82,7 +82,7 @@ def settings(): @admin_account_required @login_required def users(): - from .forms import CreateUserForm + from .models.forms import CreateUserForm form = CreateUserForm() if request.method == 'GET': users_list = decrypt_find(db.users, {}) @@ -132,7 +132,7 @@ def delete_user(_id:str): if _id == get_id_from_cookie(): flash('Cannot delete your own user account.', 'error') return redirect(url_for('admin_views.users')) - from .forms import DeleteUserForm + from .models.forms import DeleteUserForm form = DeleteUserForm() user = decrypt_find_one(db.users, {'_id': _id}) if request.method == 'GET': @@ -181,7 +181,7 @@ def update_user(_id:str): if _id == get_id_from_cookie(): flash('Cannot delete your own user account.', 'error') return redirect(url_for('admin_views.users')) - from .forms import UpdateUserForm + from .models.forms import UpdateUserForm form = UpdateUserForm() user = decrypt_find_one( db.users, {'_id': _id}) if request.method == 'GET': @@ -251,7 +251,7 @@ def tests(filter=''): if filter not in ['', 'create', 'active', 'scheduled', 'expired', 'all']: return abort(404) if filter == 'create': - from .forms import CreateTest + from .models.forms import CreateTest form = CreateTest() form.time_limit.default='none' form.process() @@ -281,7 +281,7 @@ def tests(filter=''): @admin_account_required @login_required def _tests(): - from .forms import CreateTest + from .models.forms import CreateTest form = CreateTest() form.time_limit.default='none' form.process() From 95abec2b4b8b087d3328f8b8904fc0a47cb6e753 Mon Sep 17 00:00:00 2001 From: viveksantayana Date: Thu, 25 Nov 2021 23:21:48 +0000 Subject: [PATCH 004/250] Refactor to have all models in the models package. --- ref-test/admin/auth.py | 12 +- ref-test/admin/{user => models}/__init__.py | 0 ref-test/admin/{ => models}/forms.py | 0 ref-test/admin/{models.py => models/tests.py} | 0 ref-test/admin/user/models.py | 185 ------------------ ref-test/admin/views.py | 14 +- 6 files changed, 13 insertions(+), 198 deletions(-) rename ref-test/admin/{user => models}/__init__.py (100%) rename ref-test/admin/{ => models}/forms.py (100%) rename ref-test/admin/{models.py => models/tests.py} (100%) delete mode 100644 ref-test/admin/user/models.py diff --git a/ref-test/admin/auth.py b/ref-test/admin/auth.py index fd9e854..2c6e259 100644 --- a/ref-test/admin/auth.py +++ b/ref-test/admin/auth.py @@ -1,7 +1,7 @@ from flask import Blueprint, render_template, request, session, redirect from flask.helpers import flash, url_for from flask.json import jsonify -from .user.models import User +from .models.users import User from uuid import uuid4 from security.database import decrypt_find_one, encrypted_update from werkzeug.security import check_password_hash @@ -21,7 +21,7 @@ auth = Blueprint( @admin_account_required @login_required def account(): - from .forms import UpdateAccountForm + from .models.forms import UpdateAccountForm form = UpdateAccountForm() _id = get_id_from_cookie() user = decrypt_find_one(db.users, {'_id': _id}) @@ -46,7 +46,7 @@ def account(): @admin_account_required @disable_if_logged_in def login(): - from .forms import LoginForm + from .models.forms import LoginForm form = LoginForm() if request.method == 'GET': return render_template('/admin/auth/login.html', form=form) @@ -72,7 +72,7 @@ def logout(): @auth.route('/register/', methods=['GET','POST']) @disable_on_registration def register(): - from .forms import RegistrationForm + from .models.forms import RegistrationForm form = RegistrationForm() if request.method == 'GET': return render_template('/admin/auth/register.html', form=form) @@ -93,7 +93,7 @@ def register(): @admin_account_required @disable_if_logged_in def reset(): - from .forms import ResetPasswordForm + from .models.forms import ResetPasswordForm form = ResetPasswordForm() if request.method == 'GET': return render_template('/admin/auth/reset.html', form=form) @@ -128,7 +128,7 @@ def reset_gateway(token1,token2): @admin_account_required @disable_if_logged_in def update_password_(): - from .forms import UpdatePasswordForm + from .models.forms import UpdatePasswordForm form = UpdatePasswordForm() if request.method == 'GET': if 'reset_validated' not in session: diff --git a/ref-test/admin/user/__init__.py b/ref-test/admin/models/__init__.py similarity index 100% rename from ref-test/admin/user/__init__.py rename to ref-test/admin/models/__init__.py diff --git a/ref-test/admin/forms.py b/ref-test/admin/models/forms.py similarity index 100% rename from ref-test/admin/forms.py rename to ref-test/admin/models/forms.py diff --git a/ref-test/admin/models.py b/ref-test/admin/models/tests.py similarity index 100% rename from ref-test/admin/models.py rename to ref-test/admin/models/tests.py diff --git a/ref-test/admin/user/models.py b/ref-test/admin/user/models.py deleted file mode 100644 index a983975..0000000 --- a/ref-test/admin/user/models.py +++ /dev/null @@ -1,185 +0,0 @@ -from flask import flash, make_response, Response -from flask.helpers import url_for -from flask.json import jsonify -from werkzeug.security import generate_password_hash, check_password_hash -from werkzeug.utils import redirect -from flask_mail import Message -import secrets - -from security import encrypt, decrypt -from security.database import decrypt_find_one, encrypted_update -from datetime import datetime, timedelta -from main import db, mail - -class User: - - def __init__(self, _id=None, username=None, password=None, email=None, remember=False): - self._id = _id - self.username = username - self.email = email - self.password = password - self.remember = remember - - def start_session(self, resp:Response): - resp.set_cookie( - key = '_id', - value = self._id, - max_age = timedelta(days=14) if self.remember else 'Session', - path = '/', - expires = datetime.utcnow() + timedelta(days=14) if self.remember else 'Session' - ) - if self.remember: - resp.set_cookie ( - key = 'remember', - value = 'True', - max_age = timedelta(days=14), - path = '/', - expires = datetime.utcnow() + timedelta(days=14) - ) - - def register(self): - from ..views import get_id_from_cookie - user = { - '_id': self._id, - 'email': encrypt(self.email), - 'password': generate_password_hash(self.password, method='sha256'), - 'username': encrypt(self.username) - } - if decrypt_find_one(db.users, { 'username': self.username }): - return jsonify({ 'error': f'Username {self.username} is not available.' }), 400 - if db.users.insert_one(user): - flash(f'User {self.username} has been created successfully. You may now use it to log in and administer the tests.', 'success') - resp = make_response(jsonify(user), 200) - if not get_id_from_cookie: - self.start_session(resp) - return resp - return jsonify({ 'error': f'Registration failed. An error occurred.' }), 400 - - def login(self): - user = decrypt_find_one( db.users, { 'username': self.username }) - if not user: - return jsonify({ 'error': f'Username {self.username} does not exist.' }), 401 - if not check_password_hash( user['password'], self.password ): - return jsonify({ 'error': f'The password you entered is incorrect.' }), 401 - resp = make_response(jsonify({ 'success': f'Successfully logged in user {self.username}.' }), 200) - self._id = user['_id'] - self.start_session(resp) - return resp - - def logout(self): - resp = make_response(redirect(url_for('admin_auth.login'))) - resp.set_cookie( - key = '_id', - value = '', - max_age = timedelta(days=-1), - path = '/', - expires= datetime.utcnow() + timedelta(days=-1) - ) - resp.set_cookie ( - key = 'cookie_consent', - value = 'True', - max_age = 'Session', - path = '/', - expires = 'Session' - ) - resp.set_cookie ( - key = 'remember', - value = 'True', - max_age = timedelta(days=-1), - path = '/', - expires = datetime.utcnow() + timedelta(days=-1) - ) - flash('You have been logged out. All cookies pertaining to your account have been deleted. Have a nice day.', 'alert') - return resp - - def reset_password(self): - user = decrypt_find_one(db.users, { 'username': self.username }) - if not user: - return jsonify({ 'error': f'Username {self.username} does not exist.' }), 401 - if not decrypt(user['email']) == self.email: - return jsonify({ 'error': f'The email address {self.email} does not match the user account {self.username}.' }), 401 - new_password = secrets.token_hex(12) - reset_token = secrets.token_urlsafe(16) - verification_token = secrets.token_urlsafe(16) - user['password'] = generate_password_hash(new_password, method='sha256') - if encrypted_update( { 'username': self.username }, { '$set': {'password': user['password'], 'reset_token': reset_token, 'verification_token': verification_token } } ): - flash(f'Your password has been reset. Instructions to recover your account have been sent to {self.email}. Please be sure to check your spam folder in case you have not received the email.', 'alert') - email = Message( - subject = 'RefTest | Password Reset', - recipients = [self.email], - body = f""" - Hello {user['username']}, \n\n - This email was generated because we received a request to reset the password for your administrator account for the SKA RefTest app.\n\n - If you did not make this request, please ignore this email.\n\n - If you did make this request, then you have two options to recover your account.\n\n - For the time being, your password has been reset to the following:\n\n - {new_password}\n\n - You may use this to log back in to your account, and subsequently change your password to something more suitable.\n\n - Alternatively, you may visit the following private link using your unique token to override your password. Copy and paste the following link in a web browser. Please note that this token is only valid once:\n\n - {url_for('admin_auth.reset_gateway', token1 = reset_token, token2 = verification_token, _external = True)}\n\n - Have a nice day. - """, - html = f""" -

Hello {user['username']},

-

This email was generated because we received a request to reset the password for your administrator account for the SKA RefTest app.

-

If you did not make this request, please ignore this email.

-

If you did make this request, then you have two options to recover your account.

-

For the time being, your password has been reset to the following:

- {new_password} -

You may use this to log back in to your account, and subsequently change your password to something more suitable.

-

Alternatively, you may visit the following private link using your unique token to override your password. Click on the following link, or copy and paste it into a browser. Please note that this token is only valid once:

-

{url_for('admin_auth.reset_gateway', token1 = reset_token, token2 = verification_token, _external = True)}

-

Have a nice day.

- """ - ) - mail.send(email) - return jsonify({ 'success': 'Password reset request has been processed.'}), 200 - - def update(self): - from ..views import get_id_from_cookie - retrieved_user = decrypt_find_one(db.users, { '_id': self._id }) - if not retrieved_user: - return jsonify({ 'error': f'User {retrieved_user["username"]} does not exist.' }), 401 - user = {} - updated = [] - if not self.email == '' and self.email is not None: - user['email'] = self.email - updated.append('email') - if not self.password == '' and self.password is not None: - user['password'] = generate_password_hash(self.password, method='sha256') - updated.append('password') - output = '' - if len(updated) == 0: - flash(f'There were no changes requested for your account.', 'alert'), 200 - return jsonify({'success': 'There were no changes requested for your account.'}), 200 - elif len(updated) == 1: - output = updated[0] - elif len(updated) == 2: - output = ' and '.join(updated) - elif len(updated) > 2: - output = updated[0] - for index in range(1,len(updated)): - if index < len(updated) - 2: - output = ', '.join([output, updated[index]]) - elif index == len(updated) - 2: - output = ', and '.join([output, updated[index]]) - else: - output = ''.join([output, updated[index]]) - encrypted_update(db.users, {'_id': self._id}, { '$set': user }) - if self._id == get_id_from_cookie(): - _output = 'Your ' - elif retrieved_user['username'][-1] == 's': - _output = '’'.join([retrieved_user['username'], '']) - else: - _output = '’'.join([retrieved_user['username'], 's']) - _output = f'{_output} {output} {"has" if len(updated) == 1 else "have"} been updated.' - flash(_output) - return jsonify({'success': _output}), 200 - - def delete(self): - retrieved_user = decrypt_find_one(db.users, { '_id': self._id }) - if not retrieved_user: - return jsonify({ 'error': f'User does not exist.' }), 401 - db.users.find_one_and_delete({'_id': self._id}) - flash(f'User {retrieved_user["username"]} has been deleted.') - return jsonify({'success': f'User {retrieved_user["username"]} has been deleted.'}), 200 diff --git a/ref-test/admin/views.py b/ref-test/admin/views.py index c6f4fc2..6bc4ce4 100644 --- a/ref-test/admin/views.py +++ b/ref-test/admin/views.py @@ -4,14 +4,14 @@ from functools import wraps from werkzeug.security import check_password_hash from security.database import decrypt_find, decrypt_find_one -from .user.models import User +from .models.users import User from flask_mail import Message from main import db from uuid import uuid4 import secrets from main import mail from datetime import datetime, date, timedelta -from .models import Test +from .models.tests import Test views = Blueprint( 'admin_views', @@ -82,7 +82,7 @@ def settings(): @admin_account_required @login_required def users(): - from .forms import CreateUserForm + from .models.forms import CreateUserForm form = CreateUserForm() if request.method == 'GET': users_list = decrypt_find(db.users, {}) @@ -132,7 +132,7 @@ def delete_user(_id:str): if _id == get_id_from_cookie(): flash('Cannot delete your own user account.', 'error') return redirect(url_for('admin_views.users')) - from .forms import DeleteUserForm + from .models.forms import DeleteUserForm form = DeleteUserForm() user = decrypt_find_one(db.users, {'_id': _id}) if request.method == 'GET': @@ -181,7 +181,7 @@ def update_user(_id:str): if _id == get_id_from_cookie(): flash('Cannot delete your own user account.', 'error') return redirect(url_for('admin_views.users')) - from .forms import UpdateUserForm + from .models.forms import UpdateUserForm form = UpdateUserForm() user = decrypt_find_one( db.users, {'_id': _id}) if request.method == 'GET': @@ -251,7 +251,7 @@ def tests(filter=''): if filter not in ['', 'create', 'active', 'scheduled', 'expired', 'all']: return abort(404) if filter == 'create': - from .forms import CreateTest + from .models.forms import CreateTest form = CreateTest() form.time_limit.default='none' form.process() @@ -281,7 +281,7 @@ def tests(filter=''): @admin_account_required @login_required def _tests(): - from .forms import CreateTest + from .models.forms import CreateTest form = CreateTest() form.time_limit.default='none' form.process() From 35b0d30739fac842c06be1f520f2b23b72d1834c Mon Sep 17 00:00:00 2001 From: viveksantayana Date: Sun, 28 Nov 2021 02:30:46 +0000 Subject: [PATCH 005/250] Finished data upload Refactored to move security package inside common Moved data folder to process root. --- REFERENCES.md | 4 + ref-test/admin/auth.py | 2 +- ref-test/admin/models/forms.py | 6 +- ref-test/admin/models/tests.py | 2 +- ref-test/admin/static/js/script.js | 81 ++++++++++++++++--- .../templates/admin/settings/questions.html | 24 +++++- ref-test/admin/views.py | 23 +++++- ref-test/common/__init__.py | 18 ----- ref-test/common/data_tools.py | 59 ++++++++++++++ ref-test/{ => common}/security/__init__.py | 6 +- ref-test/{ => common}/security/database.py | 0 ref-test/config.py | 1 + ref-test/data/.gitignore | 2 + ref-test/main.py | 4 +- ref-test/quiz/views.py | 2 +- 15 files changed, 194 insertions(+), 40 deletions(-) create mode 100644 ref-test/common/data_tools.py rename ref-test/{ => common}/security/__init__.py (86%) rename ref-test/{ => common}/security/database.py (100%) create mode 100644 ref-test/data/.gitignore diff --git a/REFERENCES.md b/REFERENCES.md index 1f58c48..56aed1c 100644 --- a/REFERENCES.md +++ b/REFERENCES.md @@ -77,3 +77,7 @@ Uses SQL rather than MongoDB. ### Flask techniques - [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) \ No newline at end of file diff --git a/ref-test/admin/auth.py b/ref-test/admin/auth.py index 2c6e259..7950c99 100644 --- a/ref-test/admin/auth.py +++ b/ref-test/admin/auth.py @@ -3,7 +3,7 @@ from flask.helpers import flash, url_for from flask.json import jsonify from .models.users import User 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 main import db diff --git a/ref-test/admin/models/forms.py b/ref-test/admin/models/forms.py index c63b8cb..9e762f9 100644 --- a/ref-test/admin/models/forms.py +++ b/ref-test/admin/models/forms.py @@ -1,4 +1,5 @@ from flask_wtf import FlaskForm +from flask_wtf.file import FileField, FileRequired, FileAllowed from wtforms import StringField, PasswordField, BooleanField, DateField, SelectField from wtforms.validators import InputRequired, Email, Length, EqualTo, Optional from datetime import date, timedelta @@ -53,4 +54,7 @@ class CreateTest(FlaskForm): ('120', '2 hours') ] 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) \ No newline at end of file + time_limit = SelectField('Time Limit', choices=time_options) + +class UploadDataForm(FlaskForm): + data_file = FileField('Data File', validators=[FileRequired(), FileAllowed(['json'])]) \ No newline at end of file diff --git a/ref-test/admin/models/tests.py b/ref-test/admin/models/tests.py index ce26428..45196a7 100644 --- a/ref-test/admin/models/tests.py +++ b/ref-test/admin/models/tests.py @@ -5,7 +5,7 @@ from flask import flash, jsonify import secrets from main import db -from security import encrypt +from common.security import encrypt class Test: def __init__(self, _id=None, start_date=None, expiry_date=None, time_limit=None, creator=None): diff --git a/ref-test/admin/static/js/script.js b/ref-test/admin/static/js/script.js index ad97048..e6e4d73 100644 --- a/ref-test/admin/static/js/script.js +++ b/ref-test/admin/static/js/script.js @@ -350,13 +350,18 @@ $('form[name=form-update-account]').submit(function(event) { 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({ - url: `/admin/tests/delete/${_id}`, - type: 'GET', + url: window.location.pathname, + type: 'POST', + data: data, + dataType: 'json', success: function(response) { window.location.href = '/admin/tests/'; }, @@ -386,20 +391,78 @@ $('.delete-test').click(function(event) { event.preventDefault(); }); -// Edit and Delete Test Button Handlers - -$('form[name=form-create-test]').submit(function(event) { +$('form[name=form-upload-questions]').submit(function(event) { var $form = $(this); 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 = '' $.ajax({ url: window.location.pathname, type: 'POST', data: data, - dataType: 'json', + processData: false, + contentType: false, + success: function(response) { + if (typeof response.success === 'string' || response.success instanceof String) { + alert.innerHTML = alert.innerHTML + ` + + `; + } else if (response.success instanceof Array) { + for (var i = 0; i < response.success.length; i ++) { + alert.innerHTML = alert.innerHTML + ` + + `; + } + } + }, + error: function(response) { + if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) { + alert.innerHTML = alert.innerHTML + ` + + `; + } else if (response.responseJSON.error instanceof Array) { + for (var i = 0; i < response.responseJSON.error.length; i ++) { + alert.innerHTML = alert.innerHTML + ` + + `; + } + } + } + }); + + 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) { window.location.href = '/admin/tests/'; }, diff --git a/ref-test/admin/templates/admin/settings/questions.html b/ref-test/admin/templates/admin/settings/questions.html index 9624223..d4892e3 100644 --- a/ref-test/admin/templates/admin/settings/questions.html +++ b/ref-test/admin/templates/admin/settings/questions.html @@ -1 +1,23 @@ -{% extends "admin/components/base.html" %} \ No newline at end of file +{% extends "admin/components/base.html" %} +{% block title %} SKA Referee Test | Upload Questions {% endblock %} +{% block content %} + +
+ +
+{% endblock %} \ No newline at end of file diff --git a/ref-test/admin/views.py b/ref-test/admin/views.py index 6bc4ce4..43edb22 100644 --- a/ref-test/admin/views.py +++ b/ref-test/admin/views.py @@ -1,9 +1,10 @@ from flask import Blueprint, render_template, flash, redirect, request, jsonify, abort from flask.helpers import url_for from functools import wraps +from datetime import datetime 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 flask_mail import Message 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] return jsonify({ 'error': errors}), 400 -@views.route('/settings/questions/') +@views.route('/settings/questions/', methods=['GET', 'POST']) @admin_account_required @login_required 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/') @admin_account_required diff --git a/ref-test/common/__init__.py b/ref-test/common/__init__.py index d15e272..e69de29 100644 --- a/ref-test/common/__init__.py +++ b/ref-test/common/__init__.py @@ -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 \ No newline at end of file diff --git a/ref-test/common/data_tools.py b/ref-test/common/data_tools.py new file mode 100644 index 0000000..dbc92e8 --- /dev/null +++ b/ref-test/common/data_tools.py @@ -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) \ No newline at end of file diff --git a/ref-test/security/__init__.py b/ref-test/common/security/__init__.py similarity index 86% rename from ref-test/security/__init__.py rename to ref-test/common/security/__init__.py index 83d22c2..0272741 100644 --- a/ref-test/security/__init__.py +++ b/ref-test/common/security/__init__.py @@ -2,17 +2,17 @@ from os import environ, path from cryptography.fernet import Fernet def generate_keyfile(): - with open('./security/.encryption.key', 'wb') as keyfile: + with open('./common/security/.encryption.key', 'wb') as keyfile: key = Fernet.generate_key() keyfile.write(key) def load_key(): - with open('./security/.encryption.key', 'rb') as keyfile: + with open('./common/security/.encryption.key', 'rb') as keyfile: key = keyfile.read() return key def check_keyfile_exists(): - return path.isfile('./security/.encryption.key') + return path.isfile('./common/security/.encryption.key') def encrypt(input): if not check_keyfile_exists(): diff --git a/ref-test/security/database.py b/ref-test/common/security/database.py similarity index 100% rename from ref-test/security/database.py rename to ref-test/common/security/database.py diff --git a/ref-test/config.py b/ref-test/config.py index d394ce9..a1012c5 100644 --- a/ref-test/config.py +++ b/ref-test/config.py @@ -26,6 +26,7 @@ class Config(object): MAIL_MAX_EMAILS = int(os.getenv("MAIL_MAX_EMAILS")) MAIL_SUPPRESS_SEND = False MAIL_ASCII_ATTACHMENTS = bool(os.getenv("MAIL_ASCII_ATTACHMENTS")) + DATA_FILE_DIRECTORY = os.getenv("DATA_FILE_DIRECTORY") class ProductionConfig(Config): pass diff --git a/ref-test/data/.gitignore b/ref-test/data/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/ref-test/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/ref-test/main.py b/ref-test/main.py index 2a3173c..d2f4eb3 100644 --- a/ref-test/main.py +++ b/ref-test/main.py @@ -9,7 +9,7 @@ from pymongo.errors import ConnectionFailure from flask_wtf.csrf import CSRFProtect, CSRFError 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.config.from_object('config.DevelopmentConfig') @@ -37,7 +37,7 @@ if __name__ == '__main__': if not check_keyfile_exists(): generate_keyfile() - from common import cookie_consent + from common.blueprints import cookie_consent from admin.views import views as admin_views from admin.auth import auth as admin_auth diff --git a/ref-test/quiz/views.py b/ref-test/quiz/views.py index 3f0bb35..cd88bd2 100644 --- a/ref-test/quiz/views.py +++ b/ref-test/quiz/views.py @@ -3,7 +3,7 @@ from datetime import datetime from uuid import uuid4 from main import db -from security import encrypt +from common.security import encrypt views = Blueprint( 'quiz_views', From 6d5f8bc00c0c1964c2bb50cd64e70cfacb92cc2c Mon Sep 17 00:00:00 2001 From: viveksantayana Date: Sun, 28 Nov 2021 02:30:46 +0000 Subject: [PATCH 006/250] Finished data upload Refactored to move security package inside common Moved data folder to process root. --- REFERENCES.md | 4 + ref-test/admin/auth.py | 2 +- ref-test/admin/models/forms.py | 6 +- ref-test/admin/models/tests.py | 2 +- ref-test/admin/static/js/script.js | 81 ++++++++++++++++--- .../templates/admin/settings/questions.html | 24 +++++- ref-test/admin/views.py | 23 +++++- ref-test/common/__init__.py | 18 ----- ref-test/common/data_tools.py | 59 ++++++++++++++ ref-test/{ => common}/security/__init__.py | 6 +- ref-test/{ => common}/security/database.py | 0 ref-test/config.py | 1 + ref-test/data/.gitignore | 2 + ref-test/main.py | 4 +- ref-test/quiz/views.py | 2 +- 15 files changed, 194 insertions(+), 40 deletions(-) create mode 100644 ref-test/common/data_tools.py rename ref-test/{ => common}/security/__init__.py (86%) rename ref-test/{ => common}/security/database.py (100%) create mode 100644 ref-test/data/.gitignore diff --git a/REFERENCES.md b/REFERENCES.md index 1f58c48..56aed1c 100644 --- a/REFERENCES.md +++ b/REFERENCES.md @@ -77,3 +77,7 @@ Uses SQL rather than MongoDB. ### Flask techniques - [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) \ No newline at end of file diff --git a/ref-test/admin/auth.py b/ref-test/admin/auth.py index 2c6e259..7950c99 100644 --- a/ref-test/admin/auth.py +++ b/ref-test/admin/auth.py @@ -3,7 +3,7 @@ from flask.helpers import flash, url_for from flask.json import jsonify from .models.users import User 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 main import db diff --git a/ref-test/admin/models/forms.py b/ref-test/admin/models/forms.py index c63b8cb..9e762f9 100644 --- a/ref-test/admin/models/forms.py +++ b/ref-test/admin/models/forms.py @@ -1,4 +1,5 @@ from flask_wtf import FlaskForm +from flask_wtf.file import FileField, FileRequired, FileAllowed from wtforms import StringField, PasswordField, BooleanField, DateField, SelectField from wtforms.validators import InputRequired, Email, Length, EqualTo, Optional from datetime import date, timedelta @@ -53,4 +54,7 @@ class CreateTest(FlaskForm): ('120', '2 hours') ] 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) \ No newline at end of file + time_limit = SelectField('Time Limit', choices=time_options) + +class UploadDataForm(FlaskForm): + data_file = FileField('Data File', validators=[FileRequired(), FileAllowed(['json'])]) \ No newline at end of file diff --git a/ref-test/admin/models/tests.py b/ref-test/admin/models/tests.py index ce26428..45196a7 100644 --- a/ref-test/admin/models/tests.py +++ b/ref-test/admin/models/tests.py @@ -5,7 +5,7 @@ from flask import flash, jsonify import secrets from main import db -from security import encrypt +from common.security import encrypt class Test: def __init__(self, _id=None, start_date=None, expiry_date=None, time_limit=None, creator=None): diff --git a/ref-test/admin/static/js/script.js b/ref-test/admin/static/js/script.js index ad97048..e6e4d73 100644 --- a/ref-test/admin/static/js/script.js +++ b/ref-test/admin/static/js/script.js @@ -350,13 +350,18 @@ $('form[name=form-update-account]').submit(function(event) { 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({ - url: `/admin/tests/delete/${_id}`, - type: 'GET', + url: window.location.pathname, + type: 'POST', + data: data, + dataType: 'json', success: function(response) { window.location.href = '/admin/tests/'; }, @@ -386,20 +391,78 @@ $('.delete-test').click(function(event) { event.preventDefault(); }); -// Edit and Delete Test Button Handlers - -$('form[name=form-create-test]').submit(function(event) { +$('form[name=form-upload-questions]').submit(function(event) { var $form = $(this); 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 = '' $.ajax({ url: window.location.pathname, type: 'POST', data: data, - dataType: 'json', + processData: false, + contentType: false, + success: function(response) { + if (typeof response.success === 'string' || response.success instanceof String) { + alert.innerHTML = alert.innerHTML + ` + + `; + } else if (response.success instanceof Array) { + for (var i = 0; i < response.success.length; i ++) { + alert.innerHTML = alert.innerHTML + ` + + `; + } + } + }, + error: function(response) { + if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) { + alert.innerHTML = alert.innerHTML + ` + + `; + } else if (response.responseJSON.error instanceof Array) { + for (var i = 0; i < response.responseJSON.error.length; i ++) { + alert.innerHTML = alert.innerHTML + ` + + `; + } + } + } + }); + + 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) { window.location.href = '/admin/tests/'; }, diff --git a/ref-test/admin/templates/admin/settings/questions.html b/ref-test/admin/templates/admin/settings/questions.html index 9624223..d4892e3 100644 --- a/ref-test/admin/templates/admin/settings/questions.html +++ b/ref-test/admin/templates/admin/settings/questions.html @@ -1 +1,23 @@ -{% extends "admin/components/base.html" %} \ No newline at end of file +{% extends "admin/components/base.html" %} +{% block title %} SKA Referee Test | Upload Questions {% endblock %} +{% block content %} + +
+ +
+{% endblock %} \ No newline at end of file diff --git a/ref-test/admin/views.py b/ref-test/admin/views.py index 6bc4ce4..43edb22 100644 --- a/ref-test/admin/views.py +++ b/ref-test/admin/views.py @@ -1,9 +1,10 @@ from flask import Blueprint, render_template, flash, redirect, request, jsonify, abort from flask.helpers import url_for from functools import wraps +from datetime import datetime 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 flask_mail import Message 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] return jsonify({ 'error': errors}), 400 -@views.route('/settings/questions/') +@views.route('/settings/questions/', methods=['GET', 'POST']) @admin_account_required @login_required 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/') @admin_account_required diff --git a/ref-test/common/__init__.py b/ref-test/common/__init__.py index d15e272..e69de29 100644 --- a/ref-test/common/__init__.py +++ b/ref-test/common/__init__.py @@ -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 \ No newline at end of file diff --git a/ref-test/common/data_tools.py b/ref-test/common/data_tools.py new file mode 100644 index 0000000..dbc92e8 --- /dev/null +++ b/ref-test/common/data_tools.py @@ -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) \ No newline at end of file diff --git a/ref-test/security/__init__.py b/ref-test/common/security/__init__.py similarity index 86% rename from ref-test/security/__init__.py rename to ref-test/common/security/__init__.py index 83d22c2..0272741 100644 --- a/ref-test/security/__init__.py +++ b/ref-test/common/security/__init__.py @@ -2,17 +2,17 @@ from os import environ, path from cryptography.fernet import Fernet def generate_keyfile(): - with open('./security/.encryption.key', 'wb') as keyfile: + with open('./common/security/.encryption.key', 'wb') as keyfile: key = Fernet.generate_key() keyfile.write(key) def load_key(): - with open('./security/.encryption.key', 'rb') as keyfile: + with open('./common/security/.encryption.key', 'rb') as keyfile: key = keyfile.read() return key def check_keyfile_exists(): - return path.isfile('./security/.encryption.key') + return path.isfile('./common/security/.encryption.key') def encrypt(input): if not check_keyfile_exists(): diff --git a/ref-test/security/database.py b/ref-test/common/security/database.py similarity index 100% rename from ref-test/security/database.py rename to ref-test/common/security/database.py diff --git a/ref-test/config.py b/ref-test/config.py index d394ce9..a1012c5 100644 --- a/ref-test/config.py +++ b/ref-test/config.py @@ -26,6 +26,7 @@ class Config(object): MAIL_MAX_EMAILS = int(os.getenv("MAIL_MAX_EMAILS")) MAIL_SUPPRESS_SEND = False MAIL_ASCII_ATTACHMENTS = bool(os.getenv("MAIL_ASCII_ATTACHMENTS")) + DATA_FILE_DIRECTORY = os.getenv("DATA_FILE_DIRECTORY") class ProductionConfig(Config): pass diff --git a/ref-test/data/.gitignore b/ref-test/data/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/ref-test/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/ref-test/main.py b/ref-test/main.py index 2a3173c..d2f4eb3 100644 --- a/ref-test/main.py +++ b/ref-test/main.py @@ -9,7 +9,7 @@ from pymongo.errors import ConnectionFailure from flask_wtf.csrf import CSRFProtect, CSRFError 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.config.from_object('config.DevelopmentConfig') @@ -37,7 +37,7 @@ if __name__ == '__main__': if not check_keyfile_exists(): generate_keyfile() - from common import cookie_consent + from common.blueprints import cookie_consent from admin.views import views as admin_views from admin.auth import auth as admin_auth diff --git a/ref-test/quiz/views.py b/ref-test/quiz/views.py index 3f0bb35..cd88bd2 100644 --- a/ref-test/quiz/views.py +++ b/ref-test/quiz/views.py @@ -3,7 +3,7 @@ from datetime import datetime from uuid import uuid4 from main import db -from security import encrypt +from common.security import encrypt views = Blueprint( 'quiz_views', From e37d2873977050e4f72b8fff2164b0fc1433a3e3 Mon Sep 17 00:00:00 2001 From: viveksantayana Date: Sun, 28 Nov 2021 02:30:46 +0000 Subject: [PATCH 007/250] Finished data upload Refactored to move security package inside common Moved data folder to process root. --- REFERENCES.md | 4 + ref-test/admin/auth.py | 2 +- ref-test/admin/models/forms.py | 6 +- ref-test/admin/models/tests.py | 2 +- ref-test/admin/static/js/script.js | 81 ++++++++++++++++--- .../templates/admin/settings/questions.html | 24 +++++- ref-test/admin/views.py | 23 +++++- ref-test/common/__init__.py | 18 ----- ref-test/common/data_tools.py | 59 ++++++++++++++ ref-test/{ => common}/security/__init__.py | 6 +- ref-test/{ => common}/security/database.py | 0 ref-test/config.py | 1 + ref-test/data/.gitignore | 2 + ref-test/main.py | 4 +- ref-test/quiz/views.py | 2 +- 15 files changed, 194 insertions(+), 40 deletions(-) create mode 100644 ref-test/common/data_tools.py rename ref-test/{ => common}/security/__init__.py (86%) rename ref-test/{ => common}/security/database.py (100%) create mode 100644 ref-test/data/.gitignore diff --git a/REFERENCES.md b/REFERENCES.md index 1f58c48..56aed1c 100644 --- a/REFERENCES.md +++ b/REFERENCES.md @@ -77,3 +77,7 @@ Uses SQL rather than MongoDB. ### Flask techniques - [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) \ No newline at end of file diff --git a/ref-test/admin/auth.py b/ref-test/admin/auth.py index 2c6e259..7950c99 100644 --- a/ref-test/admin/auth.py +++ b/ref-test/admin/auth.py @@ -3,7 +3,7 @@ from flask.helpers import flash, url_for from flask.json import jsonify from .models.users import User 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 main import db diff --git a/ref-test/admin/models/forms.py b/ref-test/admin/models/forms.py index c63b8cb..9e762f9 100644 --- a/ref-test/admin/models/forms.py +++ b/ref-test/admin/models/forms.py @@ -1,4 +1,5 @@ from flask_wtf import FlaskForm +from flask_wtf.file import FileField, FileRequired, FileAllowed from wtforms import StringField, PasswordField, BooleanField, DateField, SelectField from wtforms.validators import InputRequired, Email, Length, EqualTo, Optional from datetime import date, timedelta @@ -53,4 +54,7 @@ class CreateTest(FlaskForm): ('120', '2 hours') ] 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) \ No newline at end of file + time_limit = SelectField('Time Limit', choices=time_options) + +class UploadDataForm(FlaskForm): + data_file = FileField('Data File', validators=[FileRequired(), FileAllowed(['json'])]) \ No newline at end of file diff --git a/ref-test/admin/models/tests.py b/ref-test/admin/models/tests.py index ce26428..45196a7 100644 --- a/ref-test/admin/models/tests.py +++ b/ref-test/admin/models/tests.py @@ -5,7 +5,7 @@ from flask import flash, jsonify import secrets from main import db -from security import encrypt +from common.security import encrypt class Test: def __init__(self, _id=None, start_date=None, expiry_date=None, time_limit=None, creator=None): diff --git a/ref-test/admin/static/js/script.js b/ref-test/admin/static/js/script.js index ad97048..e6e4d73 100644 --- a/ref-test/admin/static/js/script.js +++ b/ref-test/admin/static/js/script.js @@ -350,13 +350,18 @@ $('form[name=form-update-account]').submit(function(event) { 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({ - url: `/admin/tests/delete/${_id}`, - type: 'GET', + url: window.location.pathname, + type: 'POST', + data: data, + dataType: 'json', success: function(response) { window.location.href = '/admin/tests/'; }, @@ -386,20 +391,78 @@ $('.delete-test').click(function(event) { event.preventDefault(); }); -// Edit and Delete Test Button Handlers - -$('form[name=form-create-test]').submit(function(event) { +$('form[name=form-upload-questions]').submit(function(event) { var $form = $(this); 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 = '' $.ajax({ url: window.location.pathname, type: 'POST', data: data, - dataType: 'json', + processData: false, + contentType: false, + success: function(response) { + if (typeof response.success === 'string' || response.success instanceof String) { + alert.innerHTML = alert.innerHTML + ` + + `; + } else if (response.success instanceof Array) { + for (var i = 0; i < response.success.length; i ++) { + alert.innerHTML = alert.innerHTML + ` + + `; + } + } + }, + error: function(response) { + if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) { + alert.innerHTML = alert.innerHTML + ` + + `; + } else if (response.responseJSON.error instanceof Array) { + for (var i = 0; i < response.responseJSON.error.length; i ++) { + alert.innerHTML = alert.innerHTML + ` + + `; + } + } + } + }); + + 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) { window.location.href = '/admin/tests/'; }, diff --git a/ref-test/admin/templates/admin/settings/questions.html b/ref-test/admin/templates/admin/settings/questions.html index 9624223..d4892e3 100644 --- a/ref-test/admin/templates/admin/settings/questions.html +++ b/ref-test/admin/templates/admin/settings/questions.html @@ -1 +1,23 @@ -{% extends "admin/components/base.html" %} \ No newline at end of file +{% extends "admin/components/base.html" %} +{% block title %} SKA Referee Test | Upload Questions {% endblock %} +{% block content %} + +
+ +
+{% endblock %} \ No newline at end of file diff --git a/ref-test/admin/views.py b/ref-test/admin/views.py index 6bc4ce4..43edb22 100644 --- a/ref-test/admin/views.py +++ b/ref-test/admin/views.py @@ -1,9 +1,10 @@ from flask import Blueprint, render_template, flash, redirect, request, jsonify, abort from flask.helpers import url_for from functools import wraps +from datetime import datetime 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 flask_mail import Message 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] return jsonify({ 'error': errors}), 400 -@views.route('/settings/questions/') +@views.route('/settings/questions/', methods=['GET', 'POST']) @admin_account_required @login_required 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/') @admin_account_required diff --git a/ref-test/common/__init__.py b/ref-test/common/__init__.py index d15e272..e69de29 100644 --- a/ref-test/common/__init__.py +++ b/ref-test/common/__init__.py @@ -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 \ No newline at end of file diff --git a/ref-test/common/data_tools.py b/ref-test/common/data_tools.py new file mode 100644 index 0000000..dbc92e8 --- /dev/null +++ b/ref-test/common/data_tools.py @@ -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) \ No newline at end of file diff --git a/ref-test/security/__init__.py b/ref-test/common/security/__init__.py similarity index 86% rename from ref-test/security/__init__.py rename to ref-test/common/security/__init__.py index 83d22c2..0272741 100644 --- a/ref-test/security/__init__.py +++ b/ref-test/common/security/__init__.py @@ -2,17 +2,17 @@ from os import environ, path from cryptography.fernet import Fernet def generate_keyfile(): - with open('./security/.encryption.key', 'wb') as keyfile: + with open('./common/security/.encryption.key', 'wb') as keyfile: key = Fernet.generate_key() keyfile.write(key) def load_key(): - with open('./security/.encryption.key', 'rb') as keyfile: + with open('./common/security/.encryption.key', 'rb') as keyfile: key = keyfile.read() return key def check_keyfile_exists(): - return path.isfile('./security/.encryption.key') + return path.isfile('./common/security/.encryption.key') def encrypt(input): if not check_keyfile_exists(): diff --git a/ref-test/security/database.py b/ref-test/common/security/database.py similarity index 100% rename from ref-test/security/database.py rename to ref-test/common/security/database.py diff --git a/ref-test/config.py b/ref-test/config.py index d394ce9..a1012c5 100644 --- a/ref-test/config.py +++ b/ref-test/config.py @@ -26,6 +26,7 @@ class Config(object): MAIL_MAX_EMAILS = int(os.getenv("MAIL_MAX_EMAILS")) MAIL_SUPPRESS_SEND = False MAIL_ASCII_ATTACHMENTS = bool(os.getenv("MAIL_ASCII_ATTACHMENTS")) + DATA_FILE_DIRECTORY = os.getenv("DATA_FILE_DIRECTORY") class ProductionConfig(Config): pass diff --git a/ref-test/data/.gitignore b/ref-test/data/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/ref-test/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/ref-test/main.py b/ref-test/main.py index 2a3173c..d2f4eb3 100644 --- a/ref-test/main.py +++ b/ref-test/main.py @@ -9,7 +9,7 @@ from pymongo.errors import ConnectionFailure from flask_wtf.csrf import CSRFProtect, CSRFError 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.config.from_object('config.DevelopmentConfig') @@ -37,7 +37,7 @@ if __name__ == '__main__': if not check_keyfile_exists(): generate_keyfile() - from common import cookie_consent + from common.blueprints import cookie_consent from admin.views import views as admin_views from admin.auth import auth as admin_auth diff --git a/ref-test/quiz/views.py b/ref-test/quiz/views.py index 3f0bb35..cd88bd2 100644 --- a/ref-test/quiz/views.py +++ b/ref-test/quiz/views.py @@ -3,7 +3,7 @@ from datetime import datetime from uuid import uuid4 from main import db -from security import encrypt +from common.security import encrypt views = Blueprint( 'quiz_views', From e0eda9df4954d8fc061e89e95a072fc49fd5da61 Mon Sep 17 00:00:00 2001 From: viveksantayana Date: Sun, 28 Nov 2021 02:30:46 +0000 Subject: [PATCH 008/250] Finished data upload Refactored to move security package inside common Moved data folder to process root. --- REFERENCES.md | 4 + ref-test/admin/auth.py | 2 +- ref-test/admin/models/forms.py | 6 +- ref-test/admin/models/tests.py | 2 +- ref-test/admin/static/js/script.js | 81 ++++++++++++++++--- .../templates/admin/settings/questions.html | 24 +++++- ref-test/admin/views.py | 23 +++++- ref-test/common/__init__.py | 18 ----- ref-test/common/data_tools.py | 59 ++++++++++++++ ref-test/{ => common}/security/__init__.py | 6 +- ref-test/{ => common}/security/database.py | 0 ref-test/config.py | 1 + ref-test/data/.gitignore | 2 + ref-test/main.py | 4 +- ref-test/quiz/views.py | 2 +- 15 files changed, 194 insertions(+), 40 deletions(-) create mode 100644 ref-test/common/data_tools.py rename ref-test/{ => common}/security/__init__.py (86%) rename ref-test/{ => common}/security/database.py (100%) create mode 100644 ref-test/data/.gitignore diff --git a/REFERENCES.md b/REFERENCES.md index 1f58c48..56aed1c 100644 --- a/REFERENCES.md +++ b/REFERENCES.md @@ -77,3 +77,7 @@ Uses SQL rather than MongoDB. ### Flask techniques - [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) \ No newline at end of file diff --git a/ref-test/admin/auth.py b/ref-test/admin/auth.py index 2c6e259..7950c99 100644 --- a/ref-test/admin/auth.py +++ b/ref-test/admin/auth.py @@ -3,7 +3,7 @@ from flask.helpers import flash, url_for from flask.json import jsonify from .models.users import User 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 main import db diff --git a/ref-test/admin/models/forms.py b/ref-test/admin/models/forms.py index c63b8cb..9e762f9 100644 --- a/ref-test/admin/models/forms.py +++ b/ref-test/admin/models/forms.py @@ -1,4 +1,5 @@ from flask_wtf import FlaskForm +from flask_wtf.file import FileField, FileRequired, FileAllowed from wtforms import StringField, PasswordField, BooleanField, DateField, SelectField from wtforms.validators import InputRequired, Email, Length, EqualTo, Optional from datetime import date, timedelta @@ -53,4 +54,7 @@ class CreateTest(FlaskForm): ('120', '2 hours') ] 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) \ No newline at end of file + time_limit = SelectField('Time Limit', choices=time_options) + +class UploadDataForm(FlaskForm): + data_file = FileField('Data File', validators=[FileRequired(), FileAllowed(['json'])]) \ No newline at end of file diff --git a/ref-test/admin/models/tests.py b/ref-test/admin/models/tests.py index ce26428..45196a7 100644 --- a/ref-test/admin/models/tests.py +++ b/ref-test/admin/models/tests.py @@ -5,7 +5,7 @@ from flask import flash, jsonify import secrets from main import db -from security import encrypt +from common.security import encrypt class Test: def __init__(self, _id=None, start_date=None, expiry_date=None, time_limit=None, creator=None): diff --git a/ref-test/admin/static/js/script.js b/ref-test/admin/static/js/script.js index ad97048..e6e4d73 100644 --- a/ref-test/admin/static/js/script.js +++ b/ref-test/admin/static/js/script.js @@ -350,13 +350,18 @@ $('form[name=form-update-account]').submit(function(event) { 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({ - url: `/admin/tests/delete/${_id}`, - type: 'GET', + url: window.location.pathname, + type: 'POST', + data: data, + dataType: 'json', success: function(response) { window.location.href = '/admin/tests/'; }, @@ -386,20 +391,78 @@ $('.delete-test').click(function(event) { event.preventDefault(); }); -// Edit and Delete Test Button Handlers - -$('form[name=form-create-test]').submit(function(event) { +$('form[name=form-upload-questions]').submit(function(event) { var $form = $(this); 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 = '' $.ajax({ url: window.location.pathname, type: 'POST', data: data, - dataType: 'json', + processData: false, + contentType: false, + success: function(response) { + if (typeof response.success === 'string' || response.success instanceof String) { + alert.innerHTML = alert.innerHTML + ` + + `; + } else if (response.success instanceof Array) { + for (var i = 0; i < response.success.length; i ++) { + alert.innerHTML = alert.innerHTML + ` + + `; + } + } + }, + error: function(response) { + if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) { + alert.innerHTML = alert.innerHTML + ` + + `; + } else if (response.responseJSON.error instanceof Array) { + for (var i = 0; i < response.responseJSON.error.length; i ++) { + alert.innerHTML = alert.innerHTML + ` + + `; + } + } + } + }); + + 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) { window.location.href = '/admin/tests/'; }, diff --git a/ref-test/admin/templates/admin/settings/questions.html b/ref-test/admin/templates/admin/settings/questions.html index 9624223..d4892e3 100644 --- a/ref-test/admin/templates/admin/settings/questions.html +++ b/ref-test/admin/templates/admin/settings/questions.html @@ -1 +1,23 @@ -{% extends "admin/components/base.html" %} \ No newline at end of file +{% extends "admin/components/base.html" %} +{% block title %} SKA Referee Test | Upload Questions {% endblock %} +{% block content %} + +
+ +
+{% endblock %} \ No newline at end of file diff --git a/ref-test/admin/views.py b/ref-test/admin/views.py index 6bc4ce4..43edb22 100644 --- a/ref-test/admin/views.py +++ b/ref-test/admin/views.py @@ -1,9 +1,10 @@ from flask import Blueprint, render_template, flash, redirect, request, jsonify, abort from flask.helpers import url_for from functools import wraps +from datetime import datetime 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 flask_mail import Message 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] return jsonify({ 'error': errors}), 400 -@views.route('/settings/questions/') +@views.route('/settings/questions/', methods=['GET', 'POST']) @admin_account_required @login_required 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/') @admin_account_required diff --git a/ref-test/common/__init__.py b/ref-test/common/__init__.py index d15e272..e69de29 100644 --- a/ref-test/common/__init__.py +++ b/ref-test/common/__init__.py @@ -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 \ No newline at end of file diff --git a/ref-test/common/data_tools.py b/ref-test/common/data_tools.py new file mode 100644 index 0000000..dbc92e8 --- /dev/null +++ b/ref-test/common/data_tools.py @@ -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) \ No newline at end of file diff --git a/ref-test/security/__init__.py b/ref-test/common/security/__init__.py similarity index 86% rename from ref-test/security/__init__.py rename to ref-test/common/security/__init__.py index 83d22c2..0272741 100644 --- a/ref-test/security/__init__.py +++ b/ref-test/common/security/__init__.py @@ -2,17 +2,17 @@ from os import environ, path from cryptography.fernet import Fernet def generate_keyfile(): - with open('./security/.encryption.key', 'wb') as keyfile: + with open('./common/security/.encryption.key', 'wb') as keyfile: key = Fernet.generate_key() keyfile.write(key) def load_key(): - with open('./security/.encryption.key', 'rb') as keyfile: + with open('./common/security/.encryption.key', 'rb') as keyfile: key = keyfile.read() return key def check_keyfile_exists(): - return path.isfile('./security/.encryption.key') + return path.isfile('./common/security/.encryption.key') def encrypt(input): if not check_keyfile_exists(): diff --git a/ref-test/security/database.py b/ref-test/common/security/database.py similarity index 100% rename from ref-test/security/database.py rename to ref-test/common/security/database.py diff --git a/ref-test/config.py b/ref-test/config.py index d394ce9..a1012c5 100644 --- a/ref-test/config.py +++ b/ref-test/config.py @@ -26,6 +26,7 @@ class Config(object): MAIL_MAX_EMAILS = int(os.getenv("MAIL_MAX_EMAILS")) MAIL_SUPPRESS_SEND = False MAIL_ASCII_ATTACHMENTS = bool(os.getenv("MAIL_ASCII_ATTACHMENTS")) + DATA_FILE_DIRECTORY = os.getenv("DATA_FILE_DIRECTORY") class ProductionConfig(Config): pass diff --git a/ref-test/data/.gitignore b/ref-test/data/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/ref-test/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/ref-test/main.py b/ref-test/main.py index 2a3173c..d2f4eb3 100644 --- a/ref-test/main.py +++ b/ref-test/main.py @@ -9,7 +9,7 @@ from pymongo.errors import ConnectionFailure from flask_wtf.csrf import CSRFProtect, CSRFError 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.config.from_object('config.DevelopmentConfig') @@ -37,7 +37,7 @@ if __name__ == '__main__': if not check_keyfile_exists(): generate_keyfile() - from common import cookie_consent + from common.blueprints import cookie_consent from admin.views import views as admin_views from admin.auth import auth as admin_auth diff --git a/ref-test/quiz/views.py b/ref-test/quiz/views.py index 3f0bb35..cd88bd2 100644 --- a/ref-test/quiz/views.py +++ b/ref-test/quiz/views.py @@ -3,7 +3,7 @@ from datetime import datetime from uuid import uuid4 from main import db -from security import encrypt +from common.security import encrypt views = Blueprint( 'quiz_views', From 6929136f905a04b8b1d5811209ed5459219cbcd9 Mon Sep 17 00:00:00 2001 From: viveksantayana Date: Sun, 28 Nov 2021 17:28:14 +0000 Subject: [PATCH 009/250] Added functionality for default datasets. Incorporated dataset selector into test creation. --- ref-test/admin/models/forms.py | 4 +- ref-test/admin/models/tests.py | 24 +++- ref-test/admin/static/css/style.css | 18 +-- ref-test/admin/static/js/script.js | 131 +++++++++++++----- .../templates/admin/components/navbar.html | 2 +- .../templates/admin/settings/questions.html | 112 ++++++++++++++- .../admin/templates/admin/settings/users.html | 4 +- ref-test/admin/templates/admin/tests.html | 8 +- ref-test/admin/views.py | 94 +++++++++++-- ref-test/common/data_tools.py | 32 +++-- ref-test/quiz/views.py | 6 +- 11 files changed, 350 insertions(+), 85 deletions(-) diff --git a/ref-test/admin/models/forms.py b/ref-test/admin/models/forms.py index 9e762f9..ee05b74 100644 --- a/ref-test/admin/models/forms.py +++ b/ref-test/admin/models/forms.py @@ -55,6 +55,8 @@ class CreateTest(FlaskForm): ] 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) + dataset = SelectField('Question Dataset') class UploadDataForm(FlaskForm): - data_file = FileField('Data File', validators=[FileRequired(), FileAllowed(['json'])]) \ No newline at end of file + data_file = FileField('Data File', validators=[FileRequired(), FileAllowed(['json'])]) + default = BooleanField('Make Default', render_kw={'checked': True}) \ No newline at end of file diff --git a/ref-test/admin/models/tests.py b/ref-test/admin/models/tests.py index 45196a7..a0e44cd 100644 --- a/ref-test/admin/models/tests.py +++ b/ref-test/admin/models/tests.py @@ -3,17 +3,20 @@ from datetime import datetime from uuid import uuid4 from flask import flash, jsonify import secrets +import os +from json import dump, loads -from main import db +from main import app, db from common.security import encrypt 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, dataset=None): self._id = _id self.start_date = start_date self.expiry_date = expiry_date self.time_limit = None if time_limit == 'none' or time_limit == '' else time_limit self.creator = creator + self.dataset = dataset def create(self): test = { @@ -23,9 +26,16 @@ class Test: 'expiry_date': self.expiry_date, 'time_limit': self.time_limit, 'creator': encrypt(self.creator), - 'test_code': secrets.token_hex(6).upper() + 'test_code': secrets.token_hex(6).upper(), + 'dataset': self.dataset } if db.tests.insert_one(test): + dataset_file_path = os.path.join(app.config["DATA_FILE_DIRECTORY"],self.dataset) + with open(dataset_file_path, 'r') as dataset_file: + data = loads(dataset_file.read()) + data['meta']['tests'].append(self._id) + with open(dataset_file_path, 'w') as dataset_file: + dump(data, dataset_file, indent=2) flash(f'Created a new exam with Exam Code {self.render_test_code(test["test_code"])}.', 'success') return jsonify({'success': test}), 200 return jsonify({'error': f'Could not create exam. An error occurred.'}), 400 @@ -54,7 +64,15 @@ class Test: return test_code.replace('—', '') def delete(self): + if self.dataset is None: + self.dataset = db.tests.find_one({'_id': self._id})['dataset'] if db.tests.delete_one({'_id': self._id}): + dataset_file_path = os.path.join(app.config["DATA_FILE_DIRECTORY"],self.dataset) + with open(dataset_file_path, 'r') as dataset_file: + data = loads(dataset_file.read()) + data['meta']['tests'].remove(self._id) + with open(dataset_file_path, 'w') as dataset_file: + dump(data, dataset_file, indent=2) message = 'Deleted exam.' flash(message, 'alert') return jsonify({'success': message}), 200 diff --git a/ref-test/admin/static/css/style.css b/ref-test/admin/static/css/style.css index 083c8fd..d719dc2 100644 --- a/ref-test/admin/static/css/style.css +++ b/ref-test/admin/static/css/style.css @@ -132,16 +132,11 @@ table.dataTable { width: 100%; } -.user-table-row { +.table-row { vertical-align: middle; } -.user-row-actions { - text-align: center; - white-space: nowrap; -} - -.test-row-actions { +.row-actions { text-align: center; white-space: nowrap; } @@ -153,8 +148,8 @@ table.dataTable { text-align:center; } -.user-row-actions button { - margin: 0px 10px; +.row-actions button, .row-actions a { + margin: 0px 5px; } #cookie-alert { @@ -214,6 +209,11 @@ table.dataTable { font-size: 20px; } +.form-upload { + margin: 2rem 0; + font-size: 14pt; +} + /* Fallback for Edge -------------------------------------------------- */ @supports (-ms-ime-align: auto) { diff --git a/ref-test/admin/static/js/script.js b/ref-test/admin/static/js/script.js index e6e4d73..ad4cd34 100644 --- a/ref-test/admin/static/js/script.js +++ b/ref-test/admin/static/js/script.js @@ -20,7 +20,7 @@ $('form[name=form-register]').submit(function(event) { var alert = document.getElementById('alert-box'); var data = $form.serialize(); - alert.innerHTML = '' + alert.innerHTML = ''; $.ajax({ url: window.location.pathname, @@ -62,7 +62,7 @@ $('form[name=form-login]').submit(function(event) { var alert = document.getElementById('alert-box'); var data = $form.serialize(); - alert.innerHTML = '' + alert.innerHTML = ''; $.ajax({ url: window.location.pathname, @@ -104,7 +104,7 @@ $('form[name=form-reset]').submit(function(event) { var alert = document.getElementById('alert-box'); var data = $form.serialize(); - alert.innerHTML = '' + alert.innerHTML = ''; $.ajax({ url: window.location.pathname, @@ -146,7 +146,7 @@ $('form[name=form-update-password]').submit(function(event) { var alert = document.getElementById('alert-box'); var data = $form.serialize(); console.log(data) - alert.innerHTML = '' + alert.innerHTML = ''; $.ajax({ url: window.location.pathname, @@ -188,7 +188,7 @@ $('form[name=form-create-user]').submit(function(event) { var alert = document.getElementById('alert-box'); var data = $form.serialize(); - alert.innerHTML = '' + alert.innerHTML = ''; $.ajax({ url: window.location.pathname, @@ -230,7 +230,7 @@ $('form[name=form-delete-user]').submit(function(event) { var alert = document.getElementById('alert-box'); var data = $form.serialize(); - alert.innerHTML = '' + alert.innerHTML = ''; $.ajax({ url: window.location.pathname, @@ -272,7 +272,7 @@ $('form[name=form-update-user]').submit(function(event) { var alert = document.getElementById('alert-box'); var data = $form.serialize(); - alert.innerHTML = '' + alert.innerHTML = ''; $.ajax({ url: window.location.pathname, @@ -314,7 +314,7 @@ $('form[name=form-update-account]').submit(function(event) { var alert = document.getElementById('alert-box'); var data = $form.serialize(); - alert.innerHTML = '' + alert.innerHTML = ''; $.ajax({ url: window.location.pathname, @@ -355,7 +355,7 @@ $('form[name=form-create-test]').submit(function(event) { var $form = $(this); var alert = document.getElementById('alert-box'); var data = $form.serialize(); - alert.innerHTML = '' + alert.innerHTML = ''; $.ajax({ url: window.location.pathname, @@ -398,7 +398,7 @@ $('form[name=form-upload-questions]').submit(function(event) { var data = new FormData($form[0]); var file = $('input[name=data_file]')[0].files[0] data.append('file', file) - alert.innerHTML = '' + alert.innerHTML = ''; $.ajax({ url: window.location.pathname, @@ -407,25 +407,7 @@ $('form[name=form-upload-questions]').submit(function(event) { processData: false, contentType: false, success: function(response) { - if (typeof response.success === 'string' || response.success instanceof String) { - alert.innerHTML = alert.innerHTML + ` - - `; - } else if (response.success instanceof Array) { - for (var i = 0; i < response.success.length; i ++) { - alert.innerHTML = alert.innerHTML + ` - - `; - } - } + window.location.reload(); }, error: function(response) { if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) { @@ -455,10 +437,10 @@ $('form[name=form-upload-questions]').submit(function(event) { // Edit and Delete Test Button Handlers - $('.delete-test').click(function(event) { _id = $(this).data('_id') + $.ajax({ url: `/admin/tests/delete/${_id}`, @@ -492,6 +474,89 @@ $('.delete-test').click(function(event) { event.preventDefault(); }); +// Edit and Delete Dataset Button Handlers +$('.delete-question-dataset').click(function(event) { + + var alert = document.getElementById('alert-box'); + alert.innerHTML = ''; + + var filename = $(this).data('filename'); + var disabled = $(this).hasClass('disabled'); + + if ( !disabled ) { + $.ajax({ + url: `/admin/settings/questions/delete/${filename}`, + type: 'GET', + success: function(response) { + window.location.reload(); + }, + error: function(response) { + if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) { + alert.innerHTML = alert.innerHTML + ` + + `; + } else if (response.responseJSON.error instanceof Array) { + for (var i = 0; i < response.responseJSON.error.length; i ++) { + alert.innerHTML = alert.innerHTML + ` + + `; + } + } + } + }); + }; + event.preventDefault(); +}); + +$('.edit-question-dataset').click(function(event) { + + var alert = document.getElementById('alert-box'); + alert.innerHTML = ''; + + var filename = $(this).data('filename'); + var disabled = $(this).hasClass('disabled'); + + if ( !disabled ) { + $.ajax({ + url: `/admin/settings/questions/default/${filename}`, + type: 'GET', + success: function(response) { + window.location.reload(); + }, + error: function(response) { + if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) { + alert.innerHTML = alert.innerHTML + ` + + `; + } else if (response.responseJSON.error instanceof Array) { + for (var i = 0; i < response.responseJSON.error.length; i ++) { + alert.innerHTML = alert.innerHTML + ` + + `; + } + } + } + }); + }; + event.preventDefault(); +}); + // Dismiss Cookie Alert $('#dismiss-cookie-alert').click(function(event){ @@ -503,13 +568,13 @@ $('#dismiss-cookie-alert').click(function(event){ }, dataType: 'json', success: function(response){ - console.log(response) + console.log(response); }, error: function(response){ - console.log(response) + console.log(response); } }) - event.preventDefault() + event.preventDefault(); }) \ No newline at end of file diff --git a/ref-test/admin/templates/admin/components/navbar.html b/ref-test/admin/templates/admin/components/navbar.html index 12fe70a..7fa655f 100644 --- a/ref-test/admin/templates/admin/components/navbar.html +++ b/ref-test/admin/templates/admin/components/navbar.html @@ -24,7 +24,7 @@ View Results
  • From ba082d4ed7515e108469032cb906df5402750c46 Mon Sep 17 00:00:00 2001 From: viveksantayana Date: Sat, 4 Dec 2021 12:48:01 +0000 Subject: [PATCH 078/250] Typo correction --- ref-test/admin/templates/admin/result-detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ref-test/admin/templates/admin/result-detail.html b/ref-test/admin/templates/admin/result-detail.html index 44b264b..b737789 100644 --- a/ref-test/admin/templates/admin/result-detail.html +++ b/ref-test/admin/templates/admin/result-detail.html @@ -13,7 +13,7 @@
    Candidate

    - {{ entry.name.surname}}, {{ entry.name.first_name }} + {{ entry.name.surname }}, {{ entry.name.first_name }}

  • From 4b671242ffc9f81f1c1fe0e8ab2bcf9a52adafb3 Mon Sep 17 00:00:00 2001 From: viveksantayana Date: Sat, 4 Dec 2021 12:48:01 +0000 Subject: [PATCH 079/250] Typo correction --- ref-test/admin/templates/admin/result-detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ref-test/admin/templates/admin/result-detail.html b/ref-test/admin/templates/admin/result-detail.html index 44b264b..b737789 100644 --- a/ref-test/admin/templates/admin/result-detail.html +++ b/ref-test/admin/templates/admin/result-detail.html @@ -13,7 +13,7 @@
    Candidate

    - {{ entry.name.surname}}, {{ entry.name.first_name }} + {{ entry.name.surname }}, {{ entry.name.first_name }}

  • From f47e22ccae4cd768acb8bc5948a33a3bc57e70d8 Mon Sep 17 00:00:00 2001 From: viveksantayana Date: Sat, 4 Dec 2021 12:48:01 +0000 Subject: [PATCH 080/250] Typo correction --- ref-test/admin/templates/admin/result-detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ref-test/admin/templates/admin/result-detail.html b/ref-test/admin/templates/admin/result-detail.html index 44b264b..b737789 100644 --- a/ref-test/admin/templates/admin/result-detail.html +++ b/ref-test/admin/templates/admin/result-detail.html @@ -13,7 +13,7 @@
    Candidate

    - {{ entry.name.surname}}, {{ entry.name.first_name }} + {{ entry.name.surname }}, {{ entry.name.first_name }}

  • From 4902d40787b738b4d67241cb4db06a7eeca4663a Mon Sep 17 00:00:00 2001 From: viveksantayana Date: Sat, 4 Dec 2021 15:41:24 +0000 Subject: [PATCH 081/250] Refactored form model for custom validators --- ref-test/admin/models/{forms.py => forms/__init__.py} | 10 ++++++++-- ref-test/admin/models/forms/validators.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) rename ref-test/admin/models/{forms.py => forms/__init__.py} (95%) create mode 100644 ref-test/admin/models/forms/validators.py diff --git a/ref-test/admin/models/forms.py b/ref-test/admin/models/forms/__init__.py similarity index 95% rename from ref-test/admin/models/forms.py rename to ref-test/admin/models/forms/__init__.py index 9ca6e1e..5bf60a4 100644 --- a/ref-test/admin/models/forms.py +++ b/ref-test/admin/models/forms/__init__.py @@ -1,9 +1,11 @@ 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, IntegerField from wtforms.validators import InputRequired, Email, Length, EqualTo, Optional from datetime import date, timedelta +from .validators import value + class LoginForm(FlaskForm): username = StringField('Username', validators=[InputRequired(), Length(min=4, max=15)]) password = PasswordField('Password', validators=[InputRequired(), Length(min=6, max=30, message='The password must be between 6 and 20 characters long.')]) @@ -53,4 +55,8 @@ class CreateTest(FlaskForm): class UploadDataForm(FlaskForm): data_file = FileField('Data File', validators=[FileRequired(), FileAllowed(['json'])]) - default = BooleanField('Make Default', render_kw={'checked': True}) \ No newline at end of file + default = BooleanField('Make Default', render_kw={'checked': True}) + +class AddTimeAdjustment(FlaskForm): + time = IntegerField('Extra Time (Minutes)', validators=[InputRequired(), value(max=60)]) + \ No newline at end of file diff --git a/ref-test/admin/models/forms/validators.py b/ref-test/admin/models/forms/validators.py new file mode 100644 index 0000000..7caeb05 --- /dev/null +++ b/ref-test/admin/models/forms/validators.py @@ -0,0 +1,11 @@ +from wtforms.validators import ValidationError + +def value(min=0, max=None): + message = f'Value must be between {min} and {max}.' + + def _length(form, field): + value = field.data or 0 + if value < min or max != None and value > max: + raise ValidationError(message) + + return _length \ No newline at end of file From 8f8a12b6097f2a498f64e292a7dcea12d127c9e8 Mon Sep 17 00:00:00 2001 From: viveksantayana Date: Sat, 4 Dec 2021 15:41:24 +0000 Subject: [PATCH 082/250] Refactored form model for custom validators --- ref-test/admin/models/{forms.py => forms/__init__.py} | 10 ++++++++-- ref-test/admin/models/forms/validators.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) rename ref-test/admin/models/{forms.py => forms/__init__.py} (95%) create mode 100644 ref-test/admin/models/forms/validators.py diff --git a/ref-test/admin/models/forms.py b/ref-test/admin/models/forms/__init__.py similarity index 95% rename from ref-test/admin/models/forms.py rename to ref-test/admin/models/forms/__init__.py index 9ca6e1e..5bf60a4 100644 --- a/ref-test/admin/models/forms.py +++ b/ref-test/admin/models/forms/__init__.py @@ -1,9 +1,11 @@ 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, IntegerField from wtforms.validators import InputRequired, Email, Length, EqualTo, Optional from datetime import date, timedelta +from .validators import value + class LoginForm(FlaskForm): username = StringField('Username', validators=[InputRequired(), Length(min=4, max=15)]) password = PasswordField('Password', validators=[InputRequired(), Length(min=6, max=30, message='The password must be between 6 and 20 characters long.')]) @@ -53,4 +55,8 @@ class CreateTest(FlaskForm): class UploadDataForm(FlaskForm): data_file = FileField('Data File', validators=[FileRequired(), FileAllowed(['json'])]) - default = BooleanField('Make Default', render_kw={'checked': True}) \ No newline at end of file + default = BooleanField('Make Default', render_kw={'checked': True}) + +class AddTimeAdjustment(FlaskForm): + time = IntegerField('Extra Time (Minutes)', validators=[InputRequired(), value(max=60)]) + \ No newline at end of file diff --git a/ref-test/admin/models/forms/validators.py b/ref-test/admin/models/forms/validators.py new file mode 100644 index 0000000..7caeb05 --- /dev/null +++ b/ref-test/admin/models/forms/validators.py @@ -0,0 +1,11 @@ +from wtforms.validators import ValidationError + +def value(min=0, max=None): + message = f'Value must be between {min} and {max}.' + + def _length(form, field): + value = field.data or 0 + if value < min or max != None and value > max: + raise ValidationError(message) + + return _length \ No newline at end of file From 49b0ea14f0de9e00e7fb811584ada0a867641017 Mon Sep 17 00:00:00 2001 From: viveksantayana Date: Sat, 4 Dec 2021 15:41:24 +0000 Subject: [PATCH 083/250] Refactored form model for custom validators --- ref-test/admin/models/{forms.py => forms/__init__.py} | 10 ++++++++-- ref-test/admin/models/forms/validators.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) rename ref-test/admin/models/{forms.py => forms/__init__.py} (95%) create mode 100644 ref-test/admin/models/forms/validators.py diff --git a/ref-test/admin/models/forms.py b/ref-test/admin/models/forms/__init__.py similarity index 95% rename from ref-test/admin/models/forms.py rename to ref-test/admin/models/forms/__init__.py index 9ca6e1e..5bf60a4 100644 --- a/ref-test/admin/models/forms.py +++ b/ref-test/admin/models/forms/__init__.py @@ -1,9 +1,11 @@ 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, IntegerField from wtforms.validators import InputRequired, Email, Length, EqualTo, Optional from datetime import date, timedelta +from .validators import value + class LoginForm(FlaskForm): username = StringField('Username', validators=[InputRequired(), Length(min=4, max=15)]) password = PasswordField('Password', validators=[InputRequired(), Length(min=6, max=30, message='The password must be between 6 and 20 characters long.')]) @@ -53,4 +55,8 @@ class CreateTest(FlaskForm): class UploadDataForm(FlaskForm): data_file = FileField('Data File', validators=[FileRequired(), FileAllowed(['json'])]) - default = BooleanField('Make Default', render_kw={'checked': True}) \ No newline at end of file + default = BooleanField('Make Default', render_kw={'checked': True}) + +class AddTimeAdjustment(FlaskForm): + time = IntegerField('Extra Time (Minutes)', validators=[InputRequired(), value(max=60)]) + \ No newline at end of file diff --git a/ref-test/admin/models/forms/validators.py b/ref-test/admin/models/forms/validators.py new file mode 100644 index 0000000..7caeb05 --- /dev/null +++ b/ref-test/admin/models/forms/validators.py @@ -0,0 +1,11 @@ +from wtforms.validators import ValidationError + +def value(min=0, max=None): + message = f'Value must be between {min} and {max}.' + + def _length(form, field): + value = field.data or 0 + if value < min or max != None and value > max: + raise ValidationError(message) + + return _length \ No newline at end of file From 0eda083bf2f66365787efe1a93a73830f0fbb594 Mon Sep 17 00:00:00 2001 From: viveksantayana Date: Sat, 4 Dec 2021 15:41:24 +0000 Subject: [PATCH 084/250] Refactored form model for custom validators --- ref-test/admin/models/{forms.py => forms/__init__.py} | 10 ++++++++-- ref-test/admin/models/forms/validators.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) rename ref-test/admin/models/{forms.py => forms/__init__.py} (95%) create mode 100644 ref-test/admin/models/forms/validators.py diff --git a/ref-test/admin/models/forms.py b/ref-test/admin/models/forms/__init__.py similarity index 95% rename from ref-test/admin/models/forms.py rename to ref-test/admin/models/forms/__init__.py index 9ca6e1e..5bf60a4 100644 --- a/ref-test/admin/models/forms.py +++ b/ref-test/admin/models/forms/__init__.py @@ -1,9 +1,11 @@ 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, IntegerField from wtforms.validators import InputRequired, Email, Length, EqualTo, Optional from datetime import date, timedelta +from .validators import value + class LoginForm(FlaskForm): username = StringField('Username', validators=[InputRequired(), Length(min=4, max=15)]) password = PasswordField('Password', validators=[InputRequired(), Length(min=6, max=30, message='The password must be between 6 and 20 characters long.')]) @@ -53,4 +55,8 @@ class CreateTest(FlaskForm): class UploadDataForm(FlaskForm): data_file = FileField('Data File', validators=[FileRequired(), FileAllowed(['json'])]) - default = BooleanField('Make Default', render_kw={'checked': True}) \ No newline at end of file + default = BooleanField('Make Default', render_kw={'checked': True}) + +class AddTimeAdjustment(FlaskForm): + time = IntegerField('Extra Time (Minutes)', validators=[InputRequired(), value(max=60)]) + \ No newline at end of file diff --git a/ref-test/admin/models/forms/validators.py b/ref-test/admin/models/forms/validators.py new file mode 100644 index 0000000..7caeb05 --- /dev/null +++ b/ref-test/admin/models/forms/validators.py @@ -0,0 +1,11 @@ +from wtforms.validators import ValidationError + +def value(min=0, max=None): + message = f'Value must be between {min} and {max}.' + + def _length(form, field): + value = field.data or 0 + if value < min or max != None and value > max: + raise ValidationError(message) + + return _length \ No newline at end of file From 6855ddfdcb6550dcbd5c6186655dd5ba88cccabf Mon Sep 17 00:00:00 2001 From: viveksantayana Date: Sat, 4 Dec 2021 15:41:47 +0000 Subject: [PATCH 085/250] Added server and admin-side time limit adjustments --- ref-test/admin/models/tests.py | 9 +- ref-test/admin/static/css/style.css | 6 +- ref-test/admin/static/js/script.js | 76 ++++++-- .../admin/components/client-alerts.html | 2 +- ref-test/admin/templates/admin/test.html | 180 ++++++++++++++++++ ref-test/admin/templates/admin/tests.html | 6 +- ref-test/admin/views.py | 24 ++- 7 files changed, 270 insertions(+), 33 deletions(-) create mode 100644 ref-test/admin/templates/admin/test.html diff --git a/ref-test/admin/models/tests.py b/ref-test/admin/models/tests.py index 656c424..a1b3397 100644 --- a/ref-test/admin/models/tests.py +++ b/ref-test/admin/models/tests.py @@ -41,13 +41,14 @@ class Test: return jsonify({'error': f'Could not create exam. An error occurred.'}), 400 def add_time_adjustment(self, time_adjustment): - code = { + adjustment = { '_id': uuid4().hex, - 'user_code': secrets.token_hex(2).upper(), + 'user_code': secrets.token_hex(3).upper(), 'time_adjustment': time_adjustment } - if db.tests.find_one_and_update({'_id': self._id}, {'$push': {'time_adjustments': code}},upsert=False): - return jsonify({'success': code}) + if db.tests.find_one_and_update({'_id': self._id}, {'$push': {'time_adjustments': adjustment}},upsert=False): + flash(f'Time adjustment for {adjustment["time_adjustment"]} minutes has been added. This can be enabled using the user code {adjustment["user_code"]}.') + return jsonify({'success': adjustment}) return jsonify({'error': 'Failed to add the time adjustment. An error occurred.'}), 400 def remove_time_adjustment(self, _id): diff --git a/ref-test/admin/static/css/style.css b/ref-test/admin/static/css/style.css index 8107688..137d546 100644 --- a/ref-test/admin/static/css/style.css +++ b/ref-test/admin/static/css/style.css @@ -214,11 +214,15 @@ table.dataTable { font-size: 14pt; } -.result-action-buttons { +.result-action-buttons, .test-action { margin: 5px auto; width: fit-content; } +.accordion-item { + background-color: unset; +} + /* Fallback for Edge -------------------------------------------------- */ @supports (-ms-ime-align: auto) { diff --git a/ref-test/admin/static/js/script.js b/ref-test/admin/static/js/script.js index d3362b5..184374a 100644 --- a/ref-test/admin/static/js/script.js +++ b/ref-test/admin/static/js/script.js @@ -63,22 +63,27 @@ $('form[name=form-upload-questions]').submit(function(event) { }); // Edit and Delete Test Button Handlers -$('.delete-test').click(function(event) { +$('.test-action').click(function(event) { - let _id = $(this).data('_id') + let _id = $(this).data('_id'); + let action = $(this).data('action'); - $.ajax({ - url: `/admin/tests/delete/`, - type: 'POST', - data: JSON.stringify({'_id': _id}), - contentType: 'application/json', - success: function(response) { - window.location.href = '/admin/tests/'; - }, - error: function(response){ - error_response(response); - }, - }); + if (action == 'delete') { + $.ajax({ + url: `/admin/tests/delete/`, + type: 'POST', + data: JSON.stringify({'_id': _id}), + 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(); }); @@ -109,11 +114,11 @@ $('.edit-question-dataset').click(function(event) { function error_response(response) { - var alert = $("#alert-box"); - alert.html(''); + const $alert = $("#alert-box"); + $alert.html(''); if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) { - alert.html(` + $alert.html(`