diff --git a/ref-test/app/admin/views.py b/ref-test/app/admin/views.py index b24d0a9..f4c57c1 100644 --- a/ref-test/app/admin/views.py +++ b/ref-test/app/admin/views.py @@ -9,7 +9,7 @@ from flask import Blueprint, jsonify, render_template, redirect, request, sessio from flask.helpers import flash, url_for from flask_login import current_user, login_required -from datetime import date, datetime, timedelta +from datetime import date, datetime from json import loads import secrets @@ -83,8 +83,7 @@ def _register(): new_user = User() new_user.set_username(request.form.get('username').lower()) new_user.set_email(request.form.get('email').lower()) - new_user.set_password(request.form.get('password')) - success, message = new_user.register() + success, message = new_user.register(password=request.form.get('password')) if success: flash(message=f'{message} Please log in to continue.', category='success') session['remembered_username'] = request.form.get('username').lower() @@ -146,11 +145,11 @@ def _users(): if request.method == 'POST': if form.validate_on_submit(): password = request.form.get('password') + password = secrets.token_hex(12) if not password else password new_user = User() new_user.set_username(request.form.get('username').lower()) - new_user.set_password(secrets.token_hex(12)) if not password else password new_user.set_email(request.form.get('email')) - success, message = new_user.register(notify=request.form.get('notify')) + success, message = new_user.register(notify=request.form.get('notify'), password=password) if success: return jsonify({'success': message}), 200 return jsonify({'error': message}), 401 errors = [*form.username.errors, *form.email.errors, *form.password.errors] @@ -368,6 +367,7 @@ def _view_entry(id:str=None): success, message = entry.delete() if success: flash(message, 'success') + entry.notify_result() return jsonify({'success': message}), 200 return jsonify({'error': message}),400 if not entry: diff --git a/ref-test/app/models/entry.py b/ref-test/app/models/entry.py index 30313c5..557003a 100644 --- a/ref-test/app/models/entry.py +++ b/ref-test/app/models/entry.py @@ -1,10 +1,11 @@ -from ..modules import db +from ..modules import db, mail from ..tools.forms import JsonEncodedDict from ..tools.encryption import decrypt, encrypt from ..tools.logs import write from .test import Test from flask_login import current_user +from flask_mail import Message from datetime import datetime, timedelta from uuid import uuid4 @@ -110,4 +111,67 @@ class Entry(db.Model): db.session.delete(self) db.session.commit() write('system.log', f'The entry {id} by {name} has been deleted by {current_user.get_username()}.') - return True, 'Entry deleted.' \ No newline at end of file + return True, 'Entry deleted.' + + def notify_result(self): + score = round(100*self.result['score']/self.result['max']) + tags_low = { tag: tag_result['max'] - tag_result['scored'] for tag, tag_result in self.result['tags'].items() } + sorted_low_tags = sorted(tags_low.items(), key=lambda x: x[1], reverse=True) + tag_output = [ tag[0] for tag in sorted_low_tags[0:3] if tag[1] > 3] + revision_plain = '' + revision_html = '' + if self.result['grade'] == 'pass': + flavour_text = """Well done on successfully completing the refereeing theory exam. We really appreciate members of our community taking the time to get qualified to referee. + """ + elif self.result['grade'] == 'merit': + flavour_text = """Congratulations on achieving a merit in the refereeing exam. We are delighted that members of our community work so hard with refereeing. + """ + elif self.result['grade'] == 'fail': + flavour_text = """Unfortunately, you were not successful in passing the theory exam in this attempt. We hope that this does not dissuade you, and that you try again in the future. + """ + revision_plain = f"""Based on your answers, we would also suggest you brush up on the following topics for your next attempt:\n\n + {','.join(tag_output)}\n\n + """ + revision_html = f"""

Based on your answers, we would also suggest you brush up on the following topics for your next attempt:

+ + """ + email = Message( + subject='RefTest | SKA Refereeing Theory Exam Results', + recipients=[self.get_email()], + body=f""" + SKA Refereeing Theory Exam + Candidate Results + Dear {self.get_first_name()}, + This email is to confirm that you have taken the SKA Refereeing Theory Exam. Your submission has been evaluated and your results are as follows: + {self.get_surname()}, {self.get_first_name()} + Email Address: {self.get_email()} + {f'Club: {self.get_club()}' if self.club else ''} + Date of Exam: {self.end_time.strftime('%d %b %Y')} + Score: {score}% + Grade: {self.result['grade']} + {flavour_text} + {revision_plain} + Thank you for taking the time to become a qualified referee. + Best wishes, + SKA Refereeing + """, + html=f""" +

SKA Refereeing Theory Exam

+

Candidate Results

+

Dear {self.get_first_name()},

+

This email is to confirm that you have taken the SKA Refereeing Theory Exam. Your submission has been evaluated and your results are as follows:

+

{self.get_surname()}, {self.get_first_name()}

+

Email Address: {self.get_email()}

+ {f'

Club: {self.get_club()}

' if self.club else ''} +

{score}%

+

{self.result['grade']}

+

{flavour_text}

+ {revision_html} +

Thank you for taking the time to become a qualified referee.

+

Have a nice day!

+

Best wishes,
SKA Refereeing

+ """ + ) + mail.send(email) \ No newline at end of file diff --git a/ref-test/app/models/user.py b/ref-test/app/models/user.py index d31a40b..9999a9d 100644 --- a/ref-test/app/models/user.py +++ b/ref-test/app/models/user.py @@ -1,10 +1,11 @@ -from ..modules import db +from ..modules import db, mail from ..tools.encryption import decrypt, encrypt from ..tools.logs import write from flask import flash, jsonify, session from flask.helpers import url_for from flask_login import current_user, login_user, logout_user, UserMixin +from flask_mail import Message from werkzeug.security import check_password_hash, generate_password_hash import secrets @@ -52,15 +53,44 @@ class User(UserMixin, db.Model): def get_email(self): return decrypt(self.email) - def register(self, notify:bool=False): + def register(self, notify:bool=False, password:str=None): self.generate_id() users = User.query.all() for user in users: if user.get_username() == self.get_username(): return False, f'Username {self.get_username()} already in use.' if user.get_email() == self.get_email(): return False, f'Email address {self.get_email()} already in use.' + self.set_password(password=password) db.session.add(self) db.session.commit() write('users.log', f'User \'{self.get_username()}\' was created with id \'{self.id}\'.') + if notify: + email = Message( + subject='RefTest | Registration Confirmation', + recipients=[self.email], + body=f""" + Hello {self.get_username()},\n\n + You have been registered as an administrator on the SKA RefTest App!\n\n + You can access your account using the username '{self.get_username()}'\n\n + Your password is as follows:\n\n + {password}\n\n + You can log in to the admin console via the following URL, where you can administer the test or change your password:\n\n + {url_for('admin._home', _external=True)}\n\n + Have a nice day!\n\n + SKA Refereeing + """, + html=f""" +

Hello {self.get_username()},

+

You have been registered as an administrator on the SKA RefTest App!

+

You can access your account using the username '{self.get_username()}'

+

Your password is as follows:

+ {password} +

You can log in to the admin console via the following URL, where you can administer the test or change your password:

+

{url_for('admin._home', _external=True)}

+

Have a nice day!

+

SKA Refereeing

+ """ + ) + mail.send(email) return True, f'User {self.get_username()} was created successfully.' def login(self, remember:bool=False): @@ -80,6 +110,39 @@ class User(UserMixin, db.Model): self.reset_token = secrets.token_urlsafe(16) self.verification_token = secrets.token_urlsafe(16) db.session.commit() + email = Message( + subject='RefTest | Password Reset', + recipients=[self.get_email()], + body=f""" + Hello {self.get_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 + 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', token = self.reset_token, verification = self.verification_token, _external = True)}\n\n + Hopefully, this should enable access to your account once again.\n\n + Have a nice day!\n\n + SKA Refereeing + """, + html=f""" +

Hello {self.get_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.

+

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. Copy and paste the following link in a web browser. Please note that this token is only valid once:

+

{url_for('admin_auth.reset_gateway', token = self.reset_token, verification = self.verification_token, _external = True)}

+

Hopefully, this should enable access to your account once again.

+

Have a nice day!

+

SKA Refereeing

+ """ + ) + mail.send(email) print('Password', new_password) print('Reset Token', self.reset_token) print('Verification Token', self.verification_token) @@ -92,16 +155,69 @@ class User(UserMixin, db.Model): def delete(self, notify:bool=False): username = self.get_username() + email_address = self.get_email() db.session.delete(self) db.session.commit() message = f'User \'{username}\' was deleted by \'{current_user.get_username()}\'.' write('users.log', message) + if notify: + email = Message( + subject='RefTest | Account Deletion', + recipients=[email_address], + bcc=[current_user.get_email()], + body=f""" + Hello {username},\n\n + Your administrator account for the SKA RefTest App, as well as all data associated with the account, have been deleted by {current_user.get_username()}.\n\n + If you believe this was done in error, please contact them immediately.\n\n + If you would like to register to administer the app, please ask an existing administrator to create a new account.\n\n + Have a nice day!\n\n + SKA Refereeing + """, + html=f""" +

Hello {username},

+

Your administrator account for the SKA RefTest App, as well as all data associated with the account, have been deleted by {current_user.get_username()}.

+

If you believe this was done in error, please contact them immediately.

+

If you would like to register to administer the app, please ask an existing administrator to create a new account.

+

Have a nice day!

+

SKA Refereeing

+ """ + ) + mail.send(email) return True, message def update(self, password:str=None, email:str=None, notify:bool=False): if not password and not email: return False, 'There were no changes requested.' if password: self.set_password(password) + old_email = self.get_email() if email: self.set_email(email) db.session.commit() write('system.log', f'Information for user {self.get_username()} has been updated by {current_user.get_username()}.') + if notify: + message = Message( + subject='RefTest | Account Update', + recipients=[email], + bcc=[old_email,current_user.get_email()], + body=f""" + Hello {self.get_username()},\n\n + Your administrator account for the SKA RefTest App has been updated by {current_user.get_username()}.\n\n + Your new account details are as follows:\n\n + Email: {email}\n + Password: {password if password else ''}\n\n + You can update your email address and password by logging in to the admin console using the following URL:\n\n + {url_for('admin._home', _external=True)}\n\n + Have a nice day!\n\n + SKA Refereeing + """, + html=f""" +

Hello {self.get_username()},

+

Your administrator account for the SKA RefTest App has been updated by {current_user.get_username()}.

+

Your new account details are as follows:

+

Email: {email}
Password: {password if password else ''}

+

You can update your email address and password by logging in to the admin console using the following URL:

+

{url_for('admin._home', _external=True)}

+

Have a nice day!

+

SKA Refereeing

+ """ + ) + mail.send(message) return True, f'Account {self.get_username()} has been updated.' diff --git a/ref-test/app/quiz/views.py b/ref-test/app/quiz/views.py index 6b6e309..8c8000c 100644 --- a/ref-test/app/quiz/views.py +++ b/ref-test/app/quiz/views.py @@ -75,25 +75,6 @@ def _result(): tags_low = { tag: tag_result['max'] - tag_result['scored'] for tag, tag_result in entry.result['tags'].items() } sorted_low_tags = sorted(tags_low.items(), key=lambda x: x[1], reverse=True) tag_output = [ tag[0] for tag in sorted_low_tags[0:3] if tag[1] > 3] - revision_plain = '' - revision_html = '' - if entry.result['grade'] == 'pass': - flavour_text_plain = """Well done on successfully completing the refereeing theory exam. We really appreciate members of our community taking the time to get qualified to referee. - """ - elif entry.result['grade'] == 'merit': - flavour_text_plain = """Congratulations on achieving a merit in the refereeing exam. We are delighted that members of our community work so hard with refereeing. - """ - elif entry.result['grade'] == 'fail': - flavour_text_plain = """Unfortunately, you were not successful in passing the theory exam in this attempt. We hope that this does not dissuade you, and that you try again in the future. - """ - revision_plain = f"""Based on your answers, we would also suggest you brush up on the following topics for your next attempt:\n\n - {','.join(tag_output)}\n\n - """ - revision_html = f"""

Based on your answers, we would also suggest you brush up on the following topics for your next attempt:

- - """ if not entry.status == 'late': - pass + entry.notify_result() return render_template('/quiz/result.html', entry=entry, score=score, tag_output=tag_output) \ No newline at end of file