Example #1
0
 def _send_invitation_email(self, section_id):
     SentEmail.create(
         section_id=section_id,
         recipient_uid=get_instructor_uids(section_id=section_id, term_id=self.term_id)[0],
         template_type='invitation',
         term_id=self.term_id,
     )
Example #2
0
    def test_approval_by_instructors(self, app, client, fake_auth):
        """Instructor can submit approval if s/he is teaching the requested course."""
        with test_approvals_workflow(app):
            instructor_uids = get_instructor_uids(section_id=section_1_id, term_id=self.term_id)
            fake_auth.login(instructor_uids[0])
            api_approve(
                client,
                publish_type='kaltura_my_media',
                recording_type='presentation_audio',
                section_id=section_1_id,
            )
            std_commit(allow_test_environment=True)

            fake_auth.login(instructor_uids[1])
            api_approve(
                client,
                publish_type='kaltura_media_gallery',
                recording_type='presentation_audio',
                section_id=section_1_id,
            )
            std_commit(allow_test_environment=True)

            QueuedEmailsJob(app.app_context).run()

            # First instructor was notified 1) that second instructor needed to approve; 2) that second instructor made changes.
            emails_sent = SentEmail.get_emails_sent_to(instructor_uids[0])
            assert len(emails_sent) == 2
            for email in emails_sent:
                assert email.section_id == section_1_id
                assert email.term_id == self.term_id
            assert emails_sent[0].template_type == 'waiting_for_approval'
            assert emails_sent[1].template_type == 'notify_instructor_of_changes'

            # Second instructor received no notifications.
            assert len(SentEmail.get_emails_sent_to(instructor_uids[1])) == 0

            fake_auth.login(admin_uid)
            api_json = api_get_course(
                client,
                term_id=self.term_id,
                section_id=section_1_id,
            )
            assert api_json['meetings']['eligible'][0]['room']['location'] == 'Barrows 106'
            instructor_uids = [i['uid'] for i in api_json['instructors']]
            assert instructor_uids == instructor_uids
            approvals_ = api_json['approvals']
            assert len(approvals_) == 2

            assert approvals_[0]['approvedBy']['uid'] == instructor_uids[0]
            assert approvals_[0]['publishType'] == 'kaltura_my_media'

            assert approvals_[1]['approvedBy']['uid'] == instructor_uids[1]
            assert approvals_[1]['publishType'] == 'kaltura_media_gallery'
            assert approvals_[1]['recordingType'] == 'presentation_audio'
            assert approvals_[1]['recordingTypeName'] == 'Presentation and Audio'

            assert api_json['hasNecessaryApprovals'] is True
            assert api_json['scheduled'] is None
Example #3
0
def _create_emails_sent():
    term_id = app.config['CURRENT_TERM_ID']
    SentEmail.create(
        recipient_uids=['8765432'],
        section_id='28165',
        template_type='invitation',
        term_id=term_id,
    )
    std_commit(allow_test_environment=True)
Example #4
0
 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'
Example #5
0
 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
Example #6
0
    def test_not_invited_filter(self, client, admin_session):
        """Not-invited filter: Courses in eligible rooms, never sent an invitation. No approval. Not scheduled."""
        with test_approvals_workflow(app):
            # The first course gets an invitation
            self._send_invitation_email(section_1_id)

            # The second course did not receive an invitation BUT it does have approval.
            invite = SentEmail.get_emails_of_type(
                section_ids=[section_4_id],
                template_type='invitation',
                term_id=self.term_id,
            )
            assert not invite

            self._create_approval(section_4_id)
            std_commit(allow_test_environment=True)
            api_json = self._api_courses(client, term_id=self.term_id, filter_='Not Invited')
            assert not _find_course(api_json=api_json, section_id=section_1_id)
            assert not _find_course(api_json=api_json, section_id=section_4_id)
            # Zero instructors is acceptable
            assert _find_course(api_json=api_json, section_id=eligible_course_with_no_instructors)
            # Third course is in enabled room and has not received an invite. Therefore, it is in the feed.
            assert _is_course_in_enabled_room(section_id=section_3_id, term_id=self.term_id)
            course = _find_course(api_json=api_json, section_id=section_3_id)
            assert course['approvalStatus'] == 'Not Invited'
            assert course['schedulingStatus'] == 'Not Scheduled'
            assert course['label'] == 'BIO 1B, LEC 001'
