def _create_waiting_appointments(): coe_advisor_user_id = AuthorizedUser.get_id_per_uid('90412') coe_scheduler_user_id = AuthorizedUser.get_id_per_uid('6972201') l_s_advisor_user_id = AuthorizedUser.get_id_per_uid('53791') Appointment.create( appointment_type='Drop-in', created_by=coe_scheduler_user_id, dept_code='COENG', details='Meet me at the crossroads.', student_sid='3456789012', topics=['Topic for appointments, 2'], ) Appointment.create( appointment_type='Drop-in', created_by=coe_advisor_user_id, dept_code='COENG', details='Life is what happens while you\'re making appointments.', student_sid='5678901234', topics=['Good Show'], ) # L&S College Advising Appointment.create( appointment_type='Drop-in', created_by=l_s_advisor_user_id, dept_code='QCADV', details='C-c-catch the wave!', student_sid='5678901234', topics=['Topic for appointments, 1', 'Good Show'], ) Appointment.create( appointment_type='Drop-in', created_by=l_s_advisor_user_id, dept_code='QCADV', details='You be you.', student_sid='11667051', topics=['Topic for appointments, 1'], )
def _update_or_create_authorized_user(memberships, profile, include_deleted=False): user_id = profile.get('id') can_access_canvas_data = to_bool_or_none( profile.get('canAccessCanvasData')) can_access_advising_data = to_bool_or_none( profile.get('canAccessAdvisingData')) degree_progress_permission = profile.get('degreeProgressPermission') if degree_progress_permission and 'COENG' not in dept_codes_where_advising( {'departments': memberships}): raise errors.BadRequestError( 'Degree Progress feature is only available to the College of Engineering.' ) is_admin = to_bool_or_none(profile.get('isAdmin')) is_blocked = to_bool_or_none(profile.get('isBlocked')) if user_id: return AuthorizedUser.update_user( user_id=user_id, can_access_advising_data=can_access_advising_data, can_access_canvas_data=can_access_canvas_data, degree_progress_permission=degree_progress_permission, is_admin=is_admin, is_blocked=is_blocked, include_deleted=include_deleted, ) else: uid = profile.get('uid') if AuthorizedUser.get_id_per_uid(uid, include_deleted=True): raise errors.BadRequestError( f'User with UID {uid} is already in the BOA database.') calnet_user = calnet.get_calnet_user_for_uid(app, uid, skip_expired_users=True) if calnet_user and calnet_user.get('csid', None): return AuthorizedUser.create_or_restore( uid=uid, created_by=current_user.get_uid(), is_admin=is_admin, is_blocked=is_blocked, can_access_advising_data=can_access_advising_data, can_access_canvas_data=can_access_canvas_data, degree_progress_permission=degree_progress_permission, ) else: raise errors.BadRequestError('Invalid UID')
def test_search_note_with_null_body(self, asc_advisor, client): """Finds newly created BOA note when note body is null.""" response = client.post( '/api/notes/create', data={ 'authorId': AuthorizedUser.get_id_per_uid(asc_advisor_uid), 'sids': ['9000000000'], 'subject': 'Patience is a conquering virtue', }, ) assert response.status_code == 200 note = response.json api_json = _api_search(client, 'a conquering virtue', notes=True) self._assert(api_json, note_count=1, note_ids=[note['id']])
def test_create_note_with_raw_url_in_body(self, app, client, fake_auth): """Create a note with topics.""" fake_auth.login(coe_advisor_uid) note = _api_note_create( app, client, author_id=AuthorizedUser.get_id_per_uid(coe_advisor_uid), sids=[coe_student['sid']], subject='Get rich quick', body='Get an online degree at send.money.edu university', ) expected_body = 'Get an online degree at <a href="http://send.money.edu" target="_blank">send.money.edu</a> university' assert note.get('body') == expected_body assert note['createdAt'] is not None assert note['updatedAt'] is None
def test_get_note_by_id(self, app, client, fake_auth, mock_coe_advising_note): """Returns note in JSON compatible with BOA front-end.""" fake_auth.login(admin_uid) note = self._api_note_by_id(client=client, note_id=mock_coe_advising_note.id) assert note assert 'id' in note assert note['type'] == 'note' assert note['body'] == note['message'] assert note['read'] is False # Mark as read and re-test NoteRead.find_or_create(AuthorizedUser.get_id_per_uid(admin_uid), note['id']) assert self._api_note_by_id( client=client, note_id=mock_coe_advising_note.id)['read'] is True
def test_search_by_appointment_cancel_reason(self, coe_advisor, client): """Appointments can be searched for by cancel reason and cancel reason explained.""" appointment = Appointment.find_by_id(1) Appointment.cancel( appointment_id=appointment.id, cancelled_by=AuthorizedUser.get_id_per_uid('6972201'), cancel_reason='Sick cat', cancel_reason_explained= 'Student needed to attend to ailing feline.', ) api_json = _api_search(client, 'cat', appointments=True) self._assert(api_json, appointment_count=1) api_json = _api_search(client, 'feline', appointments=True) self._assert(api_json, appointment_count=1)
def test_create_note_with_topics(self, app, client, fake_auth): """Create a note with topics.""" fake_auth.login(coe_advisor_uid) note = _api_note_create( app, client, author_id=AuthorizedUser.get_id_per_uid(coe_advisor_uid), sids=[coe_student['sid']], subject='Incubate transparent web services', body='Facilitate value-added initiatives', topics=['Shadrach', 'Meshach', 'Abednego'], ) assert len(note.get('topics')) == 3 for topic in ('Shadrach', 'Meshach', 'Abednego'): assert topic in note.get('topics') assert note['createdAt'] is not None assert note['updatedAt'] is None
def test_delete_note_with_topics(self, app, client, fake_auth): """Delete a note with topics.""" fake_auth.login(coe_advisor_uid) note = _api_note_create( app, client, author_id=AuthorizedUser.get_id_per_uid(coe_advisor_uid), sids=[coe_student['sid']], subject='Recontextualize open-source supply-chains', body='Conveniently repurpose enterprise-wide action items', topics=['strategic interfaces'], ) # Log in as Admin and delete the note fake_auth.login(admin_uid) note_id = note.get('id') response = client.delete(f'/api/notes/delete/{note_id}') assert response.status_code == 200
def test_create_note_with_topics(self, app, client, fake_auth): """Create a note with topics.""" fake_auth.login(coe_advisor_uid) note = _api_note_create( app, client, author_id=AuthorizedUser.get_id_per_uid(coe_advisor_uid), sid=coe_student['sid'], subject='Incubate transparent web services', body='Facilitate value-added initiatives', topics=['collaborative synergies', 'integrated architectures', 'vertical solutions'], ) assert len(note.get('topics')) == 3 assert note.get('topics')[0] == 'Collaborative Synergies' assert note.get('topics')[1] == 'Integrated Architectures' assert note.get('topics')[2] == 'Vertical Solutions' assert note['createdAt'] is not None assert note['updatedAt'] is None
def test_advisor_read_appointment(self, app, client, fake_auth): """L&S advisor reads an appointment.""" fake_auth.login(l_s_college_scheduler_uid) # As scheduler, create appointment appointment = AppointmentTestUtil.create_appointment(client, 'QCADV') appointment_id = appointment['id'] client.get('/api/auth/logout') # Verify unread by advisor uid = l_s_college_advisor_uid user_id = AuthorizedUser.get_id_per_uid(uid) assert AppointmentRead.was_read_by(user_id, appointment_id) is False # Next, log in as advisor and read the appointment fake_auth.login(uid) api_json = self._mark_appointment_read(client, appointment_id) assert api_json['appointmentId'] == appointment_id assert api_json['viewerId'] == user_id assert AppointmentRead.was_read_by(user_id, appointment_id) is True Appointment.delete(appointment_id)
def mock_degree_course(): marker = datetime.now().timestamp() sid = '11667051' degree_check = DegreeProgressTemplate.create( advisor_dept_codes=['COENG'], created_by=AuthorizedUser.get_id_per_uid(coe_advisor_read_write_uid), degree_name=f'Degree check of {coe_student_sid}', student_sid=sid, ) return DegreeProgressCourse.create( degree_check_id=degree_check.id, display_name=f'The Decline of Western Civilization ({marker})', grade='B+', section_id=datetime.utcfromtimestamp(0).microsecond, sid=sid, term_id=2218, units=4, )
def test_create_note_with_attachments(self, app, client, fake_auth): """Create a note, with two attachments.""" fake_auth.login(coe_advisor_uid) base_dir = app.config['BASE_DIR'] note = _api_note_create( app, client, author_id=AuthorizedUser.get_id_per_uid(coe_advisor_uid), sid=coe_student['sid'], subject='I come with attachments', body='I come correct', attachments=[ f'{base_dir}/fixtures/mock_advising_note_attachment_1.txt', f'{base_dir}/fixtures/mock_advising_note_attachment_2.txt', ], ) assert len(note.get('attachments')) == 2 assert note['createdAt'] is not None assert note['updatedAt'] is None
def mock_degree_check(): user_id = AuthorizedUser.get_id_per_uid(coe_advisor_read_write_uid) parent_template = DegreeProgressTemplate.create( advisor_dept_codes=['COENG'], created_by=user_id, degree_name='Zoology BS 2021', ) degree_check = DegreeProgressTemplate.create( advisor_dept_codes=['COENG'], created_by=user_id, degree_name='Zoology BS 2021', parent_template_id=parent_template.id, student_sid=coe_student_sid, ) std_commit(allow_test_environment=True) yield degree_check # Avoid polluting other tests DegreeProgressTemplate.delete(degree_check.id) std_commit(allow_test_environment=True)
def test_get_topic_usage_statistics(self, client, fake_auth): """Admin user can update a topic.""" fake_auth.login(admin_uid) api_json = self._api_usage_statistics(client) assert list(api_json.keys()) == ['appointments', 'notes'] assert len(api_json['appointments']) # Verify counts admin_user_id = AuthorizedUser.get_id_per_uid(uid=admin_uid) all_appointments = Appointment.query.filter( Appointment.deleted_at == None).all() # noqa: E711 all_appointments = [ a.to_api_json(current_user_id=admin_user_id) for a in all_appointments ] for topic_id, count in api_json['appointments'].items(): topic = Topic.find_by_id(topic_id) matches = list( filter(lambda a: topic.topic in a['topics'], all_appointments)) assert len(matches) == count
def cas_login(): logger = app.logger ticket = request.args['ticket'] target_url = request.args.get('url') uid, attributes, proxy_granting_ticket = _cas_client( target_url).verify_ticket(ticket) logger.info(f'Logged into CAS as user {uid}') user_id = AuthorizedUser.get_id_per_uid(uid) if user_id is None: logger.error(f'UID {uid} is not an authorized user.') param = ('error', f""" Sorry, you are not registered to use BOA. Please <a href="mailto:{app.config['BOAC_SUPPORT_EMAIL']}">email us</a> for assistance. """) redirect_url = add_param_to_url('/', param) else: user = UserSession(user_id=user_id, flush_cached=True) if not user.is_active: logger.error( f'UID {uid} is in the BOA db but is not authorized to use the tool.' ) param = ('error', f""" Sorry, you are not registered to use BOA. Please <a href="mailto:{app.config['BOAC_SUPPORT_EMAIL']}">email us</a> for assistance. """) redirect_url = add_param_to_url('/', param) else: login_user(user) flash('Logged in successfully.') UserLogin.record_user_login(uid) # Check if url is safe for redirects per https://flask-login.readthedocs.io/en/latest/ if not _is_safe_url(request.args.get('next')): return abort(400) if not target_url: target_url = '/' # Our googleAnalyticsService uses 'casLogin' marker to track CAS login events redirect_url = add_param_to_url(target_url, ('casLogin', 'true')) return redirect(redirect_url)
def test_add_attachments(self, app, client, fake_auth): """Add multiple attachments to an existing note.""" fake_auth.login(coe_advisor_uid) base_dir = app.config['BASE_DIR'] note = _api_note_create( app=app, author_id=AuthorizedUser.get_id_per_uid(coe_advisor_uid), body='I travel light', client=client, sids=[coe_student['sid']], subject='No attachments yet', ) assert note['updatedAt'] is None # Pause one second to ensure a distinct updatedAt. sleep(1) note_id = note['id'] with mock_advising_note_s3_bucket(app): data = { 'attachment[0]': open( f'{base_dir}/fixtures/mock_advising_note_attachment_1.txt', 'rb'), 'attachment[1]': open( f'{base_dir}/fixtures/mock_advising_note_attachment_2.txt', 'rb'), } response = client.post( f'/api/notes/{note_id}/attachments', buffered=True, content_type='multipart/form-data', data=data, ) assert response.status_code == 200 updated_note = response.json assert len(updated_note['attachments']) == 2 assert updated_note['attachments'][0][ 'filename'] == 'mock_advising_note_attachment_1.txt' assert updated_note['attachments'][1][ 'filename'] == 'mock_advising_note_attachment_2.txt' assert updated_note['updatedAt'] is not None
def create( cls, created_by, dept_code, details, appointment_type, student_sid, advisor_uid=None, topics=(), ): if advisor_uid: status = 'reserved' status_by = AuthorizedUser.get_id_per_uid(advisor_uid) else: status = 'waiting' status_by = created_by appointment = cls( advisor_uid=advisor_uid, appointment_type=appointment_type, created_by=created_by, dept_code=dept_code, details=details, status=status, student_sid=student_sid, updated_by=created_by, ) for topic in topics: appointment.topics.append( AppointmentTopic.create(appointment, topic), ) db.session.add(appointment) std_commit() AppointmentEvent.create( appointment_id=appointment.id, user_id=status_by, event_type=status, ) cls.refresh_search_index() return appointment
def to_api_json(self, current_user_id): topics = [t.to_api_json() for t in self.topics if not t.deleted_at] departments = None if self.advisor_dept_codes: departments = [{ 'code': c, 'name': BERKELEY_DEPT_CODE_TO_NAME.get(c, c) } for c in self.advisor_dept_codes] api_json = { 'id': self.id, 'advisorId': AuthorizedUser.get_id_per_uid(self.advisor_uid), 'advisorName': self.advisor_name, 'advisorRole': self.advisor_role, 'advisorUid': self.advisor_uid, 'advisorDepartments': departments, 'appointmentType': self.appointment_type, 'createdAt': _isoformat(self.created_at), 'createdBy': self.created_by, 'deptCode': self.dept_code, 'details': self.details, 'read': AppointmentRead.was_read_by(current_user_id, self.id), 'student': { 'sid': self.student_sid, }, 'topics': topics, 'updatedAt': _isoformat(self.updated_at), 'updatedBy': self.updated_by, } if self.appointment_type == 'Scheduled': api_json.update({ 'scheduledTime': _isoformat(self.scheduled_time), 'studentContactInfo': self.student_contact_info, 'studentContactType': self.student_contact_type, }) return { **api_json, **appointment_event_to_json(self.id, self.status), }
def test_create_note_with_attachments(self, app, client, fake_auth, mock_note_template): """Create a note, with two attachments.""" fake_auth.login(coe_advisor_uid) base_dir = app.config['BASE_DIR'] note = _api_note_create( app, client, author_id=AuthorizedUser.get_id_per_uid(coe_advisor_uid), sids=[coe_student['sid']], subject='I come with attachments', body='I come correct', attachments=[ f'{base_dir}/fixtures/mock_advising_note_attachment_1.txt', f'{base_dir}/fixtures/mock_advising_note_attachment_2.txt', ], template_attachment_ids=list(map(lambda a: a.id, mock_note_template.attachments)), ) template_attachment_count = len(mock_note_template.attachments) assert template_attachment_count expected_attachment_count = template_attachment_count + 2 assert len(note.get('attachments')) == expected_attachment_count assert note['createdAt'] is not None assert note['updatedAt'] is None
def test_create_note(self, app, client, fake_auth): """Create a note.""" fake_auth.login(coe_advisor_uid) subject = 'Vicar in a Tutu' new_note = _api_note_create( app, client, author_id=AuthorizedUser.get_id_per_uid(coe_advisor_uid), sids=[coe_student['sid']], subject=subject, body='A scanty bit of a thing with a decorative ring', ) note_id = new_note.get('id') assert new_note['read'] is True assert isinstance(note_id, int) and note_id > 0 assert new_note['author']['uid'] == coe_advisor_uid assert 'name' in new_note['author'] assert new_note['author']['role'] == 'advisor' assert new_note['author']['departments'][0]['name'] == 'College of Engineering' assert new_note['updatedAt'] is None # Get notes per SID and compare notes = _get_notes(client, coe_student['uid']) match = next((n for n in notes if n['id'] == note_id), None) assert match and match['subject'] == subject
def advisor_2_id(): return AuthorizedUser.get_id_per_uid('188242')
def advisor_1_id(): return AuthorizedUser.get_id_per_uid('53791')
def get_filter_option_groups(self): current_year = datetime.now().year owner_user_id = AuthorizedUser.get_id_per_uid( self.owner_uid) if self.owner_uid else None return { 'Academic': [ _filter( 'academicStandings', 'Academic Standing', options=academic_standing_options( min_term_id=sis_term_id_for_name( f'Fall {current_year - 5}')), ), _filter('colleges', 'College', options=colleges), _filter('enteringTerms', 'Entering Term', options=entering_terms), _filter('epnCpnGradingTerms', 'EPN/CPN Grading Option', options=grading_terms), _filter('expectedGradTerms', 'Expected Graduation Term', options=grad_terms), _range_filter('gpaRanges', 'GPA (Cumulative)', labels_range=['', '-'], validation='gpa'), _range_filter('lastTermGpaRanges', 'GPA (Last Term)', labels_range=['', '-'], validation='gpa'), _boolean_filter('studentHolds', 'Holds'), _filter('intendedMajors', 'Intended Major', options=intended_majors), _filter('levels', 'Level', options=level_options), _filter('majors', 'Major', options=majors), _filter('minors', 'Minor', options=minors), _boolean_filter('midpointDeficient', 'Midpoint Deficient Grade'), _boolean_filter('transfer', 'Transfer Student'), _filter('unitRanges', 'Units Completed', options=unit_range_options), ], 'Demographics': [ _filter('ethnicities', 'Ethnicity', options=ethnicities), _filter('genders', 'Gender', options=genders), _range_filter( 'lastNameRanges', 'Last Name', labels_range=['Initials', 'through'], label_min_equals_max='Starts with', validation='char[2]', ), _boolean_filter('underrepresented', 'Underrepresented Minority'), _filter('visaTypes', 'Visa Type', options=visa_types), ], 'Departmental (ASC)': [ _boolean_filter_asc( 'isInactiveAsc', 'Inactive (ASC)', default_value=False if 'UWASC' in self.scope else None, ), _boolean_filter('inIntensiveCohort', 'Intensive (ASC)', available_to=['UWASC']), _filter('groupCodes', 'Team (ASC)', options=team_groups, available_to=['UWASC']), ], 'Departmental (COE)': [ _filter('coeAdvisorLdapUids', 'Advisor (COE)', options=get_coe_profiles, available_to=['COENG']), _filter('coeEthnicities', 'Ethnicity (COE)', options=coe_ethnicities, available_to=['COENG']), _filter('coeGenders', 'Gender (COE)', options=coe_gender_options, available_to=['COENG']), _filter('coePrepStatuses', 'PREP (COE)', options=coe_prep_status_options, available_to=['COENG']), _boolean_filter_coe('coeProbation', 'Probation (COE)'), _boolean_filter_coe( 'isInactiveCoe', 'Inactive (COE)', default_value=False if 'COENG' in self.scope else None), _boolean_filter_coe('coeUnderrepresented', 'Underrepresented Minority (COE)'), ], 'Advising': [ _filter('curatedGroupIds', 'My Curated Groups', options=lambda: curated_groups(owner_user_id) if owner_user_id else None), _filter( 'cohortOwnerAcademicPlans', 'My Students', options=lambda: academic_plans_for_cohort_owner( self.owner_uid) if self.owner_uid else None, ), _filter( 'freshmanOrTransfer', 'Freshman or Transfer', options=student_admit_freshman_or_transfer_options, available_to=['ZCEEE'], domain_='admitted_students', ), _boolean_filter_ce3('sir', 'Current SIR'), _filter( 'admitColleges', 'College', available_to=['ZCEEE'], domain_='admitted_students', options=student_admit_college_options, ), _filter( 'xEthnicities', 'XEthnic', available_to=['ZCEEE'], domain_='admitted_students', options=student_admit_ethnicity_options, ), _boolean_filter_ce3('isHispanic', 'Hispanic'), _boolean_filter_ce3('isUrem', 'UREM'), _boolean_filter_ce3('isFirstGenerationCollege', 'First Generation College'), _boolean_filter_ce3('hasFeeWaiver', 'Application Fee Waiver'), _filter( 'residencyCategories', 'Residency', available_to=['ZCEEE'], domain_='admitted_students', options=student_admit_residency_category_options, ), _boolean_filter_ce3('inFosterCare', 'Foster Care'), _boolean_filter_ce3('isFamilySingleParent', 'Family Is Single Parent'), _boolean_filter_ce3('isStudentSingleParent', 'Student Is Single Parent'), _range_filter( 'familyDependentRanges', 'Family Dependents', labels_range=['', '-'], label_min_equals_max='Exactly', available_to=['ZCEEE'], domain_='admitted_students', validation='dependents', ), _range_filter( 'studentDependentRanges', 'Student Dependents', labels_range=['', '-'], label_min_equals_max='Exactly', available_to=['ZCEEE'], domain_='admitted_students', validation='dependents', ), _boolean_filter_ce3('isReentry', 'Re-entry Status'), _boolean_filter_ce3('isLastSchoolLCFF', 'Last School LCFF+'), _filter( 'specialProgramCep', 'Special Program CEP', available_to=['ZCEEE'], domain_='admitted_students', options=student_admit_special_program_cep_options, ), ], }
def _create_checked_in_appointments(): coe_scheduler_user_id = AuthorizedUser.get_id_per_uid('6972201') coe_advisor_uid = '90412' coe_advisor_user_id = AuthorizedUser.get_id_per_uid(coe_advisor_uid) l_s_advisor_uid = '53791' l_s_advisor_user_id = AuthorizedUser.get_id_per_uid(l_s_advisor_uid) check_me_in = Appointment.create( appointment_type='Drop-in', created_by=coe_scheduler_user_id, dept_code='COENG', details='Pick me, pick me!', student_sid='3456789012', topics=['Topic for appointments, 2'], ) Appointment.check_in( appointment_id=check_me_in.id, advisor_dept_codes=['COENG'], advisor_name='Johnny C. Lately', advisor_role='Advisor', advisor_uid=coe_advisor_uid, checked_in_by=coe_advisor_user_id, ) check_me_in = Appointment.create( appointment_type='Drop-in', created_by=coe_advisor_user_id, dept_code='COENG', details='Making appointments is da bomb.', student_sid='5678901234', topics=['Good Show'], ) Appointment.check_in( appointment_id=check_me_in.id, advisor_dept_codes=['COENG'], advisor_name='Johnny C. Lately', advisor_role='Advisor', advisor_uid=coe_advisor_uid, checked_in_by=coe_advisor_user_id, ) check_me_in = Appointment.create( appointment_type='Drop-in', created_by=coe_scheduler_user_id, dept_code='COENG', details='We will check in this student.', student_sid='9012345678', topics=['Please check me in.'], ) Appointment.check_in( appointment_id=check_me_in.id, checked_in_by=coe_advisor_user_id, advisor_uid=coe_advisor_uid, advisor_name='Johnny C. Lately', advisor_role='Advisor', advisor_dept_codes=['COENG'], ) # L&S College Advising check_me_in = Appointment.create( appointment_type='Drop-in', created_by=l_s_advisor_user_id, dept_code='QCADV', details='It is not the length of life, but depth of life.', student_sid='11667051', topics=['Topic for appointments, 1'], ) Appointment.check_in( appointment_id=check_me_in.id, advisor_dept_codes=['QCADV'], advisor_name='Max Headroom', advisor_role='Advisor', advisor_uid=l_s_advisor_uid, checked_in_by=l_s_advisor_user_id, )
def mock_template(): return DegreeProgressTemplate.create( advisor_dept_codes=['COENG'], created_by=AuthorizedUser.get_id_per_uid(coe_advisor_read_write_uid), degree_name='Zoology BS 2021', )