Пример #1
0
    def test_has_obsolete_instructors(self, client, admin_session):
        """Admins can see instructor changes that might disrupt scheduled recordings."""
        with test_approvals_workflow(app):
            meeting = get_eligible_meeting(section_id=section_1_id, term_id=self.term_id)
            instructor_uids = get_instructor_uids(term_id=self.term_id, section_id=section_1_id)
            # Course has multiple instructors; we will schedule using only one instructor UID.
            assert len(instructor_uids) > 1
            scheduled_with_uid = instructor_uids[0]
            Scheduled.create(
                instructor_uids=[scheduled_with_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_='kaltura_my_media',
                recording_type_='presenter_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,
            )
            std_commit(allow_test_environment=True)

            api_json = self._api_course_changes(client, term_id=self.term_id)
            course = _find_course(api_json=api_json, section_id=section_1_id)
            assert course
            assert course['scheduled']['hasObsoleteRoom'] is False
            assert course['scheduled']['hasObsoleteDates'] is False
            assert course['scheduled']['hasObsoleteTimes'] is False
            assert course['scheduled']['hasObsoleteInstructors'] is True
            assert len(course['instructors']) == 2
            assert len(course['scheduled']['instructors']) == 1
            assert course['scheduled']['instructors'][0]['uid'] == scheduled_with_uid
Пример #2
0
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))
Пример #3
0
def mock_scheduled(
    section_id,
    term_id,
    meeting=None,
    override_days=None,
    override_end_date=None,
    override_end_time=None,
    override_room_id=None,
    override_start_date=None,
    override_start_time=None,
    publish_type='kaltura_media_gallery',
    recording_type='presenter_presentation_audio',
):
    meeting = meeting or 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=get_instructor_uids(term_id=term_id,
                                            section_id=section_id),
        kaltura_schedule_id=random.randint(1, 10),
        meeting_days=override_days or meeting['days'],
        meeting_end_date=override_end_date or get_recording_end_date(meeting),
        meeting_end_time=override_end_time or meeting['endTime'],
        meeting_start_date=override_start_date
        or get_recording_start_date(meeting, return_today_if_past_start=True),
        meeting_start_time=override_start_time or meeting['startTime'],
        publish_type_=publish_type,
        recording_type_=recording_type,
        room_id=override_room_id
        or Room.get_room_id(section_id=section_id, term_id=term_id),
        section_id=section_id,
        term_id=term_id,
    )
    std_commit(allow_test_environment=True)
Пример #4
0
    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)
Пример #5
0
    def _notify(self, course, template_type):
        email_template = EmailTemplate.get_template_by_type(template_type)
        if email_template:

            def _get_interpolate_content(template):
                scheduled = course.get('scheduled', {})
                return interpolate_content(
                    course=course,
                    publish_type_name=scheduled.get('publishTypeName'),
                    recipient_name=recipient['name'],
                    recording_type_name=scheduled.get('recordingTypeName'),
                    templated_string=template,
                )

            recipient = get_admin_alert_recipient()
            QueuedEmail.create(
                message=_get_interpolate_content(email_template.message),
                recipient=recipient,
                section_id=course['sectionId'],
                subject_line=_get_interpolate_content(
                    email_template.subject_line),
                template_type=template_type,
                term_id=self.term_id,
            )
            Scheduled.add_alert(scheduled_id=course['scheduled']['id'],
                                template_type=template_type)
        else:
            send_system_error_email(f"""
                No email template of type {template_type} is available.
                Diablo admin NOT notified in regard to course {course['label']}.
            """)
Пример #6
0
    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)