Example #7
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_do_not_email_filter(self, client, db, admin_session):
        """Do Not Email filter: Courses in eligible room; "opt out" is true; all stages of approval; not scheduled."""
        with test_approvals_workflow(app):
            # Send invites them opt_out.
            for section_id in (section_1_id, section_in_ineligible_room, section_3_id, section_4_id):
                CoursePreference.update_opt_out(section_id=section_id, term_id=self.term_id, opt_out=True)

                in_enabled_room = _is_course_in_enabled_room(section_id=section_id, term_id=self.term_id)
                if section_id == section_in_ineligible_room:
                    # Courses in ineligible rooms will be excluded from the feed.
                    assert not in_enabled_room
                else:
                    assert in_enabled_room
                    SentEmail.create(
                        section_id=section_id,
                        recipient_uids=_get_instructor_uids(section_id=section_id, term_id=self.term_id),
                        template_type='invitation',
                        term_id=self.term_id,
                    )

            # If course has approvals but not scheduled then it will show up in the feed.
            Approval.create(
                approved_by_uid=_get_instructor_uids(section_id=section_1_id, term_id=self.term_id)[0],
                approver_type_='instructor',
                cross_listed_section_ids=[],
                publish_type_='canvas',
                recording_type_='presentation_audio',
                room_id=Room.get_room_id(section_id=section_1_id, term_id=self.term_id),
                section_id=section_1_id,
                term_id=self.term_id,
            )
            # Feed will exclude scheduled.
            _schedule_recordings(
                section_id=section_3_id,
                term_id=self.term_id,
            )
            std_commit(allow_test_environment=True)

            api_json = self._api_courses(client, term_id=self.term_id, filter_='Do Not Email')
            for section_id in (section_1_id, section_4_id):
                # The 'Do Not Email' course is in the feed
                assert _find_course(api_json=api_json, section_id=section_id)

            for section_id in (section_3_id, section_in_ineligible_room):
                # Excluded courses
                assert not _find_course(api_json=api_json, section_id=section_id)
