def test_create_and_delete_cohort(self): """Cohort_filter record to Flask-Login for recognized UID.""" owner = AuthorizedUser.find_by_uid(asc_advisor_uid).uid # Check validity of UID assert owner # Create cohort group_codes = ['MFB-DB', 'MFB-DL', 'MFB-MLB', 'MFB-OLB'] cohort = CohortFilter.create( uid=owner, name='Football, Defense', filter_criteria={ 'groupCodes': group_codes, }, ) cohort_id = cohort['id'] assert CohortFilter.find_by_id(cohort_id)['owner']['uid'] == owner assert cohort['totalStudentCount'] == len( CohortFilter.get_sids(cohort_id)) # Delete cohort and verify previous_owner_count = cohort_count(owner) CohortFilter.delete(cohort_id) std_commit(allow_test_environment=True) assert cohort_count(owner) == previous_owner_count - 1
def _construct_phantom_cohort(filters, **kwargs): # A "phantom" cohort is an unsaved search. cohort = CohortFilter( name=f'phantom_cohort_{datetime.now().timestamp()}', filter_criteria=_translate_filters_to_cohort_criteria(filters), ) return cohort.to_api_json(**kwargs)
def test_distinct_sids(self, client, fake_auth): """Get distinct SIDs across cohorts and curated groups.""" user_id = AuthorizedUser.get_id_per_uid(coe_advisor_uid) cohort_ids = [] sids = set() for cohort in CohortFilter.get_cohorts(user_id): cohort_id = cohort['id'] cohort_ids.append(cohort_id) sids.update(set(CohortFilter.get_sids(cohort_id))) assert len(sids) > 1 curated_group = CuratedGroup.create( user_id, 'Like a lemon to a lime, a lime to a lemon') curated_group_ids = [curated_group.id] # We use SIDs from cohorts (above). Therefore, we expect no increase in 'batch_distinct_student_count'. for sid in sids: CuratedGroup.add_student(curated_group.id, sid) # A specific student (SID) that is in neither cohorts nor curated groups. some_other_sid = '5678901234' assert some_other_sid not in sids # Log in as authorized user fake_auth.login(coe_advisor_uid) data = self._api_distinct_sids( client, cohort_ids=cohort_ids, curated_group_ids=curated_group_ids, sids=[some_other_sid], ) assert sids.union({some_other_sid}) == set(data['sids'])
def test_cohort_update_filter_criteria(self, client, asc_advisor_session): label = 'Swimming, Men\'s' original_student_count = 4 cohort = CohortFilter.create( uid=asc_advisor_uid, label=label, group_codes=['MSW', 'MSW-DV', 'MSW-SW'], student_count=original_student_count, ) assert original_student_count > 0 updated_filter_criteria = { 'groupCodes': ['MSW-DV', 'MSW-SW'], } data = { 'id': cohort.id, 'filterCriteria': updated_filter_criteria, 'studentCount': original_student_count - 1, } response = client.post('/api/filtered_cohort/update', data=json.dumps(data), content_type='application/json') assert 200 == response.status_code updated_cohort = CohortFilter.find_by_id(int(response.json['id'])) assert updated_cohort.label == label assert updated_cohort.student_count == original_student_count - 1 def remove_empties(filter_criteria): return {k: v for k, v in filter_criteria.items() if v is not None} expected = remove_empties(cohort.filter_criteria) actual = remove_empties(updated_cohort.filter_criteria) assert expected == actual
def test_filter_criteria(self): gpa_ranges = [ 'numrange(0, 2, \'[)\')', 'numrange(2, 2.5, \'[)\')', ] group_codes = ['MFB-DB', 'MFB-DL'] levels = ['Junior'] majors = ['Environmental Economics & Policy', 'Gender and Women\'s Studies'] unit_ranges = [ 'numrange(0, 5, \'[]\')', 'numrange(30, NULL, \'[)\')', ] cohort = CohortFilter.create( uid='1022796', label='All criteria, all the time', gpa_ranges=gpa_ranges, group_codes=group_codes, in_intensive_cohort=None, levels=levels, majors=majors, unit_ranges=unit_ranges, ) cohort = CohortFilter.find_by_id(cohort.id) expected = { 'gpaRanges': gpa_ranges, 'groupCodes': group_codes, 'inIntensiveCohort': None, 'levels': levels, 'majors': majors, 'unitRanges': unit_ranges, } def sort_and_format(filter_criteria): return json.dumps(filter_criteria, sort_keys=True, indent=2) assert sort_and_format(expected) == sort_and_format(cohort.filter_criteria)
def update_cohort(): params = request.get_json() cohort_id = int(params.get('id')) name = params.get('name') filters = params.get('filters') # Validation if not name and not filters: raise BadRequestError('Invalid request') if not CohortFilter.is_cohort_owned_by(cohort_id, current_user.get_id()): raise ForbiddenRequestError(f'Invalid or unauthorized request') 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' ) filter_criteria = _translate_filters_to_cohort_criteria(filters) updated = CohortFilter.update( cohort_id=cohort_id, name=name, filter_criteria=filter_criteria, include_students=False, include_alerts_for_user_id=current_user.get_id(), ) _decorate_cohort(updated) return tolerant_jsonify(updated)
def test_cohort_update(self): cohort = CohortFilter.create(label='Football teams', team_codes=['FBM', 'FBW'], uid='2040') foosball_label = 'Foosball teams' cohort = CohortFilter.update(cohort['id'], foosball_label) assert cohort['label'] == foosball_label
def test_undefined_filter_criteria(self): with pytest.raises(InternalServerError): CohortFilter.create( uid=asc_advisor_uid, label='Cohort with undefined filter criteria', genders=[], in_intensive_cohort=None, )
def dismiss_alert(alert_id): user_id = current_user.get_id() Alert.dismiss(alert_id, user_id) CohortFilter.refresh_alert_counts_for_owner(user_id) return tolerant_jsonify({ 'message': f'Alert {alert_id} dismissed by UID {current_user.get_uid()}' }), 200
def test_delete_cohort(self, client, coe_advisor_session): """Deletes existing custom cohort while enforcing rules of ownership.""" label = 'Water polo teams' cohort = CohortFilter.create(uid=coe_advisor_uid, label=label, group_codes=['WWP', 'MWP']) # Verify deletion response = client.delete(f'/api/filtered_cohort/delete/{cohort.id}') assert response.status_code == 200 cohorts = CohortFilter.all_owned_by(coe_advisor_uid) assert not next((c for c in cohorts if c.id == cohort.id), None)
def delete(cls, curated_group_id): curated_group = cls.query.filter_by(id=curated_group_id).first() if curated_group: db.session.delete(curated_group) std_commit() # Delete all cohorts that reference the deleted group for cohort_filter_id in curated_group.get_referencing_cohort_ids(): CohortFilter.delete(cohort_filter_id) std_commit()
def test_undefined_filter_criteria(self): with pytest.raises(InternalServerError): CohortFilter.create( uid=asc_advisor_uid, name='Cohort with undefined filter criteria', filter_criteria={ 'genders': [], 'inIntensiveCohort': None, }, )
def test_filter_criteria(self): gpa_ranges = [ { 'min': 0, 'max': 1.999 }, { 'min': 2, 'max': 2.499 }, ] group_codes = ['MFB-DB', 'MFB-DL'] intended_majors = ['Public Health BA'] levels = ['Junior'] majors = [ 'Environmental Economics & Policy', 'Gender and Women\'s Studies' ] minors = ['Physics UG'] unit_ranges = [ 'numrange(0, 5, \'[]\')', 'numrange(30, NULL, \'[)\')', ] cohort = CohortFilter.create( uid='1022796', name='All criteria, all the time', filter_criteria={ 'gpaRanges': gpa_ranges, 'groupCodes': group_codes, 'inIntensiveCohort': None, 'intendedMajors': intended_majors, 'levels': levels, 'majors': majors, 'minors': minors, 'unitRanges': unit_ranges, }, ) cohort_id = cohort['id'] cohort = CohortFilter.find_by_id(cohort_id) expected = { 'gpaRanges': gpa_ranges, 'groupCodes': group_codes, 'inIntensiveCohort': None, 'intendedMajors': intended_majors, 'levels': levels, 'majors': majors, 'minors': minors, 'unitRanges': unit_ranges, } for key, value in expected.items(): assert cohort['criteria'][key] == expected[key] assert cohort['totalStudentCount'] == len( CohortFilter.get_sids(cohort_id))
def test_delete_cohort(self, authenticated_session, client): """deletes existing custom cohort while enforcing rules of ownership""" label = 'Water polo teams' cohort = CohortFilter.create(label=label, team_codes=['WPW', 'WPM'], uid=test_uid) assert cohort and 'id' in cohort id_of_created_cohort = cohort['id'] # Verify deletion response = client.delete('/api/cohort/delete/{}'.format(id_of_created_cohort)) assert response.status_code == 200 cohorts = CohortFilter.all_owned_by(test_uid) assert not next((c for c in cohorts if c['id'] == id_of_created_cohort), None)
def delete_cohort(cohort_id): if cohort_id.isdigit(): cohort_id = int(cohort_id) if CohortFilter.is_cohort_owned_by(cohort_id, current_user.get_id()): CohortFilter.delete(cohort_id) return tolerant_jsonify( {'message': f'Cohort deleted (id={cohort_id})'}), 200 else: raise BadRequestError( f'User {current_user.get_uid()} does not own cohort with id={cohort_id}' ) else: raise ForbiddenRequestError( f'Programmatic deletion of canned cohorts is not allowed (id={cohort_id})' )
def update_cohort(): params = request.get_json() uid = current_user.get_id() label = params['label'] if not label: raise BadRequestError('Requested cohort label is empty or invalid') cohort = get_cohort_owned_by(params['id'], uid) if not cohort: raise BadRequestError( 'Cohort does not exist or is not owned by {}'.format(uid)) CohortFilter.update(cohort_id=cohort['id'], label=label) return jsonify({'message': 'Cohort updated (label: {})'.format(label)}), 200
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 create_cohort(): params = request.get_json() if is_unauthorized_search(params): raise ForbiddenRequestError( 'You are unauthorized to access student data managed by other departments' ) label = util.get(params, 'label', None) if not label: raise BadRequestError('Cohort creation requires \'label\'') cohort = CohortFilter.create( advisor_ldap_uids=util.get(params, 'advisorLdapUids'), coe_prep_statuses=util.get(params, 'coePrepStatuses'), ethnicities=util.get(params, 'ethnicities'), genders=util.get(params, 'genders'), gpa_ranges=util.get(params, 'gpaRanges'), group_codes=util.get(params, 'groupCodes'), in_intensive_cohort=util.to_bool_or_none( params.get('inIntensiveCohort')), is_inactive_asc=util.to_bool_or_none(params.get('isInactiveAsc')), label=label, last_name_range=util.get(params, 'lastNameRange'), levels=util.get(params, 'levels'), majors=util.get(params, 'majors'), uid=current_user.get_id(), underrepresented=util.get(params, 'underrepresented'), unit_ranges=util.get(params, 'unitRanges'), ) return tolerant_jsonify(decorate_cohort(cohort))
def my_profile(): uid = current_user.get_id() profile = calnet.get_calnet_user_for_uid(app, uid) if current_user.is_active: authorized_user_id = current_user.id curated_cohorts = CuratedCohort.get_curated_cohorts_by_owner_id(authorized_user_id) curated_cohorts = [c.to_api_json(sids_only=True) for c in curated_cohorts] departments = {} for m in current_user.department_memberships: departments.update({ m.university_dept.dept_code: { 'isAdvisor': m.is_advisor, 'isDirector': m.is_director, }, }) my_cohorts = CohortFilter.all_owned_by(uid) profile.update({ 'myFilteredCohorts': [c.to_api_json(include_students=False) for c in my_cohorts], 'myCuratedCohorts': curated_cohorts, 'isAdmin': current_user.is_admin, 'departments': departments, }) else: profile.update({ 'myFilteredCohorts': None, 'myCuratedCohorts': None, 'isAdmin': False, 'departments': None, }) return tolerant_jsonify(profile)
def _filters_to_filter_criteria(filters, order_by=None): 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' ) return CohortFilter.translate_filters_to_cohort_criteria(filters)
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 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_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 = CohortFilter.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 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 test_unauthorized_cohort_update(self, client, coe_advisor_session): cohort = CohortFilter.create(uid=asc_advisor_uid, label='Swimming, Men\'s', group_codes=['MSW', 'MSW-DV', 'MSW-SW']) data = { 'id': cohort.id, 'label': 'Hack the label!', } response = client.post('/api/filtered_cohort/update', data=json.dumps(data), content_type='application/json') assert 403 == response.status_code
def delete_cohort(cohort_id): if cohort_id.isdigit(): cohort_id = int(cohort_id) uid = current_user.get_id() cohort = get_cohort_owned_by(cohort_id, uid) if cohort: CohortFilter.delete(cohort_id) return jsonify( {'message': 'Cohort deleted (id={})'.format(cohort_id)}), 200 else: raise BadRequestError( 'User {uid} does not own cohort_filter with id={id}'.format( uid=uid, id=cohort_id)) else: raise ForbiddenRequestError( 'Programmatic deletion of teams is not supported (id={})'.format( cohort_id))
def test_create_and_delete_cohort(self): """Cohort_filter record to Flask-Login for recognized UID.""" owner = AuthorizedUser.find_by_uid(asc_advisor_uid).uid shared_with = AuthorizedUser.find_by_uid(coe_advisor_uid).uid # Check validity of UIDs assert owner assert shared_with # Create and share cohort group_codes = ['MFB-DB', 'MFB-DL', 'MFB-MLB', 'MFB-OLB'] cohort = CohortFilter.create( uid=owner, name='Football, Defense', filter_criteria={ 'groupCodes': group_codes, }, ) cohort_id = cohort['id'] CohortFilter.share(cohort_id, shared_with) owners = CohortFilter.find_by_id(cohort_id)['owners'] assert len(owners) == 2 assert owner, shared_with in [user['uid'] for user in owners] assert cohort['totalStudentCount'] == len( CohortFilter.get_sids(cohort_id)) # Delete cohort and verify previous_owner_count = cohort_count(owner) previous_shared_count = cohort_count(shared_with) CohortFilter.delete(cohort_id) assert cohort_count(owner) == previous_owner_count - 1 assert cohort_count(shared_with) == previous_shared_count - 1
def delete_cohort(cohort_id): if cohort_id.isdigit(): cohort_id = int(cohort_id) uid = current_user.get_id() cohort = next( (c for c in CohortFilter.all_owned_by(uid) if c.id == cohort_id), None) if cohort: CohortFilter.delete(cohort_id) return tolerant_jsonify( {'message': f'Cohort deleted (id={cohort_id})'}), 200 else: raise BadRequestError( f'User {uid} does not own cohort_filter with id={cohort_id}') else: raise ForbiddenRequestError( f'Programmatic deletion of canned cohorts is not allowed (id={cohort_id})' )
def all_cohorts(): cohorts = {} for cohort in CohortFilter.all(): for uid in cohort['owners']: if uid not in cohorts: cohorts[uid] = [] cohorts[uid].append(cohort) return jsonify(cohorts)
def test_delete_cohort_wrong_user(self, client, fake_auth): """custom cohort deletion is only available to owners""" cohort = CohortFilter.create(label='Badminton teams', team_codes=['MBK', 'WBK'], uid=test_uid) assert cohort and 'id' in cohort # This user does not own the custom cohort above fake_auth.login('2040') response = client.delete('/api/cohort/delete/{}'.format(cohort['id'])) assert response.status_code == 400 assert '2040 does not own' in str(response.data)