Пример #7
0
    def test_scheduled_filter(self, client, admin_session):
        """Scheduled filter: Courses with recordings scheduled."""
        with test_approvals_workflow(app):
            # Send invites
            for section_id in [section_1_id, section_6_id]:
                self._send_invitation_email(section_id)
                self._create_approval(section_id)

            # Feed will only include courses that were scheduled.
            mock_scheduled(
                section_id=section_1_id,
                term_id=self.term_id,
            )
            # Deleted records will be ignored
            mock_scheduled(
                section_id=section_2_id,
                term_id=self.term_id,
            )
            Scheduled.delete(section_id=section_2_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
            course = _find_course(api_json=api_json, section_id=section_1_id)
            assert course['approvalStatus'] == 'Partially Approved'
            assert course['schedulingStatus'] == 'Scheduled'
            assert not _find_course(api_json=api_json, section_id=section_6_id)
Пример #8
0
    def test_has_instructors(self, client, admin_session):
        """Admins can see instructor changes that might disrupt scheduled recordings."""
        with test_approvals_workflow(app):
            meeting_days, meeting_start_time, meeting_end_time = SisSection.get_meeting_times(
                term_id=self.term_id,
                section_id=section_3_id,
            )
            instructor_uids = SisSection.get_instructor_uids(term_id=self.term_id, section_id=section_3_id)
            Scheduled.create(
                cross_listed_section_ids=[],
                instructor_uids=instructor_uids + ['999999'],
                meeting_days=meeting_days,
                meeting_start_time=meeting_start_time,
                meeting_end_time=meeting_end_time,
                publish_type_='canvas',
                recording_type_='presenter_audio',
                room_id=Room.get_room_id(section_id=section_3_id, term_id=self.term_id),
                section_id=section_3_id,
                term_id=self.term_id,
            )
            std_commit(allow_test_environment=True)

            api_json = self._api_course_changes(client, term_id=self.term_id)
            course = _find_course(api_json=api_json, section_id=section_3_id)
            assert course
            assert course['scheduled']['hasObsoleteRoom'] is False
            assert course['scheduled']['hasObsoleteMeetingTimes'] is False
            assert course['scheduled']['hasObsoleteInstructors'] is True
Пример #9
0
    def _room_change_alert(self):
        template_type = 'room_change_no_longer_eligible'
        all_scheduled = list(
            filter(
                lambda s: template_type not in (s.alerts or []),
                Scheduled.get_all_scheduled(term_id=self.term_id),
            ), )
        if all_scheduled:
            email_template = EmailTemplate.get_template_by_type(template_type)
            courses = SisSection.get_courses(
                term_id=self.term_id,
                section_ids=[s.section_id for s in all_scheduled],
                include_deleted=True,
            )
            courses_per_section_id = dict(
                (course['sectionId'], course) for course in courses)
            for scheduled in all_scheduled:
                course = courses_per_section_id.get(scheduled.section_id)
                if course:
                    if self._has_moved_to_ineligible_room(
                            course, scheduled) or course['deletedAt']:
                        if email_template:
                            for instructor in course['instructors']:

                                def _get_interpolate_content(template):
                                    return interpolate_content(
                                        course=course,
                                        publish_type_name=course.get(
                                            'scheduled',
                                            {}).get('publishTypeName'),
                                        recipient_name=instructor['name'],
                                        recording_type_name=course.get(
                                            'scheduled',
                                            {}).get('recordingTypeName'),
                                        templated_string=template,
                                    )

                                QueuedEmail.create(
                                    message=_get_interpolate_content(
                                        email_template.message),
                                    recipient=instructor,
                                    section_id=course['sectionId'],
                                    subject_line=_get_interpolate_content(
                                        email_template.subject_line),
                                    template_type=template_type,
                                    term_id=self.term_id,
                                )
                            Scheduled.add_alert(
                                scheduled_id=course['scheduled']['id'],
                                template_type=template_type)
                        else:
                            send_system_error_email(f"""
                                No '{template_type}' email template available.
                                We are unable to notify {course['label']} instructors of room change.
                            """)
                else:
                    subject = f'Scheduled course has no SIS data (section_id={scheduled.section_id})'
                    message = f'{subject}\n\nScheduled:<pre>{scheduled}</pre>'
                    app.logger.error(message)
                    send_system_error_email(message=message, subject=subject)
Пример #10
0
    def test_has_obsolete_meeting_dates(self, client, admin_session):
        """Admins can see meeting date changes that might disrupt scheduled recordings."""
        with test_approvals_workflow(app):
            meeting = get_eligible_meeting(section_id=section_1_id, term_id=self.term_id)
            obsolete_meeting_end_date = '2020-04-01'
            assert meeting['endDate'] != obsolete_meeting_end_date

            Scheduled.create(
                instructor_uids=get_instructor_uids(term_id=self.term_id, section_id=section_1_id),
                kaltura_schedule_id=random.randint(1, 10),
                meeting_days=meeting['days'],
                meeting_end_date=obsolete_meeting_end_date,
                meeting_end_time=meeting['endTime'],
                meeting_start_date=meeting['startDate'],
                meeting_start_time=meeting['startTime'],
                publish_type_='kaltura_my_media',
                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,
            )
            std_commit(allow_test_environment=True)

            api_json = self._api_course_changes(client, term_id=self.term_id)
            course = _find_course(api_json=api_json, section_id=section_1_id)
            assert course
            assert course['scheduled']['hasObsoleteRoom'] is False
            assert course['scheduled']['hasObsoleteDates'] is True
            assert course['scheduled']['hasObsoleteTimes'] is False
            assert course['scheduled']['hasObsoleteInstructors'] is False
Пример #11
0
    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_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_room_change_no_longer_eligible(self, db_session):
        section_id = 50004
        term_id = app.config['CURRENT_TERM_ID']

        def _move_course(meeting_location):
            db.session.execute(
                text(
                    'UPDATE sis_sections SET meeting_location = :meeting_location WHERE term_id = :term_id AND section_id = :section_id'
                ),
                {
                    'meeting_location': meeting_location,
                    'section_id': section_id,
                    'term_id': term_id,
                },
            )

        with enabled_job(job_key=InstructorEmailsJob.key()):
            with test_approvals_workflow(app):
                course = SisSection.get_course(section_id=section_id,
                                               term_id=term_id)
                eligible_meetings = course.get('meetings',
                                               {}).get('eligible', [])
                assert len(eligible_meetings) == 1
                original_room = eligible_meetings[0]['room']
                assert original_room['location'] == 'Li Ka Shing 145'

                # Schedule
                _schedule(original_room['id'], section_id)
                _run_instructor_emails_job()
                _assert_email_count(0, section_id,
                                    'room_change_no_longer_eligible')

                # Move course to some other eligible room.
                _move_course('Barker 101')
                _run_instructor_emails_job()
                _assert_email_count(0, section_id,
                                    'room_change_no_longer_eligible')

                # Move course to an ineligible room.
                ineligible_room = 'Wheeler 150'
                _move_course(ineligible_room)
                _run_instructor_emails_job()
                _assert_email_count(1, section_id,
                                    'room_change_no_longer_eligible')

                # Move course back to its original location
                _move_course(original_room['location'])

                # Finally, let's pretend the course is scheduled to a room that was previously eligible.
                Scheduled.delete(section_id=section_id, term_id=term_id)
                _schedule(Room.find_room(ineligible_room).id, section_id)
                _run_instructor_emails_job()
                # Expect email.
                _assert_email_count(2, section_id,
                                    'room_change_no_longer_eligible')
                Scheduled.delete(section_id=section_id, term_id=term_id)
Пример #14
0
    def test_admin_alert_date_change(self, db_session):
        with enabled_job(job_key=AdminEmailsJob.key()):
            admin_uid = app.config['EMAIL_DIABLO_ADMIN_UID']
            term_id = app.config['CURRENT_TERM_ID']
            section_id = 50004
            meeting = get_eligible_meeting(section_id=section_id,
                                           term_id=term_id)
            with test_approvals_workflow(app):
                with override_config(app, 'CURRENT_TERM_RECORDINGS_BEGIN',
                                     meeting['startDate']):
                    with override_config(app, 'CURRENT_TERM_RECORDINGS_END',
                                         meeting['endDate']):

                        def _run_jobs():
                            AdminEmailsJob(simply_yield).run()
                            QueuedEmailsJob(simply_yield).run()

                        def _schedule():
                            mock_scheduled(
                                meeting=meeting,
                                override_end_time='16:59',
                                override_start_time='08:00',
                                section_id=section_id,
                                term_id=term_id,
                            )
                            course = SisSection.get_course(
                                section_id=section_id, term_id=term_id)
                            scheduled = course['scheduled']
                            assert are_scheduled_dates_obsolete(
                                meeting=meeting, scheduled=scheduled) is False
                            assert are_scheduled_times_obsolete(
                                meeting=meeting, scheduled=scheduled) is 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'

                        # First time scheduled.
                        _schedule()
                        _run_jobs()
                        _assert_alert_count(1)
                        # Unschedule and schedule a second time.
                        Scheduled.delete(section_id=section_id,
                                         term_id=term_id)
                        _schedule()
                        _run_jobs()
                        # Another alert is emailed to admin because it is a new schedule.
                        _assert_alert_count(2)
                        # Run jobs again and expect no alerts.
                        _run_jobs()
                        _assert_alert_count(2)
Пример #15
0
def get_courses_ready_to_schedule(approvals, term_id):
    ready_to_schedule = []

    scheduled_section_ids = [
        s.section_id for s in Scheduled.get_all_scheduled(term_id=term_id)
    ]
    unscheduled_approvals = [
        approval for approval in approvals
        if approval.section_id not in scheduled_section_ids
    ]

    if unscheduled_approvals:
        courses = SisSection.get_courses(
            section_ids=[a.section_id for a in unscheduled_approvals],
            term_id=term_id)
        courses_per_section_id = dict(
            (int(course['sectionId']), course) for course in courses)
        admin_user_uids = set([
            user.uid
            for user in AdminUser.all_admin_users(include_deleted=True)
        ])

        for section_id, uids in _get_uids_per_section_id(
                approvals=unscheduled_approvals).items():
            if admin_user_uids.intersection(set(uids)):
                ready_to_schedule.append(courses_per_section_id[section_id])
            else:
                course = courses_per_section_id[section_id]
                necessary_uids = [i['uid'] for i in course['instructors']]
                if all(uid in uids for uid in necessary_uids):
                    ready_to_schedule.append(
                        courses_per_section_id[section_id])
    return ready_to_schedule
Пример #16
0
    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'
Пример #17
0
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 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
Пример #20
0
    def test_room_change_no_longer_eligible(self, db_session):
        with enabled_job(job_key=InstructorEmailsJob.key()):
            term_id = app.config['CURRENT_TERM_ID']
            section_id = 50004
            with test_approvals_workflow(app):
                def _run_jobs():
                    InstructorEmailsJob(simply_yield).run()
                    QueuedEmailsJob(simply_yield).run()

                def _schedule():
                    mock_scheduled(
                        override_room_id=Room.find_room('Barker 101').id,
                        section_id=section_id,
                        term_id=term_id,
                    )
                    course = SisSection.get_course(section_id=section_id, term_id=term_id)
                    assert course['scheduled']['hasObsoleteRoom'] is True

                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

                # First time scheduled.
                _schedule()
                _run_jobs()
                _assert_alert_count(1)
                # Unschedule and schedule a second time.
                Scheduled.delete(section_id=section_id, term_id=term_id)
                _schedule()
                _run_jobs()
                # Another alert is emailed to admin because it is a new schedule.
                _assert_alert_count(2)
                # Run jobs again and expect no alerts.
                _run_jobs()
                _assert_alert_count(2)
Пример #21
0
    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'
Пример #22
0
def _schedule_recordings(
        section_id,
        term_id,
        publish_type='kaltura_media_gallery',
        recording_type='presenter_presentation_audio',
        room_id=None,
):
    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=[],
        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_=publish_type,
        recording_type_=recording_type,
        room_id=room_id or Room.get_room_id(section_id=section_id, term_id=term_id),
        section_id=section_id,
        term_id=term_id,
    )
    std_commit(allow_test_environment=True)