def _assert_email_count(expected_count, section_id, template_type):
    term_id = app.config['CURRENT_TERM_ID']
    emails_sent = SentEmail.get_emails_of_type(
        section_ids=[section_id],
        template_type=template_type,
        term_id=term_id,
    )
    assert len(emails_sent) == expected_count
 def test_partially_approved_filter(self, client, db, admin_session):
     """Partially approved: Eligible, invited course with 1+ approvals, but not ALL instructors have approved."""
     with test_approvals_workflow(app):
         # Assert multiple instructors
         section_1_instructor_uids = _get_instructor_uids(section_id=section_1_id, term_id=self.term_id)
         section_6_instructor_uids = _get_instructor_uids(section_id=section_6_id, term_id=self.term_id)
         assert len(section_1_instructor_uids) > 1
         assert len(section_6_instructor_uids) > 1
         # Send invites
         courses = [
             {'section_id': section_1_id, 'instructor_uids': section_1_instructor_uids},
             {'section_id': section_6_id, 'instructor_uids': section_6_instructor_uids},
         ]
         for course in courses:
             SentEmail.create(
                 section_id=course['section_id'],
                 recipient_uids=course['instructor_uids'],
                 template_type='invitation',
                 term_id=self.term_id,
             )
             Approval.create(
                 approved_by_uid=course['instructor_uids'][0],
                 approver_type_='instructor',
                 cross_listed_section_ids=[],
                 publish_type_='canvas',
                 recording_type_='presentation_audio',
                 room_id=Room.get_room_id(section_id=course['section_id'], term_id=self.term_id),
                 section_id=course['section_id'],
                 term_id=self.term_id,
             )
         # Feed will include both scheduled and not scheduled.
         _schedule_recordings(
             section_id=section_1_id,
             term_id=self.term_id,
         )
         std_commit(allow_test_environment=True)
         api_json = self._api_courses(client, term_id=self.term_id, filter_='Partially Approved')
         assert len(api_json) == 2
         assert _find_course(api_json=api_json, section_id=section_1_id)
         course = _find_course(api_json=api_json, section_id=section_6_id)
         assert course
         assert course['label'] == 'LAW 23, LEC 002'
    def test_approval_by_instructors(self, client, db, fake_auth):
        """Instructor can submit approval if s/he is teaching the requested course."""
        instructor_uids = _get_instructor_uids(section_id=section_1_id, term_id=self.term_id)
        fake_auth.login(instructor_uids[0])
        api_approve(
            client,
            publish_type='canvas',
            recording_type='presentation_audio',
            section_id=section_1_id,
        )
        std_commit(allow_test_environment=True)

        fake_auth.login(instructor_uids[1])
        api_approve(
            client,
            publish_type='kaltura_media_gallery',
            recording_type='presentation_audio',
            section_id=section_1_id,
        )
        std_commit(allow_test_environment=True)

        for uid in ('234567', '8765432'):
            emails_sent = SentEmail.get_emails_sent_to(uid)
            assert len(emails_sent) > 0
            most_recent = emails_sent[-1]
            assert most_recent.section_id == section_1_id
            assert most_recent.template_type == 'notify_instructor_of_changes'
            assert most_recent.term_id == self.term_id

        fake_auth.login(admin_uid)
        api_json = api_get_course(
            client,
            term_id=self.term_id,
            section_id=section_1_id,
        )
        assert api_json['room']['location'] == 'Barrows 106'
        instructor_uids = [i['uid'] for i in api_json['instructors']]
        assert instructor_uids == instructor_uids
        approvals_ = api_json['approvals']
        assert len(approvals_) == 2

        assert approvals_[0]['approvedBy']['uid'] == instructor_uids[0]
        assert approvals_[0]['publishType'] == 'canvas'

        assert approvals_[1]['approvedBy']['uid'] == instructor_uids[1]
        assert approvals_[1]['publishType'] == 'kaltura_media_gallery'
        assert approvals_[1]['recordingType'] == 'presentation_audio'
        assert approvals_[1]['recordingTypeName'] == 'Presentation and Audio'

        assert api_json['hasNecessaryApprovals'] is True
        assert api_json['scheduled'] is None
 def test_scheduled_filter(self, client, db, admin_session):
     """Scheduled filter: Courses with recordings scheduled."""
     with test_approvals_workflow(app):
         section_1_instructor_uids = _get_instructor_uids(section_id=section_1_id, term_id=self.term_id)
         section_6_instructor_uids = _get_instructor_uids(section_id=section_6_id, term_id=self.term_id)
         # Send invites
         courses = [
             {'section_id': section_1_id, 'instructor_uids': section_1_instructor_uids},
             {'section_id': section_6_id, 'instructor_uids': section_6_instructor_uids},
         ]
         for course in courses:
             SentEmail.create(
                 section_id=course['section_id'],
                 recipient_uids=course['instructor_uids'],
                 template_type='invitation',
                 term_id=self.term_id,
             )
             Approval.create(
                 approved_by_uid=course['instructor_uids'][0],
                 approver_type_='instructor',
                 cross_listed_section_ids=[],
                 publish_type_='canvas',
                 recording_type_='presentation_audio',
                 room_id=Room.get_room_id(section_id=course['section_id'], term_id=self.term_id),
                 section_id=course['section_id'],
                 term_id=self.term_id,
             )
         # Feed will only include courses that were scheduled.
         _schedule_recordings(
             section_id=section_1_id,
             term_id=self.term_id,
         )
         std_commit(allow_test_environment=True)
         api_json = self._api_courses(client, term_id=self.term_id, filter_='Scheduled')
         assert len(api_json) == 1
         assert _find_course(api_json=api_json, section_id=section_1_id)
         assert not _find_course(api_json=api_json, section_id=section_6_id)
 def test_not_invited_filter(self, client, db, admin_session):
     """Not-invited filter: Courses in eligible rooms, never sent an invitation. No approval. Not scheduled."""
     with test_approvals_workflow(app):
         # The first course gets an invitation
         section_1_instructor_uids = _get_instructor_uids(section_id=section_1_id, term_id=self.term_id)
         SentEmail.create(
             section_id=section_1_id,
             recipient_uids=section_1_instructor_uids,
             template_type='invitation',
             term_id=self.term_id,
         )
         # The second course did not receive an invitation BUT it does have approval.
         invite = SentEmail.get_emails_of_type(
             section_id=section_4_id,
             template_type='invitation',
             term_id=self.term_id,
         )
         assert not invite
         Approval.create(
             approved_by_uid=_get_instructor_uids(section_id=section_4_id, term_id=self.term_id)[0],
             approver_type_='instructor',
             cross_listed_section_ids=[],
             publish_type_='canvas',
             recording_type_='presentation_audio',
             room_id=Room.get_room_id(section_id=section_4_id, term_id=self.term_id),
             section_id=section_4_id,
             term_id=self.term_id,
         )
         std_commit(allow_test_environment=True)
         api_json = self._api_courses(client, term_id=self.term_id, filter_='Not Invited')
         assert not _find_course(api_json=api_json, section_id=section_1_id)
         assert not _find_course(api_json=api_json, section_id=section_4_id)
         # Third course is in enabled room and has not received an invite. Therefore, it is in the feed.
         assert _is_course_in_enabled_room(section_id=section_3_id, term_id=self.term_id)
         course = _find_course(api_json=api_json, section_id=section_3_id)
         assert course
         assert course['label'] == 'BIO 1B, LEC 001'
 def test_invited_filter(self, client, db, admin_session):
     """Invited filter: Course in an eligible room, have received invitation. No approvals. Not scheduled."""
     with test_approvals_workflow(app):
         # First, send invitations
         SentEmail.create(
             section_id=section_4_id,
             recipient_uids=_get_instructor_uids(section_id=section_4_id, term_id=self.term_id),
             template_type='invitation',
             term_id=self.term_id,
         )
         section_5_instructor_uids = _get_instructor_uids(section_id=section_5_id, term_id=self.term_id)
         SentEmail.create(
             section_id=section_5_id,
             recipient_uids=section_5_instructor_uids,
             template_type='invitation',
             term_id=self.term_id,
         )
         # The section with approval will NOT show up in search results
         Approval.create(
             approved_by_uid=section_5_instructor_uids[0],
             approver_type_='instructor',
             cross_listed_section_ids=[],
             publish_type_='canvas',
             recording_type_='presentation_audio',
             room_id=SisSection.get_course(term_id=self.term_id, section_id=section_5_id)['room']['id'],
             section_id=section_5_id,
             term_id=self.term_id,
         )
         std_commit(allow_test_environment=True)
         api_json = self._api_courses(client, term_id=self.term_id, filter_='Invited')
         # Section with ZERO approvals will show up in search results
         course = _find_course(api_json=api_json, section_id=section_4_id)
         assert course
         assert course['label'] == 'CHEM C110L, LAB 001'
         # The section with approval will NOT show up in search results
         assert not _find_course(api_json=api_json, section_id=section_5_id)
