def test_course_with_partial_approval(self, client, admin_session): """Course with two instructors and one approval.""" with test_approvals_workflow(app): # If course has approvals but not scheduled then it will show up in the feed. approved_by_uid = get_instructor_uids(section_id=section_1_id, term_id=self.term_id)[0] room_id = Room.get_room_id(section_id=section_1_id, term_id=self.term_id) Approval.create( approved_by_uid=approved_by_uid, approver_type_='instructor', publish_type_='kaltura_my_media', recording_type_='presentation_audio', room_id=room_id, section_id=section_1_id, term_id=self.term_id, ) std_commit(allow_test_environment=True) api_json = api_get_course( client, term_id=self.term_id, section_id=section_1_id, ) assert [i['uid'] for i in api_json['instructors']] == ['10001', '10002'] approvals = api_json['approvals'] assert len(approvals) == 1 assert approved_by_uid == approvals[0]['approvedBy']['uid'] assert api_json['approvalStatus'] == 'Partially Approved' assert api_json['schedulingStatus'] == 'Not Scheduled' assert api_json['meetings']['eligible'][0]['room']['id'] == room_id assert api_json['meetings']['eligible'][0]['room']['location'] == 'Barrows 106'
def test_admin_approval(self): """Course is scheduled for recording if an admin user has approved.""" with test_approvals_workflow(app): section_id = 22287 term_id = app.config['CURRENT_TERM_ID'] course = SisSection.get_course(section_id=section_id, term_id=term_id) instructors = course['instructors'] assert len(instructors) == 2 # Verify that course is not scheduled assert Scheduled.get_scheduled(section_id=section_id, term_id=term_id) is None Approval.create( approved_by_uid=admin_uid, approver_type_='admin', cross_listed_section_ids=[], publish_type_='canvas', recording_type_='presentation_audio', room_id=Room.find_room('Barker 101').id, section_id=section_id, term_id=term_id, ) KalturaJob(app.app_context).run() std_commit(allow_test_environment=True) # Admin approval is all we need. assert Scheduled.get_scheduled(section_id=section_id, term_id=term_id)
def unschedule(): params = request.get_json() term_id = params.get('termId') section_id = params.get('sectionId') course = SisSection.get_course(term_id, section_id, include_deleted=True) if (term_id and section_id) else None if not course: raise BadRequestError('Required params missing or invalid') if not (course['scheduled'] or course['hasNecessaryApprovals']): raise BadRequestError(f'Section id {section_id}, term id {term_id} is not currently scheduled or queued for scheduling') Approval.delete(term_id=term_id, section_id=section_id) Scheduled.delete(term_id=term_id, section_id=section_id) event_id = (course.get('scheduled') or {}).get('kalturaScheduleId') if event_id: try: Kaltura().delete(event_id) except (KalturaClientException, KalturaException) as e: message = f'Failed to delete Kaltura schedule: {event_id}' app.logger.error(message) app.logger.exception(e) send_system_error_email( message=f'{message}\n\n<pre>{traceback.format_exc()}</pre>', subject=message, ) CoursePreference.update_opt_out( term_id=term_id, section_id=section_id, opt_out=True, ) return tolerant_jsonify(SisSection.get_course(term_id, section_id, include_deleted=True))
def test_admin_approval(self): """Course is scheduled for recording if an admin user has approved.""" with test_approvals_workflow(app): section_id = 50005 term_id = app.config['CURRENT_TERM_ID'] course = SisSection.get_course(section_id=section_id, term_id=term_id) instructors = course['instructors'] assert len(instructors) == 2 # Verify that course is not scheduled assert Scheduled.get_scheduled(section_id=section_id, term_id=term_id) is None Approval.create( approved_by_uid=admin_uid, approver_type_='admin', course_display_name=course['label'], publish_type_='kaltura_my_media', recording_type_='presentation_audio', room_id=Room.find_room('Barker 101').id, section_id=section_id, term_id=term_id, ) KalturaJob(simply_yield).run() std_commit(allow_test_environment=True) # Admin approval is all we need. assert Scheduled.get_scheduled(section_id=section_id, term_id=term_id)
def test_course_with_partial_approval(self, client, db, admin_session): """Course with two instructors and one approval.""" with test_approvals_workflow(app): # If course has approvals but not scheduled then it will show up in the feed. approved_by_uid = _get_instructor_uids(section_id=section_1_id, term_id=self.term_id)[0] room_id = Room.get_room_id(section_id=section_1_id, term_id=self.term_id) Approval.create( approved_by_uid=approved_by_uid, approver_type_='instructor', cross_listed_section_ids=[], publish_type_='canvas', recording_type_='presentation_audio', room_id=room_id, section_id=section_1_id, term_id=self.term_id, ) std_commit(allow_test_environment=True) api_json = api_get_course( client, term_id=self.term_id, section_id=section_1_id, ) assert [i['uid'] for i in api_json['instructors']] == ['234567', '8765432'] approvals = api_json['approvals'] assert len(approvals) == 1 assert approved_by_uid == approvals[0]['approvedBy']['uid'] assert api_json['room']['id'] == room_id assert api_json['room']['location'] == 'Barrows 106'
def approve(): term_id = app.config['CURRENT_TERM_ID'] term_name = term_name_for_sis_id(term_id) params = request.get_json() publish_type = params.get('publishType') recording_type = params.get('recordingType') section_id = params.get('sectionId') course = SisSection.get_course(term_id, section_id) if section_id else None if not course or publish_type not in get_all_publish_types() or recording_type not in get_all_recording_types(): raise BadRequestError('One or more required params are missing or invalid') if not current_user.is_admin and current_user.uid not in [i['uid'] for i in course['instructors']]: raise ForbiddenRequestError('Sorry, request unauthorized') if Approval.get_approval(approved_by_uid=current_user.uid, section_id=section_id, term_id=term_id): raise ForbiddenRequestError(f'You have already approved recording of {course["courseName"]}, {term_name}') meetings = course.get('meetings', {}).get('eligible', []) if len(meetings) != 1: raise BadRequestError('Unique eligible meeting pattern not found for course') meeting = meetings[0] location = meeting and meeting.get('location') room = Room.find_room(location=location) if not room: raise BadRequestError(f'{location} is not eligible for Course Capture.') previous_approvals = Approval.get_approvals_per_section_ids(section_ids=[section_id], term_id=term_id) approval = Approval.create( approved_by_uid=current_user.uid, approver_type_='admin' if current_user.is_admin else 'instructor', course_display_name=course['label'], publish_type_=publish_type, recording_type_=recording_type, room_id=room.id, section_id=section_id, term_id=term_id, ) if previous_approvals: # Compare the current approval with preferences submitted in previous approval previous_approval = previous_approvals[-1] if (approval.publish_type, approval.recording_type) != (previous_approval.publish_type, previous_approval.recording_type): notify_instructors_of_changes(course, approval, previous_approvals) all_approvals = previous_approvals + [approval] if len(course['instructors']) > len(all_approvals): approval_uids = [a.approved_by_uid for a in all_approvals] pending_instructors = [i for i in course['instructors'] if i['uid'] not in approval_uids] last_approver = next((i for i in course['instructors'] if i['uid'] == approval.approved_by_uid), None) if last_approver: notify_instructor_waiting_for_approval(course, last_approver, pending_instructors) return tolerant_jsonify(_after_approval(course=SisSection.get_course(term_id, section_id)))
def _create_approval(self, section_id): Approval.create( approved_by_uid=get_instructor_uids(section_id=section_id, term_id=self.term_id)[0], approver_type_='instructor', publish_type_='kaltura_my_media', recording_type_='presentation_audio', room_id=Room.get_room_id(section_id=section_id, term_id=self.term_id), section_id=section_id, term_id=self.term_id, )
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 test_alert_admin_of_instructor_change(self): """Emails admin when a scheduled course gets a new instructor.""" with test_approvals_workflow(app): with enabled_job(job_key=AdminEmailsJob.key()): term_id = app.config['CURRENT_TERM_ID'] section_id = 50005 room_id = Room.find_room('Barker 101').id # The course has two instructors. instructor_1_uid, instructor_2_uid = get_instructor_uids( section_id=section_id, term_id=term_id) approval = Approval.create( approved_by_uid=instructor_1_uid, approver_type_='instructor', course_display_name= f'term_id:{term_id} section_id:{section_id}', 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( course_display_name= f'term_id:{term_id} section_id:{section_id}', instructor_uids=[instructor_1_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, ) admin_uid = app.config['EMAIL_DIABLO_ADMIN_UID'] email_count = _get_email_count(admin_uid) # Message queued but not sent. AdminEmailsJob(simply_yield).run() assert _get_email_count(admin_uid) == email_count queued_messages = QueuedEmail.query.filter_by( template_type='admin_alert_instructor_change').all() assert len(queued_messages) == 1 for snippet in [ 'LAW 23', 'Old instructor(s) Regan MacNeil', 'New instructor(s) Regan MacNeil, Burke Dennings' ]: assert snippet in queued_messages[0].message # Message sent. QueuedEmailsJob(simply_yield).run() assert _get_email_count(admin_uid) == email_count + 1
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 _get_approvals_and_scheduled(section_ids, term_id): approvals = Approval.get_approvals_per_section_ids(section_ids=section_ids, term_id=term_id) scheduled = None for section_id in section_ids: if not scheduled: scheduled = Scheduled.get_scheduled(section_id=section_id, term_id=term_id) scheduled = scheduled and scheduled.to_api_json() break return [a.to_api_json() for a in approvals], scheduled
def approve(): term_id = app.config['CURRENT_TERM_ID'] term_name = term_name_for_sis_id(term_id) params = request.get_json() publish_type = params.get('publishType') recording_type = params.get('recordingType') section_id = params.get('sectionId') course = SisSection.get_course(term_id, section_id) if section_id else None if not course or publish_type not in get_all_publish_types() or recording_type not in get_all_recording_types(): raise BadRequestError('One or more required params are missing or invalid') if not current_user.is_admin and current_user.uid not in [i['uid'] for i in course['instructors']]: raise ForbiddenRequestError('Sorry, request unauthorized') if Approval.get_approval(approved_by_uid=current_user.uid, section_id=section_id, term_id=term_id): raise ForbiddenRequestError(f'You have already approved recording of {course["courseName"]}, {term_name}') location = course['meetingLocation'] room = Room.find_room(location=location) if not room: raise BadRequestError(f'{location} is not eligible for Course Capture.') previous_approvals = Approval.get_approvals_per_section_ids(section_ids=[section_id], term_id=term_id) approval = Approval.create( approved_by_uid=current_user.uid, approver_type_='admin' if current_user.is_admin else 'instructor', cross_listed_section_ids=[c['sectionId'] for c in course['crossListings']], publish_type_=publish_type, recording_type_=recording_type, room_id=room.id, section_id=section_id, term_id=term_id, ) _notify_instructors_of_approval( approval=approval, course=course, previous_approvals=previous_approvals, ) return tolerant_jsonify(SisSection.get_course(term_id, section_id))
def _approval_uids_per_section_id(scheduled, term_id): section_ids = [s.section_id for s in scheduled] all_approvals = Approval.get_approvals_per_section_ids( section_ids=section_ids, term_id=term_id) approval_uids_per_section_id = { section_id: [] for section_id in section_ids } for approval in all_approvals: approval_uids_per_section_id[approval.section_id].append( approval.approved_by_uid) return approval_uids_per_section_id
def _schedule_the_ready_to_schedule(): term_id = app.config['CURRENT_TERM_ID'] approvals = Approval.get_approvals_per_term(term_id=term_id) if approvals: approvals_per_section_id = objects_to_dict_organized_by_section_id(objects=approvals) ready_to_schedule = get_courses_ready_to_schedule(approvals=approvals, term_id=term_id) app.logger.info(f'Prepare to schedule recordings for {len(ready_to_schedule)} courses.') for course in ready_to_schedule: section_id = int(course['sectionId']) schedule_recordings( all_approvals=approvals_per_section_id[section_id], course=course, )
def test_partially_approved_filter(self, client, admin_session): """Partially approved: Eligible, invited course with 1+ approvals, but not ALL instructors have approved.""" with test_approvals_workflow(app): for section_id in [section_1_id, section_6_id, section_7_id]: # Assert multiple instructors assert len(get_instructor_uids(section_id=section_id, term_id=self.term_id)) > 1 # Send invites self._send_invitation_email(section_id) if section_id == section_1_id: # If course is "approved" by admin only then it will NOT show up on the partially-approval list. Approval.create( approved_by_uid=admin_uid, approver_type_='admin', publish_type_='kaltura_my_media', recording_type_='presentation_audio', room_id=Room.get_room_id(section_id=section_id, term_id=self.term_id), section_id=section_id, term_id=self.term_id, ) else: # Approval by first instructor only self._create_approval(section_id) # Feed will include both scheduled and not scheduled. for section_id in [section_1_id, section_7_id]: mock_scheduled(section_id=section_id, term_id=self.term_id) # Unschedule one of them Approval.delete(section_id=section_7_id, term_id=self.term_id) Scheduled.delete(section_id=section_7_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) == 1 course = _find_course(api_json=api_json, section_id=section_6_id) assert course assert course['label'] == 'LAW 23, LEC 002' assert course['approvalStatus'] == 'Partially Approved' assert course['schedulingStatus'] == 'Not Scheduled'
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_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_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 test_alert_admin_of_instructor_change(self): """Emails admin when a scheduled course gets a new instructor.""" with test_approvals_workflow(app): term_id = app.config['CURRENT_TERM_ID'] section_id = 22287 approved_by_uid = '8765432' room_id = Room.find_room('Barker 101').id approval = Approval.create( approved_by_uid=approved_by_uid, approver_type_='instructor', cross_listed_section_ids=[], publish_type_='canvas', recording_type_='presenter_audio', room_id=room_id, section_id=section_id, term_id=term_id, ) meeting_days, meeting_start_time, meeting_end_time = SisSection.get_meeting_times( term_id=term_id, section_id=section_id, ) Scheduled.create( cross_listed_section_ids=approval.cross_listed_section_ids, instructor_uids=SisSection.get_instructor_uids( term_id=term_id, section_id=section_id), meeting_days=meeting_days, meeting_start_time=meeting_start_time, meeting_end_time=meeting_end_time, publish_type_=approval.publish_type, recording_type_=approval.recording_type, room_id=room_id, section_id=section_id, term_id=term_id, ) admin_uid = app.config['EMAIL_DIABLO_ADMIN_UID'] email_count = _get_email_count(admin_uid) std_commit(allow_test_environment=True) AdminEmailsJob(app.app_context).run() std_commit(allow_test_environment=True) assert _get_email_count(admin_uid) > email_count
def test_alert_admin_of_room_change(self): """Emails admin when a scheduled course gets a room change.""" with test_approvals_workflow(app): term_id = app.config['CURRENT_TERM_ID'] section_id = 26094 approved_by_uid = '6789' 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', cross_listed_section_ids=[], publish_type_='kaltura_media_gallery', recording_type_='presenter_audio', room_id=scheduled_in_room.id, section_id=section_id, term_id=term_id, ) meeting_days, meeting_start_time, meeting_end_time = SisSection.get_meeting_times( term_id=term_id, section_id=section_id, ) Scheduled.create( cross_listed_section_ids=approval.cross_listed_section_ids, instructor_uids=SisSection.get_instructor_uids( term_id=term_id, section_id=section_id), meeting_days=meeting_days, meeting_start_time=meeting_start_time, meeting_end_time=meeting_end_time, 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'] email_count = _get_email_count(admin_uid) AdminEmailsJob(app.app_context).run() assert _get_email_count(admin_uid) == email_count + 1
def _after_approval(course): section_id = course['sectionId'] term_id = course['termId'] approvals = Approval.get_approvals(section_id=section_id, term_id=term_id) if get_courses_ready_to_schedule(approvals=approvals, term_id=term_id): # Queuing course for scheduling wipes any opt-out preference. if course['hasOptedOut']: CoursePreference.update_opt_out( term_id=term_id, section_id=section_id, opt_out=False, ) if app.config['FEATURE_FLAG_SCHEDULE_RECORDINGS_SYNCHRONOUSLY']: # Feature flag intended for dev workstation ONLY. Do not enable in diablo-dev|qa|prod. schedule_recordings( all_approvals=approvals, course=course, ) return SisSection.get_course(section_id=course['sectionId'], term_id=course['termId']) else: return course
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 test_cross_listed_course(self): """Recordings will be scheduled if and only if all instructor(s) approve.""" with test_approvals_workflow(app): section_id = 50012 term_id = app.config['CURRENT_TERM_ID'] email_template_type = 'recordings_scheduled' def _get_emails_sent(): return SentEmail.get_emails_of_type( section_ids=[section_id], template_type=email_template_type, term_id=term_id, ) emails_sent = _get_emails_sent() KalturaJob(simply_yield).run() std_commit(allow_test_environment=True) # Expect no emails sent during action above assert _get_emails_sent() == emails_sent course = SisSection.get_course(section_id=section_id, term_id=term_id) instructors = course['instructors'] cross_listings = course['crossListings'] room_id = Room.find_room( course['meetings']['eligible'][0]['location']).id assert len(instructors) == 2 assert room_id == Room.find_room("O'Brien 212").id assert len(cross_listings) == 1 assert len(course['canvasCourseSites']) == 2 # This course requires two (2) approvals. approvals = [ Approval.create( approved_by_uid=instructors[0]['uid'], approver_type_='instructor', course_display_name=course['label'], publish_type_='kaltura_my_media', recording_type_='presentation_audio', room_id=room_id, section_id=section_id, term_id=term_id, ), ] """If we have insufficient approvals then do nothing.""" email_count = _get_emails_sent() KalturaJob(simply_yield).run() std_commit(allow_test_environment=True) assert _get_emails_sent() == email_count # The second approval final_approval = Approval.create( approved_by_uid=instructors[1]['uid'], approver_type_='instructor', course_display_name=course['label'], publish_type_='kaltura_media_gallery', recording_type_='presenter_presentation_audio', room_id=room_id, section_id=section_id, term_id=term_id, ) approvals.append(final_approval) """If a course is scheduled for recording then email is sent to its instructor(s).""" email_count = len(_get_emails_sent()) KalturaJob(simply_yield).run() std_commit(allow_test_environment=True) # Verify publish and recording types scheduled = Scheduled.get_scheduled(term_id=term_id, section_id=section_id) assert scheduled.publish_type == final_approval.publish_type assert scheduled.recording_type == final_approval.recording_type assert scheduled.section_id == section_id assert scheduled.term_id == term_id # Verify emails sent QueuedEmailsJob(app.app_context).run() emails_sent = _get_emails_sent() assert len(emails_sent) == email_count + 2 assert [ emails_sent[-1].recipient_uid, emails_sent[-2].recipient_uid ] == ['10009', '10010'] email_sent = emails_sent[-1] assert email_sent.section_id == section_id assert email_sent.template_type == 'recordings_scheduled' assert email_sent.term_id == term_id """If recordings were already scheduled then do nothing, send no email.""" email_count = len(_get_emails_sent()) KalturaJob(simply_yield).run() assert len(_get_emails_sent()) == email_count
def test_scheduling_of_recordings(self): """If a course is scheduled for recording then email is sent to its instructor(s).""" with test_approvals_workflow(app): section_id = 22287 term_id = app.config['CURRENT_TERM_ID'] email_template_type = 'recordings_scheduled' def _get_emails_sent(): return SentEmail.get_emails_of_type( section_id=section_id, template_type=email_template_type, term_id=term_id, ) emails_sent = _get_emails_sent() KalturaJob(app.app_context).run() std_commit(allow_test_environment=True) # Expect no emails sent during action above assert _get_emails_sent() == emails_sent course = SisSection.get_course(section_id=section_id, term_id=term_id) instructors = course['instructors'] assert len(instructors) == 2 room_id = Room.find_room(course['meetingLocation']).id # This course requires two (2) approvals. approvals = [ Approval.create( approved_by_uid=instructors[0]['uid'], approver_type_='instructor', cross_listed_section_ids=[], publish_type_='canvas', recording_type_='presentation_audio', room_id=Room.find_room('Barker 101').id, section_id=section_id, term_id=term_id, ), ] """If we have insufficient approvals then do nothing.""" email_count = _get_emails_sent() KalturaJob(app.app_context).run() std_commit(allow_test_environment=True) assert _get_emails_sent() == email_count # The second approval final_approval = Approval.create( approved_by_uid=instructors[1]['uid'], approver_type_='instructor', cross_listed_section_ids=[], publish_type_='kaltura_media_gallery', recording_type_='presenter_presentation_audio', room_id=room_id, section_id=section_id, term_id=term_id, ) approvals.append(final_approval) """If a course is scheduled for recording then email is sent to its instructor(s).""" email_count = len(_get_emails_sent()) KalturaJob(app.app_context).run() std_commit(allow_test_environment=True) # Verify publish and recording types scheduled = Scheduled.get_scheduled(term_id=term_id, section_id=section_id) assert scheduled.publish_type == final_approval.publish_type assert scheduled.recording_type == final_approval.recording_type assert scheduled.section_id == section_id assert scheduled.term_id == term_id # Verify emails sent emails_sent = _get_emails_sent() assert len(emails_sent) == email_count + 1 email_sent = emails_sent[-1].to_api_json() assert set(email_sent['recipientUids']) == {'98765', '87654'} assert email_sent['sectionId'] == section_id assert email_sent['templateType'] == 'recordings_scheduled' assert email_sent['termId'] == term_id assert email_sent['sentAt'] """If recordings were already scheduled then do nothing, send no email.""" email_count = len(_get_emails_sent()) KalturaJob(app.app_context).run() assert len(_get_emails_sent()) == email_count