Пример #23
0
 def run(self):
     term_id = app.config['CURRENT_TERM_ID']
     all_scheduled = Scheduled.get_all_scheduled(term_id=term_id)
     if all_scheduled:
         courses = SisSection.get_courses(
             term_id=term_id,
             section_ids=[s.section_id for s in all_scheduled])
         _alert_admin_of_instructor_change(
             courses=courses,
             approval_uids_per_section_id=_approval_uids_per_section_id(
                 scheduled=all_scheduled,
                 term_id=term_id,
             ),
         )
         _alert_admin_of_room_change(
             courses=courses,
             scheduled_rooms_per_section_id=
             _scheduled_locations_per_section_id(all_scheduled),
         )
Пример #24
0
 def run(self):
     term_id = app.config['CURRENT_TERM_ID']
     all_scheduled = Scheduled.get_all_scheduled(term_id=term_id)
     if all_scheduled:
         courses = SisSection.get_courses(
             term_id=term_id,
             section_ids=[s.section_id for s in all_scheduled])
         courses_per_section_id = dict(
             (course['sectionId'], course) for course in courses)
         for scheduled in all_scheduled:
             course = courses_per_section_id[scheduled.section_id]
             if course:
                 if scheduled.room_id != course['room']['id']:
                     email_template = EmailTemplate.get_template_by_type(
                         'room_change_no_longer_eligible')
                     for instructor in course['instructor']:
                         BConnected().send(
                             message=interpolate_email_content(
                                 templated_string=email_template.message,
                                 course=course,
                                 instructor_name=instructor['name'],
                                 recipient_name=instructor['name'],
                                 recording_type_name=scheduled.
                                 recording_type,
                             ),
                             recipients=course['instructors'],
                             subject_line=interpolate_email_content(
                                 templated_string=email_template.
                                 subject_line,
                                 course=course,
                                 instructor_name=instructor['name'],
                                 recipient_name=instructor['name'],
                                 recording_type_name=scheduled.
                                 recording_type,
                             ),
                         )
             else:
                 error = f'section_id of scheduled recordings was not found in SIS data: {scheduled}'
                 app.logger.error(error)
                 send_system_error_email(message=error)