Example #15
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'
Example #16
0
def get_emails_sent_to_uid(uid):
    return tolerant_jsonify(
        [e.to_api_json() for e in SentEmail.get_emails_sent_to(uid)])
Example #17
0
def _get_email_count(uid):
    return len(SentEmail.get_emails_sent_to(uid=uid))
Example #18
0
    def send(
        self,
        message,
        recipients,
        subject_line,
        term_id=None,
        section_id=None,
        template_type=None,
    ):
        @skip_when_pytest()
        def _send():
            # Connect to SMTP server
            smtp = SMTP(self.bcop_smtp_server, port=self.bcop_smtp_port)
            # TLS encryption
            smtp.starttls()
            smtp.set_debuglevel(app.logger.level == logging.DEBUG)
            smtp.login(self.bcop_smtp_username, self.bcop_smtp_password)

            emails_sent_to = set()

            for recipient in recipients:
                mock_message = _get_mock_message(
                    recipient['name'],
                    recipient['email'],
                    subject_line,
                    message,
                )
                if app.config['DIABLO_ENV'] == 'test':
                    app.logger.info(mock_message)
                else:
                    from_address = app.config['EMAIL_DIABLO_SUPPORT']
                    to_address = self.get_email_address(
                        user=recipient, subject_line=subject_line)

                    msg = MIMEMultipart('alternative')
                    msg['From'] = from_address
                    msg['To'] = to_address
                    msg['Bcc'] = app.config['EMAIL_DIABLO_ADMIN']

                    if app.config['EMAIL_TEST_MODE']:
                        # Append intended recipient email address to verify when testing.
                        intended_email = recipient['email']
                        msg['Subject'] = f'{subject_line} (To: {intended_email})'
                    else:
                        msg['Subject'] = subject_line

                    # TODO: 'plain' text version of email?
                    msg.attach(MIMEText(message, 'plain'))
                    msg.attach(MIMEText(message, 'html'))
                    # Send
                    smtp.sendmail(from_addr=from_address,
                                  to_addrs=to_address,
                                  msg=msg.as_string())

                    emails_sent_to.add(to_address)

            app.logger.info(
                f'{len(recipients)} \'{template_type}\' emails sent to {", ".join(list(emails_sent_to))}'
            )
            # Disconnect
            smtp.quit()

        # Send emails
        _send()

        SentEmail.create(
            recipient_uids=[recipient['uid'] for recipient in recipients],
            section_id=section_id,
            template_type=template_type,
            term_id=term_id or app.config['CURRENT_TERM_ID'],
        )
