Added email notifications

This commit is contained in:
Vivek Santayana 2022-06-16 12:46:03 +01:00
parent 4b08c830a1
commit e264b808fc
4 changed files with 190 additions and 29 deletions

View File

@ -9,7 +9,7 @@ from flask import Blueprint, jsonify, render_template, redirect, request, sessio
from flask.helpers import flash, url_for from flask.helpers import flash, url_for
from flask_login import current_user, login_required from flask_login import current_user, login_required
from datetime import date, datetime, timedelta from datetime import date, datetime
from json import loads from json import loads
import secrets import secrets
@ -83,8 +83,7 @@ def _register():
new_user = User() new_user = User()
new_user.set_username(request.form.get('username').lower()) new_user.set_username(request.form.get('username').lower())
new_user.set_email(request.form.get('email').lower()) new_user.set_email(request.form.get('email').lower())
new_user.set_password(request.form.get('password')) success, message = new_user.register(password=request.form.get('password'))
success, message = new_user.register()
if success: if success:
flash(message=f'{message} Please log in to continue.', category='success') flash(message=f'{message} Please log in to continue.', category='success')
session['remembered_username'] = request.form.get('username').lower() session['remembered_username'] = request.form.get('username').lower()
@ -146,11 +145,11 @@ def _users():
if request.method == 'POST': if request.method == 'POST':
if form.validate_on_submit(): if form.validate_on_submit():
password = request.form.get('password') password = request.form.get('password')
password = secrets.token_hex(12) if not password else password
new_user = User() new_user = User()
new_user.set_username(request.form.get('username').lower()) 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')) 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 if success: return jsonify({'success': message}), 200
return jsonify({'error': message}), 401 return jsonify({'error': message}), 401
errors = [*form.username.errors, *form.email.errors, *form.password.errors] errors = [*form.username.errors, *form.email.errors, *form.password.errors]
@ -368,6 +367,7 @@ def _view_entry(id:str=None):
success, message = entry.delete() success, message = entry.delete()
if success: if success:
flash(message, 'success') flash(message, 'success')
entry.notify_result()
return jsonify({'success': message}), 200 return jsonify({'success': message}), 200
return jsonify({'error': message}),400 return jsonify({'error': message}),400
if not entry: if not entry:

View File

@ -1,10 +1,11 @@
from ..modules import db from ..modules import db, mail
from ..tools.forms import JsonEncodedDict from ..tools.forms import JsonEncodedDict
from ..tools.encryption import decrypt, encrypt from ..tools.encryption import decrypt, encrypt
from ..tools.logs import write from ..tools.logs import write
from .test import Test from .test import Test
from flask_login import current_user from flask_login import current_user
from flask_mail import Message
from datetime import datetime, timedelta from datetime import datetime, timedelta
from uuid import uuid4 from uuid import uuid4
@ -111,3 +112,66 @@ class Entry(db.Model):
db.session.commit() db.session.commit()
write('system.log', f'The entry {id} by {name} has been deleted by {current_user.get_username()}.') write('system.log', f'The entry {id} by {name} has been deleted by {current_user.get_username()}.')
return True, 'Entry deleted.' 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"""<p>Based on your answers, we would also suggest you brush up on the following topics for your next attempt:</p>
<ul>
<li>{'</li><li>'.join(tag_output)}</li>
</ul>
"""
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"""
<h1>SKA Refereeing Theory Exam</h1>
<h2>Candidate Results</h2>
<p>Dear {self.get_first_name()},</p>
<p>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:</p>
<h3>{self.get_surname()}, {self.get_first_name()}</h3>
<p><strong>Email Address</strong>: {self.get_email()}</p>
{f'<p><strong>Club</strong>: {self.get_club()}</p>' if self.club else ''}
<h1>{score}%</h1>
<h2>{self.result['grade']}</h2>
<p>{flavour_text}</p>
{revision_html}
<p>Thank you for taking the time to become a qualified referee.</p>
<p>Have a nice day!</p>
<p>Best wishes, <br/> SKA Refereeing</p>
"""
)
mail.send(email)

View File