Пример #25
0
def get_courses_ready_to_schedule(approvals, term_id):
    ready_to_schedule = []
    scheduled_section_ids = [
        s.section_id for s in Scheduled.get_all_scheduled(term_id=term_id)
    ]
    unscheduled_approvals = [
        approval for approval in approvals
        if approval.section_id not in scheduled_section_ids
    ]

    if unscheduled_approvals:
        courses = SisSection.get_courses(
            section_ids=[a.section_id for a in unscheduled_approvals],
            term_id=term_id)
        courses_per_section_id = dict(
            (int(course['sectionId']), course) for course in courses)
        admin_user_uids = set([
            user.uid
            for user in AdminUser.all_admin_users(include_deleted=True)
        ])

        for section_id, approved_by_uids in _get_uids_per_section_id(
                approvals=unscheduled_approvals).items():
            course = courses_per_section_id.get(section_id)
            if not course:
                continue
            if len(course.get('meetings', {}).get('eligible', [])) != 1:
                app.logger.warn(
                    f'Unique meeting pattern not found for section id {section_id}; will not schedule.'
                )
                continue
            if admin_user_uids.intersection(set(approved_by_uids)):
                ready_to_schedule.append(course)
            else:
                necessary_uids = [i['uid'] for i in course['instructors']]
                if all(uid in approved_by_uids for uid in necessary_uids):
                    ready_to_schedule.append(course)

    return ready_to_schedule
