Пример #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 test_has_obsolete_meeting_times(self, client, admin_session):
        """Admins can see meeting time 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_days = 'MOWE'
            assert meeting['days'] != obsolete_meeting_days

            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=obsolete_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_='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 False
            assert course['scheduled']['hasObsoleteTimes'] is True
            assert course['scheduled']['hasObsoleteInstructors'] is False
Пример #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_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
Пример #5
0
 def test_recording_end_date_with_offset(self):
     recordings_end_date = '2020-11-24'
     with override_config(app, 'CURRENT_TERM_RECORDINGS_END', recordings_end_date):
         meeting = {
             'days': 'MO',
             'endDate': '2020-12-11 00:00:00 UTC',
         }
         # The last recording is on a Monday.
         assert get_recording_end_date(meeting) == _to_datetime('2020-11-23')
Пример #6
0
    def test_recording_end_date(self):
        # End date is a Friday
        recordings_end_date = '2525-11-23'
        with override_config(app, 'CURRENT_TERM_RECORDINGS_END', recordings_end_date):
            meeting = {
                'days': 'TUTH',
                'endDate': '2525-12-11 00:00:00 UTC',
            }
            # Expect preceding Thursday
            assert get_recording_end_date(meeting) == _to_datetime('2525-11-22')

            meeting = {
                'days': 'MO',
                # End date is a Thursday
                'endDate': '2525-11-01 00:00:00 UTC',
            }
            # Expect preceding Monday
            assert get_recording_end_date(meeting) == _to_datetime('2525-10-29')
 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
Пример #8
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'
Пример #9
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
Пример #10
0
    def _schedule_recurring_events_in_kaltura(
        self,
        category_ids,
        course_label,
        instructors,
        meeting,
        publish_type,
        recording_type,
        room,
        term_id,
    ):
        # Recording starts X minutes before/after official start; it ends Y minutes before/after official end time.
        days = format_days(meeting['days'])
        start_time = _adjust_time(meeting['startTime'],
                                  app.config['KALTURA_RECORDING_OFFSET_START'])
        end_time = _adjust_time(meeting['endTime'],
                                app.config['KALTURA_RECORDING_OFFSET_END'])

        app.logger.info(f"""
            Prepare to schedule recordings for {course_label}:
                Room: {room.location}
                Instructor UIDs: {[instructor['uid'] for instructor in instructors]}
                Schedule: {days}, {start_time} to {end_time}
                Recording: {recording_type}; {publish_type}
        """)

        term_name = term_name_for_sis_id(term_id)
        recording_start_date = get_recording_start_date(
            meeting, return_today_if_past_start=True)
        recording_end_date = get_recording_end_date(meeting)
        summary = f'{course_label} ({term_name})'
        app.logger.info(f"""
            {course_label} ({term_name}) meets in {room.location},
            between {start_time.strftime('%H:%M')} and {end_time.strftime('%H:%M')}, on {days}.
            Recordings of type {recording_type} will be published to {publish_type}.
        """)

        first_day_start = get_first_matching_datetime_of_term(
            meeting_days=days,
            start_date=recording_start_date,
            time_hours=start_time.hour,
            time_minutes=start_time.minute,
        )
        first_day_end = get_first_matching_datetime_of_term(
            meeting_days=days,
            start_date=recording_start_date,
            time_hours=end_time.hour,
            time_minutes=end_time.minute,
        )
        description = get_series_description(course_label, instructors,
                                             term_name)
        base_entry = self._create_kaltura_base_entry(
            description=description,
            instructors=instructors,
            name=f'{summary} in {room.location}',
        )
        for category_id in category_ids or []:
            self.add_to_kaltura_category(category_id=category_id,
                                         entry_id=base_entry.id)

        until = datetime.combine(
            recording_end_date,
            time(end_time.hour, end_time.minute),
            tzinfo=default_timezone(),
        )
        recurring_event = KalturaRecordScheduleEvent(
            # https://developer.kaltura.com/api-docs/General_Objects/Objects/KalturaScheduleEvent
            classificationType=KalturaScheduleEventClassificationType.
            PUBLIC_EVENT,
            comment=f'{summary} in {room.location}',
            contact=','.join(instructor['uid'] for instructor in instructors),
            description=description,
            duration=(end_time - start_time).seconds,
            endDate=first_day_end.timestamp(),
            organizer=app.config['KALTURA_EVENT_ORGANIZER'],
            ownerId=app.config['KALTURA_KMS_OWNER_ID'],
            partnerId=self.kaltura_partner_id,
            recurrence=KalturaScheduleEventRecurrence(
                # https://developer.kaltura.com/api-docs/General_Objects/Objects/KalturaScheduleEventRecurrence
                byDay=','.join(days),
                frequency=KalturaScheduleEventRecurrenceFrequency.WEEKLY,
                # 'interval' is not documented. When scheduling manually, the value was 1 in each individual event.
                interval=1,
                name=summary,
                timeZone='US/Pacific',
                until=until.timestamp(),
                weekStartDay=days[0],
            ),
            recurrenceType=KalturaScheduleEventRecurrenceType.RECURRING,
            startDate=first_day_start.timestamp(),
            status=KalturaScheduleEventStatus.ACTIVE,
            summary=summary,
            tags=CREATED_BY_DIABLO_TAG,
            templateEntryId=base_entry.id,
        )
        return self.kaltura_client.schedule.scheduleEvent.add(recurring_event)
Пример #11
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