def test_get_subject(self): """ get_subject properly uses the report instance and count instance variables to return a subject string. """ reporter: Reporter = Reporter(self.potions_report) reporter.exams_time_metadata = self.exams_time_metadata reporter.prepare_context() with patch('pe.reporter.datetime', autospec=True) as mock_datetime: mock_datetime.now.return_value = self.fake_finished_at subject: str = reporter.get_subject() self.assertEqual(subject, self.expected_subject)
def main(api_util: ApiUtil) -> None: """ Runs the highest-level application process, coordinating the use of ScoresOrchestration and Reporter classes and the transfer of data between them. :param api_util: Instance of ApiUtil for making API calls :type api_util: ApiUtil :return: None :rtype: None """ start_time: datetime = datetime.now(tz=utc) LOGGER.info(f'Starting new run at {start_time}') reports: list[Report] = list(Report.objects.all()) LOGGER.debug(reports) exams: list[Exam] = list(Exam.objects.all()) LOGGER.debug(exams) for report in reports: reporter: Reporter = Reporter(report) for exam in report.exams.all(): LOGGER.info(f'Processing Exam: {exam.name}') exam_start_time = datetime.now(tz=utc) exam_orca: ScoresOrchestration = ScoresOrchestration( api_util, exam) exam_orca.main() exam_end_time = datetime.now(tz=utc) metadata: dict[str, datetime] = { 'start_time': exam_start_time, 'end_time': exam_end_time, 'sub_time_filter': exam_orca.sub_time_filter } reporter.exams_time_metadata[exam.id] = metadata reporter.prepare_context() if reporter.total_successes > 0 or reporter.total_failures > 0: LOGGER.info( f'Sending {report.name} report email to {report.contact}') reporter.send_email() else: LOGGER.info( f'No email will be sent for the {report.name} report as there was no transmission activity.' ) end_time: datetime = datetime.now(tz=utc) delta: timedelta = end_time - start_time LOGGER.info(f'The run ended at {end_time}') LOGGER.info(f'Duration of run: {delta}')
def test_send_email(self): """ send_email properly renders plain text and HTML strings (localizing times) using the context and sends an email. """ # Set up snapshots with open(os.path.join(SNAPSHOTS_DIR, 'email_snap.txt'), 'r') as email_snap_plain_file: email_snap_plain: str = email_snap_plain_file.read() with open(os.path.join(SNAPSHOTS_DIR, 'email_snap.html'), 'r') as email_snap_html_file: email_snap_html: str = email_snap_html_file.read() reporter: Reporter = Reporter(self.potions_report) reporter.exams_time_metadata = self.exams_time_metadata # Patch os.environ to override environment variables with patch.dict(os.environ, {'SUPPORT_EMAIL': '*****@*****.**', 'SMTP_FROM': '*****@*****.**'}): reporter.prepare_context() with patch('pe.reporter.datetime', autospec=True) as mock_datetime: mock_datetime.now.return_value = self.fake_finished_at reporter.send_email() self.assertEqual(len(mail.outbox), 1) email: EmailMultiAlternatives = mail.outbox[0] self.assertEqual(len(email.alternatives), 1) self.assertEqual(email.alternatives[0][1], 'text/html') email_html_msg: str = email.alternatives[0][0] # Check subject, to, and from self.assertEqual(email.subject, self.expected_subject) self.assertEqual(email.to, ['*****@*****.**']) self.assertEqual(email.from_email, '*****@*****.**') # Check that body matches plain text snapshot self.assertEqual(email.body, email_snap_plain) # Check that HTML alternative matches HTML snapshot self.assertEqual(email_html_msg, email_snap_html)
def test_prepare_context(self): """ prepare_context properly uses accumulated time metadata and database records to create a context. """ reporter: Reporter = Reporter(self.potions_report) # Check that Reporter is initialized with no data self.assertEqual( (reporter.total_successes, reporter.total_failures, reporter.total_new, reporter.context), (0, 0, 0, dict()) ) # I decided to keep Reporter initialization in tests (not setUp), so time metadata is assigned all at once here. # This is different from main() in entry.py, where key-value pairs are accumulated. reporter.exams_time_metadata = self.exams_time_metadata reporter.prepare_context() self.assertEqual((reporter.total_successes, reporter.total_failures, reporter.total_new), (4, 1, 2)) self.assertEqual(sorted(list(reporter.context.keys())), ['exams', 'report', 'support_email']) self.assertEqual(reporter.context['report'], { 'id': 1, 'name': 'Potions', 'contact': '*****@*****.**', 'summary': {'success_count': 4, 'failure_count': 1, 'new_count': 2} }) self.assertEqual(len(reporter.context['exams']), 2) first_exam_dict: Dict[str, Any] = reporter.context['exams'][0] second_exam_dict: Dict[str, Any] = reporter.context['exams'][1] keys_list: List[List[str]] = [ [ 'assignment_id', 'course_id', 'default_time_filter', 'failures', 'id', 'name', 'report', 'sa_code', 'successes', 'summary', 'time' ], sorted(list(first_exam_dict.keys())), sorted(list(second_exam_dict.keys())) ] self.assertEqual(keys_list.count(keys_list[0]), 3) # Check whether time metadata correctly incorporated self.assertEqual(first_exam_dict['time'], self.exams_time_metadata[1]) self.assertEqual(second_exam_dict['time'], self.exams_time_metadata[2]) # Check whether QuerySet-derived lists have correct lengths self.assertEqual(len(first_exam_dict['successes']), 0) self.assertEqual(len(first_exam_dict['failures']), 1) self.assertEqual(len(second_exam_dict['successes']), 4) self.assertEqual(len(second_exam_dict['failures']), 0) # Check roughly that failures and successes are present self.assertEqual( first_exam_dict['failures'][0], { 'submission_id': 123458, 'student_uniqname': 'rweasley', 'score': 150.0, 'graded_timestamp': datetime(2020, 6, 12, 16, 0, 0, tzinfo=utc) } ) val_success_ids: List[int] = sorted( [success_dict['submission_id'] for success_dict in second_exam_dict['successes']] ) self.assertEqual(val_success_ids, [123460, 210000, 444444, 444445])