def _send_invitation_email(self, section_id): SentEmail.create( section_id=section_id, recipient_uid=get_instructor_uids(section_id=section_id, term_id=self.term_id)[0], template_type='invitation', term_id=self.term_id, )
def test_approval_by_instructors(self, app, client, fake_auth): """Instructor can submit approval if s/he is teaching the requested course.""" with test_approvals_workflow(app): instructor_uids = get_instructor_uids(section_id=section_1_id, term_id=self.term_id) fake_auth.login(instructor_uids[0]) api_approve( client, publish_type='kaltura_my_media', recording_type='presentation_audio', section_id=section_1_id, ) std_commit(allow_test_environment=True) fake_auth.login(instructor_uids[1]) api_approve( client, publish_type='kaltura_media_gallery', recording_type='presentation_audio', section_id=section_1_id, ) std_commit(allow_test_environment=True) QueuedEmailsJob(app.app_context).run() # First instructor was notified 1) that second instructor needed to approve; 2) that second instructor made changes. emails_sent = SentEmail.get_emails_sent_to(instructor_uids[0]) assert len(emails_sent) == 2 for email in emails_sent: assert email.section_id == section_1_id assert email.term_id == self.term_id assert emails_sent[0].template_type == 'waiting_for_approval' assert emails_sent[1].template_type == 'notify_instructor_of_changes' # Second instructor received no notifications. assert len(SentEmail.get_emails_sent_to(instructor_uids[1])) == 0 fake_auth.login(admin_uid) api_json = api_get_course( client, term_id=self.term_id, section_id=section_1_id, ) assert api_json['meetings']['eligible'][0]['room']['location'] == 'Barrows 106' instructor_uids = [i['uid'] for i in api_json['instructors']] assert instructor_uids == instructor_uids approvals_ = api_json['approvals'] assert len(approvals_) == 2 assert approvals_[0]['approvedBy']['uid'] == instructor_uids[0] assert approvals_[0]['publishType'] == 'kaltura_my_media' assert approvals_[1]['approvedBy']['uid'] == instructor_uids[1] assert approvals_[1]['publishType'] == 'kaltura_media_gallery' assert approvals_[1]['recordingType'] == 'presentation_audio' assert approvals_[1]['recordingTypeName'] == 'Presentation and Audio' assert api_json['hasNecessaryApprovals'] is True assert api_json['scheduled'] is None
def _create_emails_sent(): term_id = app.config['CURRENT_TERM_ID'] SentEmail.create( recipient_uids=['8765432'], section_id='28165', template_type='invitation', term_id=term_id, ) std_commit(allow_test_environment=True)
def _assert_alert_count(count): emails_sent = SentEmail.get_emails_sent_to( uid=admin_uid) assert len(emails_sent) == count assert emails_sent[0].section_id == section_id assert emails_sent[ 0].template_type == 'admin_alert_date_change'
def _assert_alert_count(count): emails_sent = SentEmail.get_emails_of_type( section_ids=[section_id], template_type='room_change_no_longer_eligible', term_id=term_id, ) assert len(emails_sent) == count
def test_not_invited_filter(self, client, admin_session): """Not-invited filter: Courses in eligible rooms, never sent an invitation. No approval. Not scheduled.""" with test_approvals_workflow(app): # The first course gets an invitation self._send_invitation_email(section_1_id) # The second course did not receive an invitation BUT it does have approval. invite = SentEmail.get_emails_of_type( section_ids=[section_4_id], template_type='invitation', term_id=self.term_id, ) assert not invite self._create_approval(section_4_id) std_commit(allow_test_environment=True) api_json = self._api_courses(client, term_id=self.term_id, filter_='Not Invited') assert not _find_course(api_json=api_json, section_id=section_1_id) assert not _find_course(api_json=api_json, section_id=section_4_id) # Zero instructors is acceptable assert _find_course(api_json=api_json, section_id=eligible_course_with_no_instructors) # Third course is in enabled room and has not received an invite. Therefore, it is in the feed. assert _is_course_in_enabled_room(section_id=section_3_id, term_id=self.term_id) course = _find_course(api_json=api_json, section_id=section_3_id) assert course['approvalStatus'] == 'Not Invited' assert course['schedulingStatus'] == 'Not Scheduled' assert course['label'] == 'BIO 1B, LEC 001'
def test_admin_alert_multiple_meeting_patterns(self): """Emails admin if course is scheduled with weird start/end dates.""" with test_approvals_workflow(app): with enabled_job(job_key=AdminEmailsJob.key()): term_id = app.config['CURRENT_TERM_ID'] section_id = 50014 room_id = Room.find_room('Barker 101').id # The course has two instructors. instructor_uid = get_instructor_uids(section_id=section_id, term_id=term_id)[0] approval = Approval.create( approved_by_uid=instructor_uid, approver_type_='instructor', publish_type_='kaltura_my_media', recording_type_='presenter_audio', room_id=room_id, section_id=section_id, term_id=term_id, ) # Uh oh! Only one of them has been scheduled. meeting = get_eligible_meeting(section_id=section_id, term_id=term_id) Scheduled.create( instructor_uids=[instructor_uid], kaltura_schedule_id=random.randint(1, 10), meeting_days=meeting['days'], meeting_end_date=get_recording_end_date(meeting), meeting_end_time=meeting['endTime'], meeting_start_date=get_recording_start_date( meeting, return_today_if_past_start=True), meeting_start_time=meeting['startTime'], publish_type_=approval.publish_type, recording_type_=approval.recording_type, room_id=room_id, section_id=section_id, term_id=term_id, ) courses = SisSection.get_courses_scheduled_nonstandard_dates( term_id=term_id) course = next( (c for c in courses if c['sectionId'] == section_id), None) assert course # Message queued but not sent. admin_uid = app.config['EMAIL_DIABLO_ADMIN_UID'] AdminEmailsJob(simply_yield).run() queued_messages = QueuedEmail.query.filter_by( section_id=section_id).all() assert len(queued_messages) == 1 for queued_message in queued_messages: assert '2020-08-26 to 2020-10-02' in queued_message.message # Message sent. QueuedEmailsJob(simply_yield).run() emails_sent = SentEmail.get_emails_sent_to(uid=admin_uid) assert len(emails_sent) == 1 assert emails_sent[ 0].template_type == 'admin_alert_multiple_meeting_patterns' assert emails_sent[0].section_id == section_id
def test_do_not_email_filter(self, client, db, admin_session): """Do Not Email filter: Courses in eligible room; "opt out" is true; all stages of approval; not scheduled.""" with test_approvals_workflow(app): # Send invites them opt_out. for section_id in (section_1_id, section_in_ineligible_room, section_3_id, section_4_id): CoursePreference.update_opt_out(section_id=section_id, term_id=self.term_id, opt_out=True) in_enabled_room = _is_course_in_enabled_room(section_id=section_id, term_id=self.term_id) if section_id == section_in_ineligible_room: # Courses in ineligible rooms will be excluded from the feed. assert not in_enabled_room else: assert in_enabled_room SentEmail.create( section_id=section_id, recipient_uids=_get_instructor_uids(section_id=section_id, term_id=self.term_id), template_type='invitation', term_id=self.term_id, ) # If course has approvals but not scheduled then it will show up in the feed. Approval.create( approved_by_uid=_get_instructor_uids(section_id=section_1_id, term_id=self.term_id)[0], approver_type_='instructor', cross_listed_section_ids=[], publish_type_='canvas', recording_type_='presentation_audio', room_id=Room.get_room_id(section_id=section_1_id, term_id=self.term_id), section_id=section_1_id, term_id=self.term_id, ) # Feed will exclude scheduled. _schedule_recordings( section_id=section_3_id, term_id=self.term_id, ) std_commit(allow_test_environment=True) api_json = self._api_courses(client, term_id=self.term_id, filter_='Do Not Email') for section_id in (section_1_id, section_4_id): # The 'Do Not Email' course is in the feed assert _find_course(api_json=api_json, section_id=section_id) for section_id in (section_3_id, section_in_ineligible_room): # Excluded courses assert not _find_course(api_json=api_json, section_id=section_id)
def _assert_email_count(expected_count, section_id, template_type): term_id = app.config['CURRENT_TERM_ID'] emails_sent = SentEmail.get_emails_of_type( section_ids=[section_id], template_type=template_type, term_id=term_id, ) assert len(emails_sent) == expected_count
def test_partially_approved_filter(self, client, db, admin_session): """Partially approved: Eligible, invited course with 1+ approvals, but not ALL instructors have approved.""" with test_approvals_workflow(app): # Assert multiple instructors section_1_instructor_uids = _get_instructor_uids(section_id=section_1_id, term_id=self.term_id) section_6_instructor_uids = _get_instructor_uids(section_id=section_6_id, term_id=self.term_id) assert len(section_1_instructor_uids) > 1 assert len(section_6_instructor_uids) > 1 # Send invites courses = [ {'section_id': section_1_id, 'instructor_uids': section_1_instructor_uids}, {'section_id': section_6_id, 'instructor_uids': section_6_instructor_uids}, ] for course in courses: SentEmail.create( section_id=course['section_id'], recipient_uids=course['instructor_uids'], template_type='invitation', term_id=self.term_id, ) Approval.create( approved_by_uid=course['instructor_uids'][0], approver_type_='instructor', cross_listed_section_ids=[], publish_type_='canvas', recording_type_='presentation_audio', room_id=Room.get_room_id(section_id=course['section_id'], term_id=self.term_id), section_id=course['section_id'], term_id=self.term_id, ) # Feed will include both scheduled and not scheduled. _schedule_recordings( section_id=section_1_id, term_id=self.term_id, ) std_commit(allow_test_environment=True) api_json = self._api_courses(client, term_id=self.term_id, filter_='Partially Approved') assert len(api_json) == 2 assert _find_course(api_json=api_json, section_id=section_1_id) course = _find_course(api_json=api_json, section_id=section_6_id) assert course assert course['label'] == 'LAW 23, LEC 002'
def test_approval_by_instructors(self, client, db, fake_auth): """Instructor can submit approval if s/he is teaching the requested course.""" instructor_uids = _get_instructor_uids(section_id=section_1_id, term_id=self.term_id) fake_auth.login(instructor_uids[0]) api_approve( client, publish_type='canvas', recording_type='presentation_audio', section_id=section_1_id, ) std_commit(allow_test_environment=True) fake_auth.login(instructor_uids[1]) api_approve( client, publish_type='kaltura_media_gallery', recording_type='presentation_audio', section_id=section_1_id, ) std_commit(allow_test_environment=True) for uid in ('234567', '8765432'): emails_sent = SentEmail.get_emails_sent_to(uid) assert len(emails_sent) > 0 most_recent = emails_sent[-1] assert most_recent.section_id == section_1_id assert most_recent.template_type == 'notify_instructor_of_changes' assert most_recent.term_id == self.term_id fake_auth.login(admin_uid) api_json = api_get_course( client, term_id=self.term_id, section_id=section_1_id, ) assert api_json['room']['location'] == 'Barrows 106' instructor_uids = [i['uid'] for i in api_json['instructors']] assert instructor_uids == instructor_uids approvals_ = api_json['approvals'] assert len(approvals_) == 2 assert approvals_[0]['approvedBy']['uid'] == instructor_uids[0] assert approvals_[0]['publishType'] == 'canvas' assert approvals_[1]['approvedBy']['uid'] == instructor_uids[1] assert approvals_[1]['publishType'] == 'kaltura_media_gallery' assert approvals_[1]['recordingType'] == 'presentation_audio' assert approvals_[1]['recordingTypeName'] == 'Presentation and Audio' assert api_json['hasNecessaryApprovals'] is True assert api_json['scheduled'] is None
def test_scheduled_filter(self, client, db, admin_session): """Scheduled filter: Courses with recordings scheduled.""" with test_approvals_workflow(app): section_1_instructor_uids = _get_instructor_uids(section_id=section_1_id, term_id=self.term_id) section_6_instructor_uids = _get_instructor_uids(section_id=section_6_id, term_id=self.term_id) # Send invites courses = [ {'section_id': section_1_id, 'instructor_uids': section_1_instructor_uids}, {'section_id': section_6_id, 'instructor_uids': section_6_instructor_uids}, ] for course in courses: SentEmail.create( section_id=course['section_id'], recipient_uids=course['instructor_uids'], template_type='invitation', term_id=self.term_id, ) Approval.create( approved_by_uid=course['instructor_uids'][0], approver_type_='instructor', cross_listed_section_ids=[], publish_type_='canvas', recording_type_='presentation_audio', room_id=Room.get_room_id(section_id=course['section_id'], term_id=self.term_id), section_id=course['section_id'], term_id=self.term_id, ) # Feed will only include courses that were scheduled. _schedule_recordings( section_id=section_1_id, term_id=self.term_id, ) std_commit(allow_test_environment=True) api_json = self._api_courses(client, term_id=self.term_id, filter_='Scheduled') assert len(api_json) == 1 assert _find_course(api_json=api_json, section_id=section_1_id) assert not _find_course(api_json=api_json, section_id=section_6_id)
def test_not_invited_filter(self, client, db, admin_session): """Not-invited filter: Courses in eligible rooms, never sent an invitation. No approval. Not scheduled.""" with test_approvals_workflow(app): # The first course gets an invitation section_1_instructor_uids = _get_instructor_uids(section_id=section_1_id, term_id=self.term_id) SentEmail.create( section_id=section_1_id, recipient_uids=section_1_instructor_uids, template_type='invitation', term_id=self.term_id, ) # The second course did not receive an invitation BUT it does have approval. invite = SentEmail.get_emails_of_type( section_id=section_4_id, template_type='invitation', term_id=self.term_id, ) assert not invite Approval.create( approved_by_uid=_get_instructor_uids(section_id=section_4_id, term_id=self.term_id)[0], approver_type_='instructor', cross_listed_section_ids=[], publish_type_='canvas', recording_type_='presentation_audio', room_id=Room.get_room_id(section_id=section_4_id, term_id=self.term_id), section_id=section_4_id, term_id=self.term_id, ) std_commit(allow_test_environment=True) api_json = self._api_courses(client, term_id=self.term_id, filter_='Not Invited') assert not _find_course(api_json=api_json, section_id=section_1_id) assert not _find_course(api_json=api_json, section_id=section_4_id) # Third course is in enabled room and has not received an invite. Therefore, it is in the feed. assert _is_course_in_enabled_room(section_id=section_3_id, term_id=self.term_id) course = _find_course(api_json=api_json, section_id=section_3_id) assert course assert course['label'] == 'BIO 1B, LEC 001'
def test_invited_filter(self, client, db, admin_session): """Invited filter: Course in an eligible room, have received invitation. No approvals. Not scheduled.""" with test_approvals_workflow(app): # First, send invitations SentEmail.create( section_id=section_4_id, recipient_uids=_get_instructor_uids(section_id=section_4_id, term_id=self.term_id), template_type='invitation', term_id=self.term_id, ) section_5_instructor_uids = _get_instructor_uids(section_id=section_5_id, term_id=self.term_id) SentEmail.create( section_id=section_5_id, recipient_uids=section_5_instructor_uids, template_type='invitation', term_id=self.term_id, ) # The section with approval will NOT show up in search results Approval.create( approved_by_uid=section_5_instructor_uids[0], approver_type_='instructor', cross_listed_section_ids=[], publish_type_='canvas', recording_type_='presentation_audio', room_id=SisSection.get_course(term_id=self.term_id, section_id=section_5_id)['room']['id'], section_id=section_5_id, term_id=self.term_id, ) std_commit(allow_test_environment=True) api_json = self._api_courses(client, term_id=self.term_id, filter_='Invited') # Section with ZERO approvals will show up in search results course = _find_course(api_json=api_json, section_id=section_4_id) assert course assert course['label'] == 'CHEM C110L, LAB 001' # The section with approval will NOT show up in search results assert not _find_course(api_json=api_json, section_id=section_5_id)
def test_alert_admin_of_room_change(self, db_session): """Emails admin when a scheduled course gets a room change.""" with test_approvals_workflow(app): with enabled_job(job_key=AdminEmailsJob.key()): term_id = app.config['CURRENT_TERM_ID'] section_id = 50004 approved_by_uid = '10004' the_old_room = 'Wheeler 150' scheduled_in_room = Room.find_room(the_old_room) approval = Approval.create( approved_by_uid=approved_by_uid, approver_type_='instructor', publish_type_='kaltura_media_gallery', recording_type_='presenter_audio', room_id=scheduled_in_room.id, section_id=section_id, term_id=term_id, ) meeting = get_eligible_meeting(section_id=section_id, term_id=term_id) Scheduled.create( instructor_uids=get_instructor_uids(term_id=term_id, section_id=section_id), kaltura_schedule_id=random.randint(1, 10), meeting_days=meeting['days'], meeting_end_date=get_recording_end_date(meeting), meeting_end_time=meeting['endTime'], meeting_start_date=get_recording_start_date( meeting, return_today_if_past_start=True), meeting_start_time=meeting['startTime'], publish_type_=approval.publish_type, recording_type_=approval.recording_type, room_id=scheduled_in_room.id, section_id=section_id, term_id=term_id, ) admin_uid = app.config['EMAIL_DIABLO_ADMIN_UID'] # Message queued, then sent. AdminEmailsJob(simply_yield).run() QueuedEmailsJob(simply_yield).run() emails_sent = SentEmail.get_emails_sent_to(uid=admin_uid) assert len(emails_sent) == 1 assert emails_sent[0].section_id == section_id assert emails_sent[ 0].template_type == 'admin_alert_room_change'
def get_emails_sent_to_uid(uid): return tolerant_jsonify( [e.to_api_json() for e in SentEmail.get_emails_sent_to(uid)])
def _get_email_count(uid): return len(SentEmail.get_emails_sent_to(uid=uid))
def send( self, message, recipients, subject_line, term_id=None, section_id=None, template_type=None, ): @skip_when_pytest() def _send(): # Connect to SMTP server smtp = SMTP(self.bcop_smtp_server, port=self.bcop_smtp_port) # TLS encryption smtp.starttls() smtp.set_debuglevel(app.logger.level == logging.DEBUG) smtp.login(self.bcop_smtp_username, self.bcop_smtp_password) emails_sent_to = set() for recipient in recipients: mock_message = _get_mock_message( recipient['name'], recipient['email'], subject_line, message, ) if app.config['DIABLO_ENV'] == 'test': app.logger.info(mock_message) else: from_address = app.config['EMAIL_DIABLO_SUPPORT'] to_address = self.get_email_address( user=recipient, subject_line=subject_line) msg = MIMEMultipart('alternative') msg['From'] = from_address msg['To'] = to_address msg['Bcc'] = app.config['EMAIL_DIABLO_ADMIN'] if app.config['EMAIL_TEST_MODE']: # Append intended recipient email address to verify when testing. intended_email = recipient['email'] msg['Subject'] = f'{subject_line} (To: {intended_email})' else: msg['Subject'] = subject_line # TODO: 'plain' text version of email? msg.attach(MIMEText(message, 'plain')) msg.attach(MIMEText(message, 'html')) # Send smtp.sendmail(from_addr=from_address, to_addrs=to_address, msg=msg.as_string()) emails_sent_to.add(to_address) app.logger.info( f'{len(recipients)} \'{template_type}\' emails sent to {", ".join(list(emails_sent_to))}' ) # Disconnect smtp.quit() # Send emails _send() SentEmail.create( recipient_uids=[recipient['uid'] for recipient in recipients], section_id=section_id, template_type=template_type, term_id=term_id or app.config['CURRENT_TERM_ID'], )
def _get_emails_sent(email_template_type, section_id, term_id): return SentEmail.get_emails_of_type( section_ids=[section_id], template_type=email_template_type, term_id=term_id, )
def _to_api_json(term_id, rows, include_rooms=True): courses_per_id = {} instructors_per_section_id = {} section_ids_opted_out = CoursePreference.get_section_ids_opted_out( term_id=term_id) # If course has multiple instructors then the section_id will be represented across multiple rows. for row in rows: approvals = [] section_id = int(row['section_id']) if section_id not in courses_per_id: # Construct new course instructors_per_section_id[section_id] = [] has_opted_out = section_id in section_ids_opted_out cross_listings = _get_cross_listed_courses(section_id=section_id, term_id=term_id) approvals, scheduled = _get_approvals_and_scheduled( section_ids=[section_id] + [c['sectionId'] for c in cross_listings], term_id=term_id, ) course_name = row['course_name'] instruction_format = row['instruction_format'] section_num = row['section_num'] course = { 'allowedUnits': row['allowed_units'], 'canvasCourseSites': _canvas_course_sites(term_id, section_id), 'courseName': course_name, 'courseTitle': row['course_title'], 'crossListings': cross_listings, 'hasOptedOut': has_opted_out, 'instructionFormat': instruction_format, 'instructors': [], 'isPrimary': row['is_primary'], 'label': f'{course_name}, {instruction_format} {section_num}', 'meetingDays': format_days(row['meeting_days']), 'meetingEndDate': row['meeting_end_date'], 'meetingEndTime': format_time(row['meeting_end_time']), 'meetingLocation': row['meeting_location'], 'meetingStartDate': row['meeting_start_date'], 'meetingStartTime': format_time(row['meeting_start_time']), 'sectionId': section_id, 'sectionNum': section_num, 'termId': row['term_id'], 'approvals': approvals, 'scheduled': scheduled, } invites = SentEmail.get_emails_of_type( section_id=section_id, template_type='invitation', term_id=term_id, ) course['invitees'] = [] for invite in invites: course['invitees'].extend(invite.recipient_uids) if scheduled: course['status'] = 'Scheduled' elif approvals: course['status'] = 'Partially Approved' else: course['status'] = 'Invited' if invites else 'Not Invited' if include_rooms: room = Room.get_room( row['room_id']).to_api_json() if 'room_id' in row else None course['room'] = room courses_per_id[section_id] = course # Build upon course object with one instructor per row. instructor_uid = row['instructor_uid'] if instructor_uid not in [ i['uid'] for i in instructors_per_section_id[section_id] ]: instructors_per_section_id[section_id].append({ 'approval': next((a for a in approvals if a['approvedBy']['uid'] == instructor_uid), False), 'deptCode': row['instructor_dept_code'], 'email': row['instructor_email'], 'name': row['instructor_name'], 'roleCode': row['instructor_role_code'], 'uid': instructor_uid, 'wasSentInvite': instructor_uid in courses_per_id[section_id]['invitees'], }) api_json = [] for section_id, course in courses_per_id.items(): room_id = course.get('room', {}).get('id') def _add_and_verify_room(approval_or_scheduled): action_room_id = approval_or_scheduled.get('room', {}).get('id') is_obsolete_room = not room_id or room_id != action_room_id approval_or_scheduled['hasObsoleteRoom'] = is_obsolete_room course['instructors'] = instructors_per_section_id[section_id] course['hasNecessaryApprovals'] = _has_necessary_approvals(course) scheduled = course['scheduled'] # Check for course changes w.r.t. room, meeting times, and instructors. if scheduled: def _meeting(obj): return f'{obj["meetingDays"]}-{obj["meetingStartTime"]}-{obj["meetingEndTime"]}' instructor_uids = set( [instructor['uid'] for instructor in course['instructors']]) scheduled['hasObsoleteInstructors'] = instructor_uids != set( scheduled['instructorUids']) scheduled['hasObsoleteMeetingTimes'] = _meeting( course) != _meeting(scheduled) _add_and_verify_room(scheduled) for approval in course['approvals']: _add_and_verify_room(approval) # Add course to the feed api_json.append(course) return api_json
def _system_error_email_count(): return len(SentEmail.get_emails_sent_to('0'))
def _to_api_json(term_id, rows, include_rooms=True): rows = rows.fetchall() section_ids = list(set(int(row['section_id']) for row in rows)) courses_per_id = {} # Perform bulk queries and build data structures for feed generation. section_ids_opted_out = CoursePreference.get_section_ids_opted_out( term_id=term_id) invited_uids_by_section_id = {section_id: [] for section_id in section_ids} for invite in SentEmail.get_emails_of_type(section_ids=section_ids, template_type='invitation', term_id=term_id): if invite.recipient_uid not in invited_uids_by_section_id[ invite.section_id]: invited_uids_by_section_id[invite.section_id].append( invite.recipient_uid) approval_results = Approval.get_approvals_per_section_ids( section_ids=section_ids, term_id=term_id) scheduled_results = Scheduled.get_scheduled_per_section_ids( section_ids=section_ids, term_id=term_id) room_ids = set(row['room_id'] for row in rows) room_ids.update(a.room_id for a in approval_results) room_ids.update(s.room_id for s in scheduled_results) rooms = Room.get_rooms(list(room_ids)) rooms_by_id = {room.id: room for room in rooms} approvals_by_section_id = {section_id: [] for section_id in section_ids} for approval in approval_results: approvals_by_section_id[approval.section_id].append( approval.to_api_json(rooms_by_id=rooms_by_id)) scheduled_by_section_id = { s.section_id: s.to_api_json(rooms_by_id=rooms_by_id) for s in scheduled_results } cross_listings_per_section_id, instructors_per_section_id, canvas_sites_by_section_id = _get_cross_listed_courses( section_ids=section_ids, term_id=term_id, approvals=approvals_by_section_id, invited_uids=invited_uids_by_section_id, ) # Construct course objects. # If course has multiple instructors or multiple rooms then the section_id will be represented across multiple rows. # Multiple rooms are rare, but a course is sometimes associated with both an eligible and an ineligible room. We # order rooms in SQL by capability, NULLS LAST, and use scheduling data from the first row available. for row in rows: section_id = int(row['section_id']) if section_id in courses_per_id: course = courses_per_id[section_id] else: # Approvals and scheduled (JSON) approvals = approvals_by_section_id.get(section_id) scheduled = scheduled_by_section_id.get(section_id) # Instructors per cross-listings cross_listed_courses = cross_listings_per_section_id.get( section_id, []) instructors = instructors_per_section_id.get(section_id, []) # Construct course course = { 'allowedUnits': row['allowed_units'], 'approvals': approvals, 'canvasCourseSites': canvas_sites_by_section_id.get(section_id, []), 'courseName': row['course_name'], 'courseTitle': row['course_title'], 'crossListings': cross_listed_courses, 'deletedAt': safe_strftime(row['deleted_at'], '%Y-%m-%d'), 'hasOptedOut': section_id in section_ids_opted_out, 'instructionFormat': row['instruction_format'], 'instructors': instructors, 'invitees': invited_uids_by_section_id.get(section_id), 'isPrimary': row['is_primary'], 'label': _construct_course_label( course_name=row['course_name'], instruction_format=row['instruction_format'], section_num=row['section_num'], cross_listings=cross_listed_courses, ), 'meetings': { 'eligible': [], 'ineligible': [], }, 'nonstandardMeetingDates': False, 'sectionId': section_id, 'sectionNum': row['section_num'], 'scheduled': scheduled, 'termId': row['term_id'], } courses_per_id[section_id] = course # Note: Instructors associated with cross-listings are slurped up separately. instructor_uid = row['instructor_uid'] if instructor_uid and instructor_uid not in [ i['uid'] for i in course['instructors'] ]: course['instructors'].append( _to_instructor_json( row=row, approvals=course['approvals'], invited_uids=course['invitees'], ), ) meeting = _to_meeting_json(row) eligible_meetings = course['meetings']['eligible'] ineligible_meetings = course['meetings']['ineligible'] if not next((m for m in (eligible_meetings + ineligible_meetings) if meeting.items() <= m.items()), None): room = rooms_by_id.get( row['room_id']) if 'room_id' in row.keys() else None if room and room.capability: meeting['eligible'] = True meeting.update({ 'recordingEndDate': safe_strftime(get_recording_end_date(meeting), '%Y-%m-%d'), 'recordingStartDate': safe_strftime(get_recording_start_date(meeting), '%Y-%m-%d'), }) eligible_meetings.append(meeting) eligible_meetings.sort( key=lambda m: f"{m['startDate']} {m['startTime']}") if meeting['startDate'] != app.config[ 'CURRENT_TERM_BEGIN'] or meeting[ 'endDate'] != app.config['CURRENT_TERM_END']: course['nonstandardMeetingDates'] = True else: meeting['eligible'] = False ineligible_meetings.append(meeting) ineligible_meetings.sort( key=lambda m: f"{m['startDate']} {m['startTime']}") if include_rooms: meeting['room'] = room.to_api_json() if room else None # Next, construct the feed api_json = [] for section_id, course in courses_per_id.items(): _decorate_course(course) # Add course to the feed api_json.append(course) return api_json
def send( self, message, recipient, subject_line, term_id=None, section_id=None, template_type=None, ): if not message or not subject_line or not recipient: app.logger.error( 'Attempted to send a message without required fields: ' f'(recipient={recipient}, subject_line={subject_line}, message={message}' ) return False eb_env = get_eb_environment() prefix = '' if 'prod' in (eb_env or '') else f"[{eb_env or 'diablo-local'}] " subject_line = f'{prefix}{scrub_email_content(subject_line)}' message = scrub_email_content(message) @skip_when_pytest() def _send(): smtp = SMTP(self.bcop_smtp_server, port=self.bcop_smtp_port) # TLS encryption smtp.starttls() smtp.set_debuglevel(app.logger.level == logging.DEBUG) smtp.login(self.bcop_smtp_username, self.bcop_smtp_password) emails_sent_to = set() if app.config['DIABLO_ENV'] == 'test': write_email_to_log(message=message, recipient=recipient, subject_line=subject_line) else: from_address = f"{app.config['EMAIL_COURSE_CAPTURE_SUPPORT_LABEL']} <{app.config['EMAIL_COURSE_CAPTURE_SUPPORT']}>" for email_address in self.get_email_addresses(user=recipient): msg = MIMEMultipart('alternative') msg['From'] = from_address msg['To'] = email_address if app.config['EMAIL_TEST_MODE']: # Append intended recipient email address to verify when testing. intended_email = recipient['email'] msg['Subject'] = f'{subject_line} (To: {intended_email})' else: msg['Subject'] = subject_line # TODO: 'plain' text version of email? msg.attach(MIMEText(message, 'plain')) msg.attach(MIMEText(message, 'html')) # Send smtp.sendmail(from_addr=from_address, to_addrs=email_address, msg=msg.as_string()) emails_sent_to.add(email_address) app.logger.info( f'\'{template_type}\' email sent to {", ".join(list(emails_sent_to))}' ) # Disconnect smtp.quit() # Send emails _send() SentEmail.create( recipient_uid=recipient['uid'], section_id=section_id, template_type=template_type, term_id=term_id or app.config['CURRENT_TERM_ID'], ) return True