def create_cohort(): params = request.get_json() domain = get_param(params, 'domain', 'default') if is_unauthorized_domain(domain): raise ForbiddenRequestError( f'You are unauthorized to query the \'{domain}\' domain') name = get_param(params, 'name', None) filters = get_param(params, 'filters', None) order_by = params.get('orderBy') # Authorization check filter_keys = list(map(lambda f: f['key'], filters)) if is_unauthorized_search(filter_keys, order_by): raise ForbiddenRequestError( 'You are unauthorized to access student data managed by other departments' ) filter_criteria = _translate_filters_to_cohort_criteria(filters, domain) if not name or not filter_criteria: raise BadRequestError( 'Cohort creation requires \'name\' and \'filters\'') cohort = CohortFilter.create( uid=current_user.get_uid(), name=name, filter_criteria=filter_criteria, domain=domain, order_by=order_by, include_alerts_for_user_id=current_user.get_id(), ) _decorate_cohort(cohort) return tolerant_jsonify(cohort)
def get_cohort(cohort_id): benchmark = get_benchmarker(f'cohort {cohort_id} get_cohort') benchmark('begin') filter_keys = list(request.args.keys()) order_by = get_param(request.args, 'orderBy', None) if is_unauthorized_search(filter_keys, order_by): raise ForbiddenRequestError( 'You are unauthorized to access student data managed by other departments' ) include_students = to_bool(get_param(request.args, 'includeStudents')) include_students = True if include_students is None else include_students offset = get_param(request.args, 'offset', 0) limit = get_param(request.args, 'limit', 50) benchmark('begin cohort filter query') cohort = CohortFilter.find_by_id( int(cohort_id), order_by=order_by, offset=int(offset), limit=int(limit), include_alerts_for_user_id=current_user.get_id(), include_profiles=True, include_students=include_students, ) if cohort and _can_current_user_view_cohort(cohort): _decorate_cohort(cohort) benchmark('end') return tolerant_jsonify(cohort) else: raise ResourceNotFoundError( f'No cohort found with identifier: {cohort_id}')
def get_cohort_per_filters(): benchmark = get_benchmarker('cohort get_students_per_filters') benchmark('begin') params = request.get_json() filters = get_param(params, 'filters', []) if not filters: raise BadRequestError('API requires \'filters\'') include_students = to_bool(get_param(params, 'includeStudents')) include_students = True if include_students is None else include_students order_by = get_param(params, 'orderBy', None) offset = get_param(params, 'offset', 0) limit = get_param(params, 'limit', 50) filter_keys = list(map(lambda f: f['key'], filters)) if is_unauthorized_search(filter_keys, order_by): raise ForbiddenRequestError( 'You are unauthorized to access student data managed by other departments' ) benchmark('begin phantom cohort query') cohort = _construct_phantom_cohort( filters=filters, order_by=order_by, offset=int(offset), limit=int(limit), include_alerts_for_user_id=current_user.get_id(), include_profiles=True, include_students=include_students, ) _decorate_cohort(cohort) benchmark('end') return tolerant_jsonify(cohort)
def download_csv_per_filters(): benchmark = get_benchmarker('cohort download_csv_per_filters') benchmark('begin') params = request.get_json() filters = get_param(params, 'filters', []) fieldnames = get_param(params, 'csvColumnsSelected', []) domain = get_param(params, 'domain', 'default') if (domain == 'default' and not filters) or filters is None: raise BadRequestError('API requires \'filters\'') filter_keys = list(map(lambda f: f['key'], filters)) if is_unauthorized_search(filter_keys): raise ForbiddenRequestError( 'You are unauthorized to access student data managed by other departments' ) domain = get_param(params, 'domain', 'default') if is_unauthorized_domain(domain): raise ForbiddenRequestError( f'You are unauthorized to query the \'{domain}\' domain') cohort = _construct_phantom_cohort( domain=domain, filters=filters, offset=0, limit=None, include_profiles=False, include_sids=True, include_students=False, ) return _response_with_csv_download(benchmark, domain, fieldnames, cohort['sids'])
def get_curated_group(curated_group_id): offset = get_param(request.args, 'offset', 0) limit = get_param(request.args, 'limit', 50) order_by = get_param(request.args, 'orderBy', 'last_name') curated_group = _curated_group_with_complete_student_profiles( curated_group_id, order_by, int(offset), int(limit)) return tolerant_jsonify(curated_group)
def students_with_alerts(cohort_id): benchmark = get_benchmarker(f'cohort {cohort_id} students_with_alerts') benchmark('begin') offset = get_param(request.args, 'offset', 0) limit = get_param(request.args, 'limit', 50) cohort = CohortFilter.find_by_id( cohort_id, include_alerts_for_user_id=current_user.get_id(), include_students=False, alert_offset=offset, alert_limit=limit, ) benchmark('fetched cohort') if cohort and _can_current_user_view_cohort(cohort): _decorate_cohort(cohort) students = cohort.get('alerts', []) alert_sids = [s['sid'] for s in students] alert_profiles = get_summary_student_profiles(alert_sids) benchmark('fetched student profiles') alert_profiles_by_sid = {p['sid']: p for p in alert_profiles} for student in students: student.update(alert_profiles_by_sid[student['sid']]) # The enrolled units count is the one piece of term data we want to preserve. if student.get('term'): student['term'] = { 'enrolledUnits': student['term'].get('enrolledUnits') } else: raise ResourceNotFoundError( f'No cohort found with identifier: {cohort_id}') benchmark('end') return tolerant_jsonify(students)
def get_students_with_alerts(curated_group_id): offset = get_param(request.args, 'offset', 0) limit = get_param(request.args, 'limit', 50) benchmark = get_benchmarker( f'curated group {curated_group_id} students_with_alerts') benchmark('begin') curated_group = CuratedGroup.find_by_id(curated_group_id) if not curated_group: raise ResourceNotFoundError( f'Sorry, no curated group found with id {curated_group_id}.') if not _can_current_user_view_curated_group(curated_group): raise ForbiddenRequestError( f'Current user, {current_user.get_uid()}, cannot view curated group {curated_group.id}' ) students = Alert.include_alert_counts_for_students( benchmark=benchmark, viewer_user_id=current_user.get_id(), group={'sids': CuratedGroup.get_all_sids(curated_group_id)}, count_only=True, offset=offset, limit=limit, ) alert_count_per_sid = {} for s in list(filter(lambda s: s.get('alertCount') > 0, students)): sid = s.get('sid') alert_count_per_sid[sid] = s.get('alertCount') sids = list(alert_count_per_sid.keys()) benchmark('begin profile query') students_with_alerts = get_student_profile_summaries(sids=sids) benchmark('end profile query') for student in students_with_alerts: student['alertCount'] = alert_count_per_sid[student['sid']] benchmark('end') return tolerant_jsonify(students_with_alerts)
def batch_degree_checks(): params = request.get_json() sids = get_param(params, 'sids') template_id = get_param(params, 'templateId') if not template_id or not sids: raise BadRequestError('sids and templateId are required.') return tolerant_jsonify( create_batch_degree_checks(template_id=template_id, sids=sids))
def assign_course(course_id): params = request.get_json() course = DegreeProgressCourse.find_by_id(course_id) if course: # Get existing category assignment. If it's a placeholder then delete it at end of transaction. previous_category = DegreeProgressCategory.find_by_id( course.category_id) if course.category_id else None category_id = get_param(params, 'categoryId') category = DegreeProgressCategory.find_by_id( category_id) if category_id else None if category: if category.template_id != course.degree_check_id: raise BadRequestError( 'The category and course do not belong to the same degree_check instance.' ) children = DegreeProgressCategory.find_by_parent_category_id( parent_category_id=category_id) if next((c for c in children if c.category_type == 'Subcategory'), None): raise BadRequestError( 'A course cannot be assigned to a category with a subcategory.' ) course = DegreeProgressCourse.assign(category_id=category_id, course_id=course.id) else: # When user un-assigns a course we delete all copies of that course,. for copy_of_course in DegreeProgressCourse.get_courses( degree_check_id=course.degree_check_id, manually_created_at=course.manually_created_at, manually_created_by=course.manually_created_by, section_id=course.section_id, sid=course.sid, term_id=course.term_id, ): if copy_of_course.id != course.id: DegreeProgressCourseUnitRequirement.delete( copy_of_course.id) category = DegreeProgressCategory.find_by_id( copy_of_course.category_id) if category and 'Placeholder' in category.category_type: DegreeProgressCategory.delete(category.id) DegreeProgressCourse.delete(copy_of_course) ignore = to_bool_or_none(get_param(params, 'ignore')) course = DegreeProgressCourse.unassign(course_id=course.id, ignore=ignore) # If previous assignment was a "placeholder" category then delete it. if previous_category and 'Placeholder' in previous_category.category_type: DegreeProgressCategory.delete(previous_category.id) # Update updated_at date of top-level record DegreeProgressTemplate.refresh_updated_at(course.degree_check_id, current_user.get_id()) return tolerant_jsonify(course.to_api_json()) else: raise ResourceNotFoundError('Course not found.')
def get_all_admits(): limit = get_param(request.args, 'limit', 50) offset = get_param(request.args, 'offset', 0) order_by = get_param(request.args, 'orderBy', None) admits = query_admitted_students( limit=int(limit), offset=int(offset), order_by=order_by, ) return tolerant_jsonify(admits)
def translate_cohort_filter_to_menu(cohort_owner_uid): params = request.get_json() domain = get_param(params, 'domain', 'default') if is_unauthorized_domain(domain): raise ForbiddenRequestError( f'You are unauthorized to query the \'{domain}\' domain') if cohort_owner_uid == 'me': cohort_owner_uid = current_user.get_uid() criteria = get_param(params, 'criteria') return tolerant_jsonify( CohortFilterOptions.translate_to_filter_options( cohort_owner_uid, domain, criteria))
def distinct_student_count(): params = request.get_json() sids = get_param(params, 'sids', None) cohort_ids = get_param(params, 'cohortIds', None) curated_group_ids = get_param(params, 'curatedGroupIds', None) if cohort_ids or curated_group_ids: count = len(get_batch_distinct_sids(sids, cohort_ids, curated_group_ids)) else: count = len(sids) return tolerant_jsonify({ 'count': count, })
def get_curated_group(curated_group_id): offset = get_param(request.args, 'offset', 0) limit = get_param(request.args, 'limit', 50) order_by = get_param(request.args, 'orderBy', 'last_name') term_id = get_param(request.args, 'termId', None) curated_group = _curated_group_with_complete_student_profiles( curated_group_id=curated_group_id, limit=int(limit), offset=int(offset), order_by=order_by, term_id=term_id, ) return tolerant_jsonify(curated_group)
def create_category(): params = request.get_json() category_type = get_param(params, 'categoryType') course_units_lower = get_param(params, 'unitsLower') course_units_upper = get_param(params, 'unitsUpper') description = get_param(params, 'description') name = get_param(params, 'name') parent_category_id = get_param(params, 'parentCategoryId') position = get_param(params, 'position') template_id = get_param(params, 'templateId') # Categories are mapped to degree_progress_unit_requirements value = get_param(request.get_json(), 'unitRequirementIds') unit_requirement_ids = list(filter(None, value.split(','))) if isinstance( value, str) else value if not category_type or not name or not _is_valid_position( position) or not template_id: raise BadRequestError( "Insufficient data: categoryType, name, position and templateId are required.'" ) if category_type in ['Category', 'Campus Requirements' ] and parent_category_id: raise BadRequestError(f'{category_type} cannot have a parent.') if category_type in ('Course Requirement', 'Subcategory') and not parent_category_id: raise BadRequestError( f"The parentCategoryId param is required when categoryType equals '{category_type}'." ) if parent_category_id: parent = _get_degree_category(parent_category_id) parent_type = parent.category_type if parent_type == 'Course Requirement' or ( parent_type == 'Subcategory' and category_type == 'Category'): raise BadRequestError( f'Type {category_type} not allowed, based on parent type: {parent_type}.' ) if position != parent.position: raise BadRequestError( f'Category position ({position}) must match its parent ({parent.position}).' ) category = _create_category( category_type=category_type, course_units_lower=course_units_lower, course_units_upper=course_units_upper, description=description, name=name, parent_category_id=parent_category_id, position=position, template_id=template_id, unit_requirement_ids=unit_requirement_ids, ) # Update updated_at date of top-level record DegreeProgressTemplate.refresh_updated_at(template_id, current_user.get_id()) return tolerant_jsonify(category.to_api_json())
def create_course(): params = request.get_json() accent_color = normalize_accent_color(get_param(params, 'accentColor')) degree_check_id = get_param(params, 'degreeCheckId') grade = get_param(params, 'grade') name = get_param(params, 'name') note = get_param(params, 'note') sid = get_param(params, 'sid') value = get_param(request.get_json(), 'unitRequirementIds') unit_requirement_ids = list(filter(None, value.split(','))) if isinstance( value, str) else value units = get_param(params, 'units') if 0 in map(lambda v: len(str(v).strip()) if v else 0, (name, sid)): raise BadRequestError('Missing one or more required parameters') course = DegreeProgressCourse.create( accent_color=accent_color, degree_check_id=degree_check_id, display_name=name, grade=grade, manually_created_at=datetime.now(), manually_created_by=current_user.get_id(), note=note, section_id=None, sid=sid, term_id=None, unit_requirement_ids=unit_requirement_ids or (), units=units, ) # Update updated_at date of top-level record DegreeProgressTemplate.refresh_updated_at(course.degree_check_id, current_user.get_id()) return tolerant_jsonify(course.to_api_json())
def all_cohort_filter_options(cohort_owner_uid): if cohort_owner_uid == 'me': cohort_owner_uid = current_user.get_uid() params = request.get_json() existing_filters = get_param(params, 'existingFilters', []) domain = get_param(params, 'domain', 'default') if is_unauthorized_domain(domain): raise ForbiddenRequestError( f'You are unauthorized to query the \'{domain}\' domain') return tolerant_jsonify( CohortFilterOptions.get_cohort_filter_option_groups( cohort_owner_uid, domain, existing_filters, ), )
def toggle_campus_requirement(category_id): params = request.get_json() is_satisfied = get_param(params, 'isSatisfied') if is_satisfied is None: raise BadRequestError('Parameter \'isSatisfied\' is required') category = _get_degree_category(category_id) if category.category_type not in [ 'Campus Requirement, Satisfied', 'Campus Requirement, Unsatisfied' ]: raise BadRequestError('Category must be a \'Campus Requirement\' type') if ((category.category_type == 'Campus Requirement, Satisfied' and is_satisfied is True) or (category.category_type == 'Campus Requirement, Unsatisfied' and is_satisfied is False)): app.logger.info( f'Request ignored: set is_satisfied={is_satisfied} on {category.category_type}' ) else: category = DegreeProgressCategory.set_campus_requirement_satisfied( category_id=category_id, is_satisfied=is_satisfied, ) # Update updated_at date of top-level record DegreeProgressTemplate.refresh_updated_at(category.template_id, current_user.get_id()) return tolerant_jsonify(category.to_api_json())
def create_degree_check(sid): params = request.get_json() template_id = get_param(params, 'templateId') if not template_id or not is_int(sid): raise BadRequestError('sid and templateId are required.') return tolerant_jsonify( clone_degree_template(template_id=template_id, sid=sid).to_api_json())
def create_cohort(): params = request.get_json() name = get_param(params, 'name', None) filters = get_param(params, 'filters', None) order_by = params.get('orderBy') filter_criteria = _filters_to_filter_criteria(filters, order_by) if not name or not filter_criteria: raise BadRequestError( 'Cohort creation requires \'name\' and \'filters\'') cohort = CohortFilter.create( uid=current_user.get_uid(), name=name, filter_criteria=filter_criteria, order_by=order_by, include_alerts_for_user_id=current_user.get_id(), ) _decorate_cohort(cohort) return tolerant_jsonify(cohort)
def create_degree(): params = request.get_json() name = get_param(params, 'name', None) validate_template_upsert(name=name) degree = DegreeProgressTemplate.create( advisor_dept_codes=dept_codes_where_advising(current_user), created_by=current_user.get_id(), degree_name=name, ) return tolerant_jsonify(degree.to_api_json())
def my_cohorts(): domain = get_param(request.args, 'domain', 'default') if is_unauthorized_domain(domain): raise ForbiddenRequestError( f'You are unauthorized to query the \'{domain}\' domain') cohorts = [] for cohort in CohortFilter.get_cohorts_of_user_id(current_user.get_id(), domain=domain): cohort['isOwnedByCurrentUser'] = True cohorts.append(cohort) return tolerant_jsonify(cohorts)
def download_cohort_csv(): benchmark = get_benchmarker('cohort download_csv') benchmark('begin') params = request.get_json() cohort_id = int(get_param(params, 'cohortId')) cohort = CohortFilter.find_by_id( cohort_id, offset=0, limit=None, include_profiles=False, include_sids=True, include_students=False, ) if cohort and _can_current_user_view_cohort(cohort): fieldnames = get_param(params, 'csvColumnsSelected', []) sids = CohortFilter.get_sids(cohort['id']) return _response_with_csv_download(benchmark, cohort['domain'], fieldnames, sids) else: raise ResourceNotFoundError( f'No cohort found with identifier: {cohort_id}')
def update_degree_note(degree_check_id): params = request.get_json() body = get_param(params, 'body') note = DegreeProgressNote.upsert( body=body, template_id=degree_check_id, updated_by=current_user.get_id(), ) # Update updated_at date of top-level record DegreeProgressTemplate.refresh_updated_at(degree_check_id, current_user.get_id()) return tolerant_jsonify(note.to_api_json())
def recommend_category(category_id): params = request.get_json() is_recommended = get_param(params, 'isRecommended') if is_recommended is None: raise BadRequestError('Parameter \'isRecommended\' is required') accent_color = normalize_accent_color(get_param(params, 'accentColor')) grade = get_param(params, 'grade') note = get_param(params, 'note') units_lower = get_param(params, 'unitsLower') units_upper = get_param(params, 'unitsUpper') category = DegreeProgressCategory.recommend( accent_color=accent_color, category_id=category_id, course_units_lower=units_lower, course_units_upper=units_upper, grade=grade, is_recommended=is_recommended, note=(note or '').strip() or None, ) # Update updated_at date of top-level record DegreeProgressTemplate.refresh_updated_at(category.template_id, current_user.get_id()) return tolerant_jsonify(category.to_api_json())
def update_category(category_id): params = request.get_json() units_lower = get_param(params, 'unitsLower') units_upper = get_param(params, 'unitsUpper') description = get_param(params, 'description') name = get_param(params, 'name') parent_category_id = get_param(params, 'parentCategoryId') # Courses can be mapped to degree_progress_unit_requirements value = get_param(request.get_json(), 'unitRequirementIds') unit_requirement_ids = list(filter(None, value.split(','))) if isinstance( value, str) else value category = DegreeProgressCategory.update( category_id=category_id, course_units_lower=units_lower, course_units_upper=units_upper, description=description, parent_category_id=parent_category_id, name=name, unit_requirement_ids=unit_requirement_ids, ) # Update updated_at date of top-level record DegreeProgressTemplate.refresh_updated_at(category.template_id, current_user.get_id()) return tolerant_jsonify(category.to_api_json())
def update_course(course_id): course = DegreeProgressCourse.find_by_id(course_id) if course: params = request.get_json() accent_color = normalize_accent_color(get_param(params, 'accentColor')) grade = get_param(params, 'grade') grade = course.grade if grade is None else grade name = get_param(params, 'name') name = course.display_name if name is None else name if name is None: raise BadRequestError('name is required.') note = get_param(params, 'note') # Courses are mapped to degree_progress_unit_requirements value = get_param(request.get_json(), 'unitRequirementIds') unit_requirement_ids = list(filter( None, value.split(','))) if isinstance(value, str) else value units = get_param(params, 'units') or None course = DegreeProgressCourse.update( accent_color=accent_color, course_id=course_id, grade=grade, name=name, note=note, unit_requirement_ids=unit_requirement_ids, units=units, ) # Update updated_at date of top-level record DegreeProgressTemplate.refresh_updated_at(course.degree_check_id, current_user.get_id()) return tolerant_jsonify(course.to_api_json()) else: raise ResourceNotFoundError('Course not found.')
def get_cohort_events(cohort_id): cohort = CohortFilter.find_by_id(cohort_id, include_students=False) if not cohort or not _can_current_user_view_cohort(cohort): raise ResourceNotFoundError( f'No cohort found with identifier: {cohort_id}') if cohort['domain'] != 'default': raise BadRequestError( f"Cohort events are not supported in domain {cohort['domain']}") offset = get_param(request.args, 'offset', 0) limit = get_param(request.args, 'limit', 50) results = CohortFilterEvent.events_for_cohort(cohort_id, offset, limit) count = results['count'] events = results['events'] event_sids = [e.sid for e in events] event_profiles_by_sid = { e['sid']: e for e in get_summary_student_profiles(event_sids, include_historical=True) } def _event_feed(event): profile = event_profiles_by_sid.get(event.sid, {}) return { 'createdAt': event.created_at.isoformat(), 'eventType': event.event_type, 'firstName': profile.get('firstName'), 'lastName': profile.get('lastName'), 'sid': event.sid, 'uid': profile.get('uid'), } feed = { 'count': count, 'events': [_event_feed(e) for e in events], } return tolerant_jsonify(feed)
def download_csv_per_filters(): benchmark = get_benchmarker('cohort download_csv_per_filters') benchmark('begin') params = request.get_json() filters = get_param(params, 'filters', []) fieldnames = get_param(params, 'csvColumnsSelected', []) if not filters: raise BadRequestError('API requires \'filters\'') filter_keys = list(map(lambda f: f['key'], filters)) if is_unauthorized_search(filter_keys): raise ForbiddenRequestError( 'You are unauthorized to access student data managed by other departments' ) cohort = _construct_phantom_cohort( filters=filters, offset=0, limit=None, include_profiles=False, include_sids=True, include_students=False, ) return response_with_students_csv_download(sids=cohort['sids'], fieldnames=fieldnames, benchmark=benchmark)
def create_curated_group(): params = request.get_json() domain = get_param(params, 'domain', 'default') if is_unauthorized_domain(domain): raise ForbiddenRequestError( f'You are unauthorized to use the \'{domain}\' domain') name = params.get('name', None) if not name: raise BadRequestError('Curated group creation requires \'name\'') curated_group = CuratedGroup.create(domain=domain, name=name, owner_id=current_user.get_id()) # Optionally, add students if 'sids' in params: sids = [sid for sid in set(params.get('sids')) if sid.isdigit()] CuratedGroup.add_students(curated_group_id=curated_group.id, sids=sids) return tolerant_jsonify(curated_group.to_api_json(include_students=True))
def download_csv(curated_group_id): benchmark = get_benchmarker( f'curated group {curated_group_id} download_csv') benchmark('begin') curated_group = CuratedGroup.find_by_id(curated_group_id) params = request.get_json() fieldnames = get_param(params, 'csvColumnsSelected', []) if not curated_group: raise ResourceNotFoundError( f'No curated group found with id: {curated_group_id}') if not _can_current_user_view_curated_group(curated_group): raise ForbiddenRequestError( f'Current user, {current_user.get_uid()}, cannot view curated group {curated_group.id}' ) return response_with_students_csv_download( sids=CuratedGroup.get_all_sids(curated_group_id), fieldnames=fieldnames, benchmark=benchmark)