Added email notifications
This commit is contained in:
		@@ -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:
 | 
			
		||||
 
 | 
			
		||||
@@ -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.'
 | 
			
		||||
        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)
 | 
			
		||||
@@ -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"""
 | 
			
		||||
                <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.'
 | 
			
		||||
 | 
			
		||||
    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"""
 | 
			
		||||
            <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('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"""
 | 
			
		||||
                <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
 | 
			
		||||
 | 
			
		||||
    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 '<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.'
 | 
			
		||||
 
 | 
			
		||||
@@ -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"""<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':
 | 
			
		||||
        pass
 | 
			
		||||
        entry.notify_result()
 | 
			
		||||
    return render_template('/quiz/result.html', entry=entry, score=score, tag_output=tag_output)
 | 
			
		||||
		Reference in New Issue
	
	Block a user