Example #19
0
def _get_emails_sent(email_template_type, section_id, term_id):
    return SentEmail.get_emails_of_type(
        section_ids=[section_id],
        template_type=email_template_type,
        term_id=term_id,
    )
Example #20
0
def _to_api_json(term_id, rows, include_rooms=True):
    courses_per_id = {}
    instructors_per_section_id = {}
    section_ids_opted_out = CoursePreference.get_section_ids_opted_out(
        term_id=term_id)
    # If course has multiple instructors then the section_id will be represented across multiple rows.
    for row in rows:
        approvals = []
        section_id = int(row['section_id'])
        if section_id not in courses_per_id:
            # Construct new course
            instructors_per_section_id[section_id] = []
            has_opted_out = section_id in section_ids_opted_out
            cross_listings = _get_cross_listed_courses(section_id=section_id,
                                                       term_id=term_id)
            approvals, scheduled = _get_approvals_and_scheduled(
                section_ids=[section_id] +
                [c['sectionId'] for c in cross_listings],
                term_id=term_id,
            )
            course_name = row['course_name']
            instruction_format = row['instruction_format']
            section_num = row['section_num']
            course = {
                'allowedUnits': row['allowed_units'],
                'canvasCourseSites': _canvas_course_sites(term_id, section_id),
                'courseName': course_name,
                'courseTitle': row['course_title'],
                'crossListings': cross_listings,
                'hasOptedOut': has_opted_out,
                'instructionFormat': instruction_format,
                'instructors': [],
                'isPrimary': row['is_primary'],
                'label': f'{course_name}, {instruction_format} {section_num}',
                'meetingDays': format_days(row['meeting_days']),
                'meetingEndDate': row['meeting_end_date'],
                'meetingEndTime': format_time(row['meeting_end_time']),
                'meetingLocation': row['meeting_location'],
                'meetingStartDate': row['meeting_start_date'],
                'meetingStartTime': format_time(row['meeting_start_time']),
                'sectionId': section_id,
                'sectionNum': section_num,
                'termId': row['term_id'],
                'approvals': approvals,
                'scheduled': scheduled,
            }
            invites = SentEmail.get_emails_of_type(
                section_id=section_id,
                template_type='invitation',
                term_id=term_id,
            )
            course['invitees'] = []
            for invite in invites:
                course['invitees'].extend(invite.recipient_uids)

            if scheduled:
                course['status'] = 'Scheduled'
            elif approvals:
                course['status'] = 'Partially Approved'
            else:
                course['status'] = 'Invited' if invites else 'Not Invited'

            if include_rooms:
                room = Room.get_room(
                    row['room_id']).to_api_json() if 'room_id' in row else None
                course['room'] = room
            courses_per_id[section_id] = course

        # Build upon course object with one instructor per row.
        instructor_uid = row['instructor_uid']
        if instructor_uid not in [
                i['uid'] for i in instructors_per_section_id[section_id]
        ]:
            instructors_per_section_id[section_id].append({
                'approval':
                next((a for a in approvals
                      if a['approvedBy']['uid'] == instructor_uid), False),
                'deptCode':
                row['instructor_dept_code'],
                'email':
                row['instructor_email'],
                'name':
                row['instructor_name'],
                'roleCode':
                row['instructor_role_code'],
                'uid':
                instructor_uid,
                'wasSentInvite':
                instructor_uid in courses_per_id[section_id]['invitees'],
            })

    api_json = []
    for section_id, course in courses_per_id.items():
        room_id = course.get('room', {}).get('id')

        def _add_and_verify_room(approval_or_scheduled):
            action_room_id = approval_or_scheduled.get('room', {}).get('id')
            is_obsolete_room = not room_id or room_id != action_room_id
            approval_or_scheduled['hasObsoleteRoom'] = is_obsolete_room

        course['instructors'] = instructors_per_section_id[section_id]
        course['hasNecessaryApprovals'] = _has_necessary_approvals(course)
        scheduled = course['scheduled']
        # Check for course changes w.r.t. room, meeting times, and instructors.
        if scheduled:

            def _meeting(obj):
                return f'{obj["meetingDays"]}-{obj["meetingStartTime"]}-{obj["meetingEndTime"]}'

            instructor_uids = set(
                [instructor['uid'] for instructor in course['instructors']])
            scheduled['hasObsoleteInstructors'] = instructor_uids != set(
                scheduled['instructorUids'])
            scheduled['hasObsoleteMeetingTimes'] = _meeting(
                course) != _meeting(scheduled)
            _add_and_verify_room(scheduled)

        for approval in course['approvals']:
            _add_and_verify_room(approval)

        # Add course to the feed
        api_json.append(course)
    return api_json