Пример #26
0
def courses_report(term_id):
    return tolerant_jsonify({
        'totalScheduledCount': len(Scheduled.get_all_scheduled(term_id=term_id)),
    })
Пример #27
0
def schedule_recordings(all_approvals, course):
    def _report_error(subject):
        message = f'{subject}\n\n<pre>{course}</pre>'
        app.logger.error(message)
        send_system_error_email(message=message, subject=subject)

    meetings = course.get('meetings', {}).get('eligible', [])
    meeting = meetings[0] if len(meetings) == 1 else None
    if not meeting:
        _report_error(
            subject=
            f"{course['label']} not scheduled. Unique eligible meeting pattern not found."
        )
        return None

    all_approvals.sort(key=lambda a: a.created_at.isoformat())
    latest_approval = all_approvals[-1]
    room = Room.get_room(latest_approval.room_id)
    if room.location != meeting['location']:
        _report_error(
            subject=
            f"{course['label']} not scheduled. Room change: {room.location} to {meeting['location']}"
        )
        return None

    has_admin_approval = next(
        (a for a in all_approvals if a.approver_type == 'admin'), None)
    approved_by_uids = set(a.approved_by_uid for a in all_approvals)
    instructor_uids = set([i['uid'] for i in course['instructors']])
    if not has_admin_approval and not instructor_uids.issubset(
            approved_by_uids):
        _report_error(
            subject=
            f"{course['label']} not scheduled. We are missing instructor approval(s)."
        )
        return None

    term_id = course['termId']
    section_id = int(course['sectionId'])
    scheduled = None
    if room.kaltura_resource_id:
        try:
            kaltura_schedule_id = Kaltura().schedule_recording(
                canvas_course_site_ids=[
                    c['courseSiteId'] for c in course['canvasCourseSites']
                ],
                course_label=course['label'],
                instructors=course['instructors'],
                meeting=meeting,
                publish_type=latest_approval.publish_type,
                recording_type=latest_approval.recording_type,
                room=room,
                term_id=term_id,
            )
            scheduled = Scheduled.create(
                course_display_name=course['label'],
                instructor_uids=instructor_uids,
                kaltura_schedule_id=kaltura_schedule_id,
                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_=latest_approval.publish_type,
                recording_type_=latest_approval.recording_type,
                room_id=room.id,
                section_id=section_id,
                term_id=term_id,
            )
            # Turn off opt-out setting if present.
            if section_id in CoursePreference.get_section_ids_opted_out(
                    term_id=term_id):
                CoursePreference.update_opt_out(
                    term_id=term_id,
                    section_id=section_id,
                    opt_out=False,
                )
            notify_instructors_recordings_scheduled(course=course,
                                                    scheduled=scheduled)
            uids = [approval.approved_by_uid for approval in all_approvals]
            app.logger.info(
                f'Recordings scheduled for course {section_id} per approvals: {", ".join(uids)}'
            )

        except (KalturaClientException, KalturaException) as e:
            # Error codes: https://developer.kaltura.com/api-docs/Error_Codes
            summary = f"Failed to schedule recordings {course['label']} (section_id: {course['sectionId']})"
            app.logger.error(summary)
            app.logger.exception(e)
            send_system_error_email(
                message=f'{summary}\n\n<pre>{traceback.format_exc()}</pre>',
                subject=f'{summary[:50]}...' if len(summary) > 50 else summary,
            )

    else:
        app.logger.warn(f"""
            SKIP schedule recordings because room has no 'kaltura_resource_id'.
            Course: {course['label']}
            Room: {room.location}
            Latest approved_by_uid: {latest_approval.approved_by_uid}
        """)

    return scheduled
