def _curated_group_with_complete_student_profiles(curated_group_id, order_by='last_name', term_id=None, offset=0, limit=50): benchmark = get_benchmarker( f'curated group {curated_group_id} with student profiles') 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}' ) api_json = curated_group.to_api_json(order_by=order_by, offset=offset, limit=limit) sids = [s['sid'] for s in api_json['students']] benchmark('begin profile query') api_json['students'] = get_summary_student_profiles( sids, term_id=term_id, include_historical=True) benchmark('begin alerts query') Alert.include_alert_counts_for_students( viewer_user_id=current_user.get_id(), group=api_json) benchmark('end') benchmark('begin get_referencing_cohort_ids') api_json[ 'referencingCohortIds'] = curated_group.get_referencing_cohort_ids() benchmark('end') return api_json
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 appointment_check_in(appointment_id): appointment = Appointment.find_by_id(appointment_id) if not appointment: raise ResourceNotFoundError('Unknown path') if appointment.dept_code in _dept_codes_with_scheduler_privilege(): params = request.get_json() advisor_uid = params.get('advisorUid', None) if not advisor_uid: raise BadRequestError( 'Appointment check-in requires \'advisor_uid\'') appointment = Appointment.check_in( appointment_id=appointment_id, checked_in_by=current_user.get_id(), advisor_dept_codes=params.get('advisorDeptCodes', None), advisor_name=params.get('advisorName', None), advisor_role=params.get('advisorRole', None), advisor_uid=advisor_uid, ) api_json = appointment.to_api_json(current_user.get_id()) _put_student_profile_per_appointment([api_json]) return tolerant_jsonify(api_json) else: raise ForbiddenRequestError( f'You are unauthorized to manage {appointment.dept_code} appointments.' )
def update_note_template(): params = request.form note_template_id = params.get('id', None) subject = params.get('subject', None) if not subject: raise BadRequestError('Requires \'subject\'') body = params.get('body', None) topics = get_note_topics_from_http_post() delete_ids_ = params.get('deleteAttachmentIds') or [] delete_ids_ = delete_ids_ if isinstance( delete_ids_, list) else str(delete_ids_).split(',') delete_attachment_ids = [int(id_) for id_ in delete_ids_] note_template = NoteTemplate.find_by_id(note_template_id=note_template_id) if not note_template: raise ResourceNotFoundError('Template not found') if note_template.creator_id != current_user.get_id(): raise ForbiddenRequestError('Template not available.') note_template = NoteTemplate.update( attachments=get_note_attachments_from_http_post(tolerate_none=True), body=process_input_from_rich_text_editor(body), delete_attachment_ids=delete_attachment_ids, is_private=to_bool_or_none(params.get('isPrivate', False)), note_template_id=note_template_id, subject=subject, topics=topics, ) return tolerant_jsonify(note_template.to_api_json())
def get_cohort(cohort_id): if is_unauthorized_search(request.args): raise ForbiddenRequestError( 'You are unauthorized to access student data managed by other departments' ) include_students = util.to_bool_or_none( util.get(request.args, 'includeStudents')) include_students = True if include_students is None else include_students order_by = util.get(request.args, 'orderBy', None) offset = util.get(request.args, 'offset', 0) limit = util.get(request.args, 'limit', 50) cohort = CohortFilter.find_by_id(int(cohort_id)) if cohort and can_view_cohort(current_user, cohort): cohort = decorate_cohort( cohort, order_by=order_by, offset=int(offset), limit=int(limit), include_alerts_for_uid=current_user.uid, include_profiles=True, include_students=include_students, ) return tolerant_jsonify(cohort) else: raise ResourceNotFoundError( f'No cohort found with identifier: {cohort_id}')
def _dev_auth_login(uid, password): if app.config['DEVELOPER_AUTH_ENABLED']: logger = app.logger if password != app.config['DEVELOPER_AUTH_PASSWORD']: logger.error('Dev-auth: Wrong password') return tolerant_jsonify({'message': 'Invalid credentials'}, 401) user_id = AuthorizedUser.get_id_per_uid(uid) user = user_id and app.login_manager.user_callback(user_id=user_id, flush_cached=True) if user is None: logger.error( f'Dev-auth: User with UID {uid} is not registered in BOA.') return tolerant_jsonify( { 'message': f'Sorry, user with UID {uid} is not registered to use BOA.' }, 403) if not user.is_active: logger.error( f'Dev-auth: UID {uid} is registered with BOA but not active.') return tolerant_jsonify( { 'message': f'Sorry, user with UID {uid} is not authorized to use BOA.' }, 403) logger.info(f'Dev-auth used to log in as UID {uid}') login_user(user, force=True, remember=True) return tolerant_jsonify(current_user.to_api_json()) else: raise ResourceNotFoundError('Unknown path')
def get_appointment(appointment_id): appointment = Appointment.find_by_id(appointment_id) if not appointment: raise ResourceNotFoundError('Unknown path') api_json = appointment.to_api_json(current_user.get_id()) _put_student_profile_per_appointment([api_json]) return tolerant_jsonify(api_json)
def get_degree_checks(uid): sid = get_sid_by_uid(uid) if sid: return tolerant_jsonify( DegreeProgressTemplate.find_by_sid(student_sid=sid)) else: raise ResourceNotFoundError('Student not found')
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 update_note(): params = request.form body = params.get('body', None) is_private = to_bool_or_none(params.get('isPrivate', False)) note_id = params.get('id', None) subject = params.get('subject', None) topics = get_note_topics_from_http_post() note = Note.find_by_id(note_id=note_id) if note_id else None if not note: raise ResourceNotFoundError('Note not found') if not subject: raise BadRequestError('Note subject is required') if note.author_uid != current_user.get_uid(): raise ForbiddenRequestError( 'Sorry, you are not the author of this note.') if (is_private is not note.is_private ) and not current_user.can_access_private_notes: raise ForbiddenRequestError( 'Sorry, you are not authorized to manage note privacy') note = Note.update( body=process_input_from_rich_text_editor(body), is_private=is_private, note_id=note_id, subject=subject, topics=topics, ) note_read = NoteRead.find_or_create(current_user.get_id(), note_id) return tolerant_jsonify( _boa_note_to_compatible_json(note=note, note_read=note_read))
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_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 update_appointment(appointment_id): appointment = Appointment.find_by_id(appointment_id) if not appointment: raise ResourceNotFoundError('Unknown path') has_privilege = current_user.is_admin or appointment.dept_code in _dept_codes_with_scheduler_privilege() if not has_privilege: raise ForbiddenRequestError(f'You are unauthorized to manage {appointment.dept_code} appointments.') params = request.get_json() details = params.get('details', None) scheduled_time = params.get('scheduledTime', None) if scheduled_time: scheduled_time = localized_timestamp_to_utc(scheduled_time) student_contact_info = params.get('studentContactInfo', None) student_contact_type = params.get('studentContactType', None) topics = params.get('topics', None) appointment.update( details=process_input_from_rich_text_editor(details), scheduled_time=scheduled_time, student_contact_info=student_contact_info, student_contact_type=student_contact_type, topics=topics, updated_by=current_user.get_id(), ) api_json = appointment.to_api_json(current_user.get_id()) _put_student_profile_per_appointment([api_json]) return tolerant_jsonify(api_json)
def get_waitlist(dept_code): def _is_current_user_authorized(): return current_user.is_admin or dept_code in _dept_codes_with_scheduler_privilege() dept_code = dept_code.upper() if dept_code not in BERKELEY_DEPT_CODE_TO_NAME: raise ResourceNotFoundError(f'Unrecognized department code: {dept_code}') elif _is_current_user_authorized(): show_all_statuses = current_user.is_drop_in_advisor or current_user.is_admin statuses = appointment_event_type.enums if show_all_statuses else ['reserved', 'waiting'] unresolved = [] resolved = [] for appointment in Appointment.get_drop_in_waitlist(dept_code, statuses): a = appointment.to_api_json(current_user.get_id()) if a['status'] in ['reserved', 'waiting']: unresolved.append(a) else: resolved.append(a) _put_student_profile_per_appointment(unresolved) _put_student_profile_per_appointment(resolved) return tolerant_jsonify({ 'advisors': drop_in_advisors_for_dept_code(dept_code), 'waitlist': { 'unresolved': unresolved, 'resolved': resolved, }, }) else: raise ForbiddenRequestError(f'You are unauthorized to manage {dept_code} appointments.')
def get_note_template(note_template_id): note_template = NoteTemplate.find_by_id(note_template_id=note_template_id) if not note_template: raise ResourceNotFoundError('Template not found') if note_template.creator_id != current_user.get_id(): raise ForbiddenRequestError('Template not available') return tolerant_jsonify(note_template.to_api_json())
def _curated_group_with_complete_student_profiles(curated_group_id, order_by='last_name', offset=0, limit=50): benchmark = get_benchmarker( f'curated group {curated_group_id} with student profiles') 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 curated_group.owner_id != current_user.get_id(): raise ForbiddenRequestError( f'Current user, {current_user.get_uid()}, does not own curated group {curated_group.id}' ) api_json = curated_group.to_api_json(order_by=order_by, offset=offset, limit=limit) sids = [s['sid'] for s in api_json['students']] benchmark('begin profile query') api_json['students'] = get_summary_student_profiles(sids) benchmark('begin alerts query') Alert.include_alert_counts_for_students( viewer_user_id=current_user.get_id(), group=api_json) benchmark('end') return api_json
def create_appointment(): params = request.get_json() dept_code = params.get('deptCode', None) sid = params.get('sid', None) advisor_uid = params.get('advisorUid', None) appointment_type = params.get('appointmentType', None) topics = params.get('topics', None) if not dept_code or not sid or not appointment_type or not len(topics): raise BadRequestError( 'Appointment creation: required parameters were not provided') dept_code = dept_code.upper() if dept_code not in BERKELEY_DEPT_CODE_TO_NAME: raise ResourceNotFoundError( f'Unrecognized department code: {dept_code}') if dept_code not in _dept_codes_with_scheduler_privilege(): raise ForbiddenRequestError( f'You are unauthorized to manage {dept_code} appointments.') appointment = Appointment.create( advisor_uid=advisor_uid, appointment_type=appointment_type, created_by=current_user.get_id(), dept_code=dept_code, details=params.get('details', None), student_sid=sid, topics=topics, ) AppointmentRead.find_or_create(current_user.get_id(), appointment.id) api_json = appointment.to_api_json(current_user.get_id()) _put_student_profile_per_appointment([api_json]) return tolerant_jsonify(api_json)
def get_note(note_id): note = Note.find_by_id(note_id=note_id) if not note: raise ResourceNotFoundError('Note not found') note_read = NoteRead.when_user_read_note(current_user.get_id(), str(note.id)) return tolerant_jsonify( _boa_note_to_compatible_json(note=note, note_read=note_read))
def delete_note(note_id): if not current_user.is_admin: raise ForbiddenRequestError('Sorry, you are not authorized to delete notes.') note = Note.find_by_id(note_id=note_id) if not note: raise ResourceNotFoundError('Note not found') Note.delete(note_id=note_id) return tolerant_jsonify({'message': f'Note {note_id} deleted'}), 200
def is_unauthorized_domain(domain): if domain not in ['default', 'admitted_students']: raise BadRequestError(f'Invalid domain: {domain}') elif domain == 'admitted_students' and not app.config[ 'FEATURE_FLAG_ADMITTED_STUDENTS']: raise ResourceNotFoundError('Unknown path') return domain == 'admitted_students' and not current_user.is_admin and 'ZCEEE' not in dept_codes_where_advising( current_user)
def user_analytics(uid): feed = get_student_and_terms(uid) if not feed: raise ResourceNotFoundError('Unknown student') # CalCentral's Student Overview page is advisors' official information source for the student. feed[ 'studentProfileLink'] = f'https://calcentral.berkeley.edu/user/overview/{uid}' return tolerant_jsonify(feed)
def become(): if app.config['DEVELOPER_AUTH_ENABLED']: params = request.get_json() or {} logout_user() _dev_login(params.get('uid'), app.config['DEVELOPER_AUTH_PASSWORD']) return redirect('/') else: raise ResourceNotFoundError('Unknown path')
def get_student_by_uid(uid): profile_only = to_bool_or_none(request.args.get('profileOnly')) student = get_student_and_terms_by_uid(uid) if not student: raise ResourceNotFoundError('Unknown student') if not profile_only: put_notifications(student) _put_degree_checks_json(student) return tolerant_jsonify(student)
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 delete_note_template(note_template_id): note_template = NoteTemplate.find_by_id(note_template_id=note_template_id) if not note_template: raise ResourceNotFoundError('Template not found') if note_template.creator_id != current_user.get_id(): raise ForbiddenRequestError('Template not available') NoteTemplate.delete(note_template_id=note_template_id) return tolerant_jsonify( {'message': f'Note template {note_template_id} deleted'}), 200
def clone_degree_template(template_id, name=None, sid=None): template = DegreeProgressTemplate.find_by_id(template_id) if template_id and not template: raise ResourceNotFoundError( f'No template found with id={template_id}.') if name: validate_template_upsert(name=name, template_id=template_id) created_by = current_user.get_id() return clone(template, created_by, name=name, sid=sid)
def delete_unit_requirement(unit_requirement_id): unit_requirement = DegreeProgressUnitRequirement.find_by_id(unit_requirement_id) if unit_requirement: DegreeProgressUnitRequirement.delete(unit_requirement_id) # Update updated_at date of top-level record DegreeProgressTemplate.refresh_updated_at(unit_requirement.template_id, current_user.get_id()) return tolerant_jsonify({'message': f'Unit requirement {unit_requirement_id} deleted'}), 200 else: raise ResourceNotFoundError(f'No unit_requirement found with id={unit_requirement_id}.')
def unreserve_appointment(appointment_id): appointment = Appointment.find_by_id(appointment_id) if not appointment: raise ResourceNotFoundError('Unknown path') has_privilege = current_user.is_admin or appointment.dept_code in _dept_codes_with_scheduler_privilege() if not has_privilege: raise ForbiddenRequestError(f'You are unauthorized to manage appointment {appointment_id}.') if appointment.status != 'reserved': raise BadRequestError(appointment.to_api_json(current_user.get_id())) _set_appointment_to_waiting(appointment) return Response(status=200)
def remove_student_from_curated_group(curated_group_id, sid): curated_group = CuratedGroup.find_by_id(curated_group_id) if not curated_group: raise ResourceNotFoundError( f'No curated group found with id: {curated_group_id}') if curated_group.owner_id != current_user.get_id(): raise ForbiddenRequestError( f'Current user, {current_user.get_uid()}, does not own curated group {curated_group.id}' ) CuratedGroup.remove_student(curated_group_id, sid) return tolerant_jsonify(curated_group.to_api_json(include_students=False))
def delete_curated_group(curated_group_id): curated_group = CuratedGroup.find_by_id(curated_group_id) if not curated_group: raise ResourceNotFoundError( f'No curated group found with id: {curated_group_id}') if curated_group.owner_id != current_user.get_id(): raise ForbiddenRequestError( f'Current user, {current_user.get_uid()}, does not own curated group {curated_group.id}' ) CuratedGroup.delete(curated_group_id) return tolerant_jsonify( {'message': f'Curated group {curated_group_id} has been deleted'}), 200