Example #21
0
def _system_error_email_count():
    return len(SentEmail.get_emails_sent_to('0'))
Example #22
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
Example #23
0
    def send(
        self,
        message,
        recipient,
        subject_line,
        term_id=None,
        section_id=None,
        template_type=None,
    ):
        if not message or not subject_line or not recipient:
            app.logger.error(
                'Attempted to send a message without required fields: '
                f'(recipient={recipient}, subject_line={subject_line}, message={message}'
            )
            return False

        eb_env = get_eb_environment()
        prefix = '' if 'prod' in (eb_env
                                  or '') else f"[{eb_env or 'diablo-local'}] "
        subject_line = f'{prefix}{scrub_email_content(subject_line)}'
        message = scrub_email_content(message)

        @skip_when_pytest()
        def _send():
            smtp = SMTP(self.bcop_smtp_server, port=self.bcop_smtp_port)
            # TLS encryption
            smtp.starttls()
            smtp.set_debuglevel(app.logger.level == logging.DEBUG)
            smtp.login(self.bcop_smtp_username, self.bcop_smtp_password)

            emails_sent_to = set()

            if app.config['DIABLO_ENV'] == 'test':
                write_email_to_log(message=message,
                                   recipient=recipient,
                                   subject_line=subject_line)
            else:
                from_address = f"{app.config['EMAIL_COURSE_CAPTURE_SUPPORT_LABEL']} <{app.config['EMAIL_COURSE_CAPTURE_SUPPORT']}>"

                for email_address in self.get_email_addresses(user=recipient):
                    msg = MIMEMultipart('alternative')
                    msg['From'] = from_address
                    msg['To'] = email_address

                    if app.config['EMAIL_TEST_MODE']:
                        # Append intended recipient email address to verify when testing.
                        intended_email = recipient['email']
                        msg['Subject'] = f'{subject_line} (To: {intended_email})'
                    else:
                        msg['Subject'] = subject_line

                    # TODO: 'plain' text version of email?
                    msg.attach(MIMEText(message, 'plain'))
                    msg.attach(MIMEText(message, 'html'))
                    # Send
                    smtp.sendmail(from_addr=from_address,
                                  to_addrs=email_address,
                                  msg=msg.as_string())

                    emails_sent_to.add(email_address)

            app.logger.info(
                f'\'{template_type}\' email sent to {", ".join(list(emails_sent_to))}'
            )
            # Disconnect
            smtp.quit()

        # Send emails
        _send()

        SentEmail.create(
            recipient_uid=recipient['uid'],
            section_id=section_id,
            template_type=template_type,
            term_id=term_id or app.config['CURRENT_TERM_ID'],
        )
        return True