@ -1,10 +1,11 @@
from ..modules import db from ..modules import db, mail
from ..tools.encryption import decrypt, encrypt from ..tools.encryption import decrypt, encrypt
from ..tools.logs import write from ..tools.logs import write
from flask import flash, jsonify, session from flask import flash, jsonify, session
from flask.helpers import url_for from flask.helpers import url_for
from flask_login import current_user, login_user, logout_user, UserMixin 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 from werkzeug.security import check_password_hash, generate_password_hash
import secrets import secrets
@ -52,15 +53,44 @@ class User(UserMixin, db.Model):
def get_email(self): return decrypt(self.email) 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() self.generate_id()
users = User.query.all() users = User.query.all()
for user in users: for user in users:
if user.get_username() == self.get_username(): return False, f'Username {self.get_username()} already in use.' 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.' 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.add(self)
db.session.commit() db.session.commit()
write('users.log', f'User \'{self.get_username()}\' was created with id \'{self.id}\'.') 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"""
<p>Hello {self.get_username()},</p>
<p>You have been registered as an administrator on the SKA RefTest App!</p>
<p>You can access your account using the username '{self.get_username()}'</p>
<p>Your password is as follows:</p>
<strong>{password}</strong>
<p>You can log in to the admin console via the following URL, where you can administer the test or change your password:</p>
<p><a href='{url_for('admin._home', _external=True)}'>{url_for('admin._home', _external=True)}</a></p>
<p>Have a nice day!</p>
<p>SKA Refereeing</p>
"""
)
mail.send(email)
return True, f'User {self.get_username()} was created successfully.' return True, f'User {self.get_username()} was created successfully.'
def login(self, remember:bool=False): def login(self, remember:bool=False):
@ -80,6 +110,39 @@ class User(UserMixin, db.Model):
self.reset_token = secrets.token_urlsafe(16) self.reset_token = secrets.token_urlsafe(16)
self.verification_token = secrets.token_urlsafe(16) self.verification_token = secrets.token_urlsafe(16)
db.session.commit() 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"""
<p>Hello {self.get_username()},</p>
<p>This email was generated because we received a request to reset the password for your administrator account for the SKA RefTest app.</p>
<p>If you did not make this request, please ignore this email.</p>
<p>If you did make this request, then you have two options to recover your account.</p>
<p>Your password has been reset to the following:</p>
<strong>{new_password}</strong>
<p>You may use this to log back in to your account, and subsequently change your password to something more suitable.</p>
<p>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:</p>
<p><a href='{url_for('admin_auth.reset_gateway', token = self.reset_token, verification = self.verification_token, _external = True)}'>{url_for('admin_auth.reset_gateway', token = self.reset_token, verification = self.verification_token, _external = True)}</a></p>
<p>Hopefully, this should enable access to your account once again.</p>
<p>Have a nice day!</p>
<p>SKA Refereeing</p>
"""
)
mail.send(email)
print('Password', new_password) print('Password', new_password)
print('Reset Token', self.reset_token) print('Reset Token', self.reset_token)
print('Verification Token', self.verification_token) print('Verification Token', self.verification_token)
@ -92,16 +155,69 @@ class User(UserMixin, db.Model):
def delete(self, notify:bool=False): def delete(self, notify:bool=False):
username = self.get_username() username = self.get_username()
email_address = self.get_email()
db.session.delete(self) db.session.delete(self)
db.session.commit() db.session.commit()
message = f'User \'{username}\' was deleted by \'{current_user.get_username()}\'.' message = f'User \'{username}\' was deleted by \'{current_user.get_username()}\'.'
write('users.log', message) 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"""
<p>Hello {username},</p>
<p>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()}.</p>
<p>If you believe this was done in error, please contact them immediately.</p>
<p>If you would like to register to administer the app, please ask an existing administrator to create a new account.</p>
<p>Have a nice day!</p>
<p>SKA Refereeing</p>
"""
)
mail.send(email)
return True, message return True, message
def update(self, password:str=None, email:str=None, notify:bool=False): 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 not password and not email: return False, 'There were no changes requested.'
if password: self.set_password(password) if password: self.set_password(password)
old_email = self.get_email()
if email: self.set_email(email) if email: self.set_email(email)
db.session.commit() db.session.commit()
write('system.log', f'Information for user {self.get_username()} has been updated by {current_user.get_username()}.') 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 '<same as old>'}\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"""
<p>Hello {self.get_username()},</p>
<p>Your administrator account for the SKA RefTest App has been updated by {current_user.get_username()}.</p>
<p>Your new account details are as follows:</p>
<p>Email: {email} <br/> Password: <strong>{password if password else '<same as old>'}</strong></p>
<p>You can update your email address and password by logging in to the admin console using the following URL:</p>
<p><a href='{url_for('admin._home', _external=True)}'>{url_for('admin._home', _external=True)}</a></p>
<p>Have a nice day!</p>
<p>SKA Refereeing</p>
"""
)
mail.send(message)
return True, f'Account {self.get_username()} has been updated.' return True, f'Account {self.get_username()} has been updated.'

View File

@ -75,25 +75,6 @@ def _result():
tags_low = { tag: tag_result['max'] - tag_result['scored'] for tag, tag_result in entry.result['tags'].items() } 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) 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] 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"""<p>Based on your answers, we would also suggest you brush up on the following topics for your next attempt:</p>
<ul>
<li>{'</li><li>'.join(tag_output)}</li>
</ul>
"""
if not entry.status == 'late': if not entry.status == 'late':
pass entry.notify_result()
return render_template('/quiz/result.html', entry=entry, score=score, tag_output=tag_output) return render_template('/quiz/result.html', entry=entry, score=score, tag_output=tag_output)