def app_config(): term_id = app.config['CURRENT_TERM_ID'] return tolerant_jsonify({ 'canvasBaseUrl': app.config['CANVAS_BASE_URL'], 'courseCaptureExplainedUrl': app.config['COURSE_CAPTURE_EXPLAINED_URL'], 'courseCapturePoliciesUrl': app.config['COURSE_CAPTURE_POLICIES_URL'], 'currentTermId': term_id, 'currentTermName': term_name_for_sis_id(term_id), 'devAuthEnabled': app.config['DEVELOPER_AUTH_ENABLED'], 'diabloEnv': app.config['DIABLO_ENV'], 'ebEnvironment': app.config['EB_ENVIRONMENT'] if 'EB_ENVIRONMENT' in app.config else None, 'emailTemplateTypes': EmailTemplate.get_template_type_options(), 'publishTypeOptions': NAMES_PER_PUBLISH_TYPE, 'roomCapabilityOptions': Room.get_room_capability_options(), 'searchFilterOptions': get_search_filter_options(), 'searchItemsPerPage': app.config['SEARCH_ITEMS_PER_PAGE'], 'supportEmailAddress': app.config['EMAIL_DIABLO_SUPPORT'], 'timezone': app.config['TIMEZONE'], })
def approve(): term_id = app.config['CURRENT_TERM_ID'] term_name = term_name_for_sis_id(term_id) params = request.get_json() publish_type = params.get('publishType') recording_type = params.get('recordingType') section_id = params.get('sectionId') course = SisSection.get_course(term_id, section_id) if section_id else None if not course or publish_type not in get_all_publish_types() or recording_type not in get_all_recording_types(): raise BadRequestError('One or more required params are missing or invalid') if not current_user.is_admin and current_user.uid not in [i['uid'] for i in course['instructors']]: raise ForbiddenRequestError('Sorry, request unauthorized') if Approval.get_approval(approved_by_uid=current_user.uid, section_id=section_id, term_id=term_id): raise ForbiddenRequestError(f'You have already approved recording of {course["courseName"]}, {term_name}') meetings = course.get('meetings', {}).get('eligible', []) if len(meetings) != 1: raise BadRequestError('Unique eligible meeting pattern not found for course') meeting = meetings[0] location = meeting and meeting.get('location') room = Room.find_room(location=location) if not room: raise BadRequestError(f'{location} is not eligible for Course Capture.') previous_approvals = Approval.get_approvals_per_section_ids(section_ids=[section_id], term_id=term_id) approval = Approval.create( approved_by_uid=current_user.uid, approver_type_='admin' if current_user.is_admin else 'instructor', course_display_name=course['label'], publish_type_=publish_type, recording_type_=recording_type, room_id=room.id, section_id=section_id, term_id=term_id, ) if previous_approvals: # Compare the current approval with preferences submitted in previous approval previous_approval = previous_approvals[-1] if (approval.publish_type, approval.recording_type) != (previous_approval.publish_type, previous_approval.recording_type): notify_instructors_of_changes(course, approval, previous_approvals) all_approvals = previous_approvals + [approval] if len(course['instructors']) > len(all_approvals): approval_uids = [a.approved_by_uid for a in all_approvals] pending_instructors = [i for i in course['instructors'] if i['uid'] not in approval_uids] last_approver = next((i for i in course['instructors'] if i['uid'] == approval.approved_by_uid), None) if last_approver: notify_instructor_waiting_for_approval(course, last_approver, pending_instructors) return tolerant_jsonify(_after_approval(course=SisSection.get_course(term_id, section_id)))
def _get_substitutions( course=None, instructor_name=None, pending_instructors=None, previous_publish_type_name=None, previous_recording_type_name=None, publish_type_name=None, recipient_name=None, recording_type_name=None, ): term_id = (course and course['termId']) or app.config['CURRENT_TERM_ID'] return { 'course.days': course and course['meetingDays'], 'course.format': course and course['instructionFormat'], 'course.name': course and course['courseName'], 'course.room': course and course['meetingLocation'], 'course.section': course and course['sectionNum'], 'course.time.end': course and course['meetingEndTime'], 'course.time.start': course and course['meetingStartTime'], 'course.title': course and course['courseTitle'], 'instructor.name': instructor_name, 'instructors.pending': pending_instructors and [p['name'] for p in pending_instructors], 'publish.type': publish_type_name, 'publish.type.previous': previous_publish_type_name, 'recording.type': recording_type_name, 'recording.type.previous': previous_recording_type_name, 'signup.url': course and _get_sign_up_url(term_id, course['sectionId']), 'term.name': term_name_for_sis_id(term_id), 'user.name': recipient_name, }
def get_template_substitutions( course, recipient_name, pending_instructors=None, previous_publish_type_name=None, previous_recording_type_name=None, publish_type_name=None, recording_type_name=None, ): term_id = (course and course['termId']) or app.config['CURRENT_TERM_ID'] def _join_names(_dict): if not _dict: return None return ', '.join(i['name'] for i in _dict if i['name']) if course: meetings = course.get('meetings', {}).get('eligible', []) meetings = meetings or course.get('meetings', {}).get('ineligible', []) meeting = meetings and meetings[0] days = meeting and get_names_of_days(meeting['daysFormatted']) else: days = None meeting = None return { 'course.date.end': meeting and meeting['endDate'], 'course.date.start': meeting and meeting['startDate'], 'course.days': days and readable_join(days), 'course.format': course and course['instructionFormat'], 'course.name': course and course['courseName'], 'course.room': meeting and meeting['location'], 'course.section': course and course['sectionNum'], 'course.time.end': meeting and meeting['endTimeFormatted'], 'course.time.start': meeting and meeting['startTimeFormatted'], 'course.title': course and course['courseTitle'], 'instructors.all': course and _join_names(course.get('instructors')), 'instructors.pending': _join_names(pending_instructors), 'instructors.previous': course and course.get('scheduled') and _join_names(course['scheduled'].get('instructors')), 'publish.type': publish_type_name, 'publish.type.previous': previous_publish_type_name, 'recipient.name': recipient_name, 'recording.type': recording_type_name, 'recording.type.previous': previous_recording_type_name, 'signup.url': course and get_sign_up_url(term_id, course['sectionId']), 'term.name': term_name_for_sis_id(term_id), }
def test_series_description(self): """Series description for cross-listed course.""" cross_listed_section_id = 50012 term_id = app.config['CURRENT_TERM_ID'] course = SisSection.get_course( section_id=cross_listed_section_id, term_id=term_id, ) description = get_series_description( course_label=course['label'], instructors=course['instructors'], term_name=term_name_for_sis_id(term_id), ) assert 'MATH C51, LEC 001 | STAT C151, COL 001' in description assert 'Fall 2021' in description assert 'Rudolf Schündler and Arthur Storch' in description assert '2021' in description
def app_config(): def _to_api_key(key): chunks = key.split('_') return f"{chunks[0].lower()}{''.join(chunk.title() for chunk in chunks[1:])}" return tolerant_jsonify( { **dict((_to_api_key(key), app.config[key]) for key in PUBLIC_CONFIGS), **{ 'currentTermName': term_name_for_sis_id(app.config['CURRENT_TERM_ID']), 'ebEnvironment': get_eb_environment(), 'emailTemplateTypes': EmailTemplate.get_template_type_options(), 'publishTypeOptions': NAMES_PER_PUBLISH_TYPE, 'roomCapabilityOptions': Room.get_room_capability_options(), 'searchFilterOptions': get_search_filter_options(), }, }, )
def approve(): term_id = app.config['CURRENT_TERM_ID'] term_name = term_name_for_sis_id(term_id) params = request.get_json() publish_type = params.get('publishType') recording_type = params.get('recordingType') section_id = params.get('sectionId') course = SisSection.get_course(term_id, section_id) if section_id else None if not course or publish_type not in get_all_publish_types() or recording_type not in get_all_recording_types(): raise BadRequestError('One or more required params are missing or invalid') if not current_user.is_admin and current_user.uid not in [i['uid'] for i in course['instructors']]: raise ForbiddenRequestError('Sorry, request unauthorized') if Approval.get_approval(approved_by_uid=current_user.uid, section_id=section_id, term_id=term_id): raise ForbiddenRequestError(f'You have already approved recording of {course["courseName"]}, {term_name}') location = course['meetingLocation'] room = Room.find_room(location=location) if not room: raise BadRequestError(f'{location} is not eligible for Course Capture.') previous_approvals = Approval.get_approvals_per_section_ids(section_ids=[section_id], term_id=term_id) approval = Approval.create( approved_by_uid=current_user.uid, approver_type_='admin' if current_user.is_admin else 'instructor', cross_listed_section_ids=[c['sectionId'] for c in course['crossListings']], publish_type_=publish_type, recording_type_=recording_type, room_id=room.id, section_id=section_id, term_id=term_id, ) _notify_instructors_of_approval( approval=approval, course=course, previous_approvals=previous_approvals, ) return tolerant_jsonify(SisSection.get_course(term_id, section_id))
def _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)
def test_term_name_for_sis_id(self): assert term_name_for_sis_id('2208') == 'Fall 2020' assert term_name_for_sis_id('2212') == 'Spring 2021'