Пример #28
0
def _schedule_recordings(all_approvals, course):
    term_id = course['termId']
    section_id = int(course['sectionId'])
    all_approvals.sort(key=lambda a: a.created_at.isoformat())
    approval = all_approvals[-1]

    room = Room.get_room(approval.room_id)
    meeting_days, meeting_start_time, meeting_end_time = SisSection.get_meeting_times(
        term_id=term_id,
        section_id=section_id,
    )
    time_format = '%H:%M'
    # Recording starts X minutes before/after official start; it ends Y minutes before/after official end time.
    recording_offset_start = app.config['KALTURA_RECORDING_OFFSET_START']
    recording_offset_end = app.config['KALTURA_RECORDING_OFFSET_END']
    adjusted_start_time = datetime.strptime(
        meeting_start_time,
        time_format) + timedelta(minutes=recording_offset_start)
    adjusted_end_time = datetime.strptime(
        meeting_end_time,
        time_format) + timedelta(minutes=recording_offset_end)
    days = format_days(meeting_days)
    instructor_uids = [
        instructor['uid'] for instructor in course['instructors']
    ]

    app.logger.info(f"""
        Prepare to schedule recordings for {course["label"]}:
            Room: {room.location}
            Instructor UIDs: {instructor_uids}
            Schedule: {days}, {adjusted_start_time} to {adjusted_end_time}
            Recording: {approval.recording_type}; {approval.publish_type}
    """)

    if room.kaltura_resource_id:
        Kaltura().schedule_recording(
            course_label=course['label'],
            instructor_uids=instructor_uids,
            days=days,
            start_time=adjusted_start_time,
            end_time=adjusted_end_time,
            publish_type=approval.publish_type,
            recording_type=approval.recording_type,
            room=room,
        )
        scheduled = 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=approval.room_id,
            section_id=section_id,
            term_id=term_id,
        )
        notify_instructors_recordings_scheduled(course=course,
                                                scheduled=scheduled)

        uids = [approval.approved_by_uid for approval in all_approvals]
        app.logger.info(
            f'Recordings scheduled for course {section_id} per approvals: {", ".join(uids)}'
        )

    else:
        app.logger.error(f"""
            FAILED to schedule recordings because room has no 'kaltura_resource_id'.
            Course: {course}
            Room: {room}
            Latest approval: {approval}
        """)
Пример #29
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
Пример #30
0
 def test_total_scheduled_count(self, client, admin_session):
     """The courses report includes valid total_scheduled_count."""
     report = self._api_courses_report(client, term_id=self.term_id)
     assert report['totalScheduledCount'] == len(Scheduled.get_all_scheduled(self.term_id))