202 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			202 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from ..extensions 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 smtplib import SMTPException
 | |
| from sqlalchemy.exc import SQLAlchemyError
 | |
| 
 | |
| from datetime import datetime, timedelta
 | |
| from uuid import uuid4
 | |
| 
 | |
| class Entry(db.Model):
 | |
| 
 | |
|     id = db.Column(db.String(36), primary_key=True)
 | |
|     first_name = db.Column(db.String(128), nullable=False)
 | |
|     surname = db.Column(db.String(128), nullable=False)
 | |
|     email = db.Column(db.String(128), nullable=False)
 | |
|     club = db.Column(db.String(128), nullable=True)
 | |
|     test_id = db.Column(db.String(36), db.ForeignKey('test.id'))
 | |
|     user_code = db.Column(db.String(6), nullable=True)
 | |
|     start_time = db.Column(db.DateTime, nullable=True)
 | |
|     end_time = db.Column(db.DateTime, nullable=True)
 | |
|     status = db.Column(db.String(16), nullable=True)
 | |
|     valid = db.Column(db.Boolean, default=True, nullable=True)
 | |
|     answers = db.Column(JsonEncodedDict, nullable=True)
 | |
|     result = db.Column(JsonEncodedDict, nullable=True)
 | |
|     
 | |
|     def __repr__(self):
 | |
|         return f'<New entry by {self.first_name} {self.surname}> was added with <id {self.id}>.'
 | |
| 
 | |
|     @property
 | |
|     def generate_id(self): raise AttributeError('generate_id is not a readable attribute.')
 | |
| 
 | |
|     generate_id.setter
 | |
|     def generate_id(self): self.id = uuid4().hex
 | |
| 
 | |
|     @property
 | |
|     def set_first_name(self): raise AttributeError('set_first_name is not a readable attribute.')
 | |
| 
 | |
|     set_first_name.setter
 | |
|     def set_first_name(self, name:str): self.first_name = encrypt(name)
 | |
| 
 | |
|     def get_first_name(self): return decrypt(self.first_name)
 | |
| 
 | |
|     @property
 | |
|     def set_surname(self): raise AttributeError('set_first_name is not a readable attribute.')
 | |
| 
 | |
|     set_surname.setter
 | |
|     def set_surname(self, name:str): self.surname = encrypt(name)
 | |
| 
 | |
|     def get_surname(self): return decrypt(self.surname)
 | |
| 
 | |
|     @property
 | |
|     def set_email(self): raise AttributeError('set_email is not a readable attribute.')
 | |
| 
 | |
|     set_email.setter
 | |
|     def set_email(self, email:str): self.email = encrypt(email)
 | |
| 
 | |
|     def get_email(self): return decrypt(self.email)
 | |
| 
 | |
|     @property
 | |
|     def set_club(self): raise AttributeError('set_club is not a readable attribute.')
 | |
| 
 | |
|     set_club.setter
 | |
|     def set_club(self, club:str): self.club = encrypt(club)
 | |
| 
 | |
|     def get_club(self): return decrypt(self.club)
 | |
| 
 | |
|     def ready(self):
 | |
|         self.generate_id()
 | |
|         try:
 | |
|             db.session.add(self)
 | |
|             db.session.commit()
 | |
|         except (SQLAlchemyError, ConnectionError) as exception:
 | |
|             db.session.rollback()
 | |
|             write('system.log', f'Database error when preparing new entry for {self.get_surname()}, {self.get_first_name()}: {exception}')
 | |
|             return False, f'Database error: {exception}'
 | |
|         write('tests.log', f'New test ready for {self.get_surname()}, {self.get_first_name()} with id {self.id}.')
 | |
|         return True, f'Test ready.'
 | |
| 
 | |
|     def start(self):
 | |
|         self.start_time = datetime.now()
 | |
|         self.status = 'started'
 | |
|         try: db.session.commit()
 | |
|         except (SQLAlchemyError, ConnectionError) as exception:
 | |
|             db.session.rollback()
 | |
|             write('system.log', f'Database error when starting test for {self.get_surname()}, {self.get_first_name()}: {exception}')
 | |
|             return False, f'Database error: {exception}'
 | |
|         write('tests.log', f'Test started by {self.get_surname()}, {self.get_first_name()} with id {self.id}.')
 | |
|         return True, f'New test started with id {self.id}.'
 | |
| 
 | |
|     def complete(self, answers:dict=None, result:dict=None):
 | |
|         self.end_time = datetime.now()
 | |
|         self.answers = answers
 | |
|         self.result = result
 | |
|         delta = timedelta(minutes=int(0 if self.test.time_limit is None else self.test.time_limit)+1)
 | |
|         if not self.test.time_limit or self.end_time <= self.start_time + delta:
 | |
|             self.status = 'completed'
 | |
|             self.valid = True
 | |
|         else:
 | |
|             self.status = 'late'
 | |
|             self.valid = False
 | |
|         try: db.session.commit()
 | |
|         except (SQLAlchemyError, ConnectionError) as exception:
 | |
|             db.session.rollback()
 | |
|             write('system.log', f'Database error when submitting entry for {self.get_surname()}, {self.get_first_name()}: {exception}')
 | |
|             return False, f'Database error: {exception}'
 | |
|         write('tests.log', f'Test completed by {self.get_surname()}, {self.get_first_name()} with id {self.id}.')
 | |
|         return True, f'Test entry completed for id {self.id}.'
 | |
| 
 | |
|     def validate(self):
 | |
|         if self.valid: return False, f'The entry is already valid.'
 | |
|         if self.status == 'started': return False, 'The entry is still pending.'
 | |
|         self.valid = True
 | |
|         self.status = 'completed'
 | |
|         try: db.session.commit()
 | |
|         except (SQLAlchemyError, ConnectionError) as exception:
 | |
|             db.session.rollback()
 | |
|             write('system.log', f'Database error when validating entry {self.id}: {exception}')
 | |
|             return False, f'Database error: {exception}'
 | |
|         write('system.log', f'The entry {self.id} has been validated by {current_user.get_username()}.')
 | |
|         return True, f'The entry {self.id} has been validated.'
 | |
| 
 | |
|     def delete(self):
 | |
|         id = self.id
 | |
|         name = f'{self.get_first_name()} {self.get_surname()}'
 | |
|         try:
 | |
|             db.session.delete(self)
 | |
|             db.session.commit()
 | |
|         except (SQLAlchemyError, ConnectionError) as exception:
 | |
|             db.session.rollback()
 | |
|             write('system.log', f'Database error when deleting entry {id}: {exception}')
 | |
|             return False, f'Database error: {exception}'
 | |
|         write('system.log', f'The entry {id} by {name} has been deleted by {current_user.get_username()}.')
 | |
|         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>
 | |
|             """
 | |
|         )
 | |
|         try: mail.send(email)
 | |
|         except (SMTPException, ConnectionError) as exception: write('system.log', f'SMTP Error when trying to notify results to {self.get_surname()}, {self.get_first_name()} with error: {exception}') |