def _update_drop_in_availability(uid, dept_code, new_availability): dept_code = dept_code.upper() if uid != current_user.get_uid(): authorized_to_toggle = current_user.is_admin or dept_code in [ d['code'] for d in current_user.departments if d.get('role') == 'scheduler' ] if not authorized_to_toggle: raise errors.ForbiddenRequestError( f'Unauthorized to toggle drop-in availability for department {dept_code}' ) drop_in_membership = None user = AuthorizedUser.find_by_uid(uid) if user: drop_in_membership = next( (d for d in user.drop_in_departments if d.dept_code == dept_code), None) if drop_in_membership: if drop_in_membership.is_available is True and new_availability is False: Appointment.unreserve_all_for_advisor(uid, current_user.get_id()) drop_in_membership.update_availability(new_availability) UserSession.flush_cache_for_id(user.id) return tolerant_jsonify(drop_in_membership.to_api_json()) else: raise errors.ResourceNotFoundError( f'No drop-in advisor membership found: (uid={uid}, dept_code={dept_code})' )
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, canceled_by=AuthorizedUser.get_id_per_uid('6972201'), cancel_reason='Sick cat', cancel_reason_explained= 'Student needed to attend to ailing feline.', ) response = client.post( '/api/search', data=json.dumps({ 'appointments': True, 'notes': True, 'searchPhrase': 'cat' }), content_type='application/json', ) self._assert(response, appointment_count=1) response = client.post( '/api/search', data=json.dumps({ 'appointments': True, 'notes': True, 'searchPhrase': 'feline' }), content_type='application/json', ) self._assert(response, appointment_count=1)
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 _create_reserved_appointments(): coe_advisor_user_id = AuthorizedUser.get_id_per_uid('90412') reserve_me = Appointment.create( appointment_type='Drop-in', created_by=coe_advisor_user_id, dept_code='COENG', details='I will reserve this appointment.', student_sid='7890123456', topics=['Whoops'], ) Appointment.reserve(appointment_id=reserve_me.id, reserved_by=coe_advisor_user_id)
def test_reserve_appointment(self, app, client, fake_auth): """Drop-in advisor can reserve an appointment.""" dept_code = 'QCADV' advisor = DropInAdvisor.advisors_for_dept_code(dept_code)[0] user = AuthorizedUser.find_by_id(advisor.authorized_user_id) fake_auth.login(user.uid) waiting = AppointmentTestUtil.create_appointment(client, dept_code) appointment = self._reserve_appointment(client, waiting['id']) assert appointment['status'] == 'reserved' assert appointment['statusDate'] is not None assert appointment['statusBy']['id'] == user.id Appointment.delete(appointment['id'])
def _create_reserved_appointments(): coe_advisor_attrs = _advisor_attrs_for_uid('90412') reserve_me = Appointment.create( appointment_type='Drop-in', created_by=coe_advisor_attrs['id'], dept_code='COENG', details='I will reserve this appointment.', student_sid='7890123456', topics=['Whoops'], ) Appointment.reserve(appointment_id=reserve_me.id, reserved_by=coe_advisor_attrs['id'], advisor_attrs=coe_advisor_attrs)
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 test_search_with_no_input_and_date(self, coe_advisor, client): """Notes and appointments search needs no input when date options are set.""" from boac import db, std_commit appointment = Appointment.find_by_id(2) appointment.created_at = util.localized_timestamp_to_utc( '2017-11-01T00:00:00') std_commit() db.session.refresh(appointment) response = client.post( '/api/search', data=json.dumps({ 'appointments': True, 'notes': True, 'searchPhrase': '', 'appointmentOptions': { 'dateFrom': '2017-11-01', 'dateTo': '2017-11-02' }, 'noteOptions': { 'dateFrom': '2017-11-01', 'dateTo': '2017-11-02' }, }), content_type='application/json', ) self._assert(response, note_count=4, appointment_count=1)
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 test_appointment_cancel(self, app, client, fake_auth): """Drop-in advisor can cancel appointment.""" dept_code = 'QCADV' advisor = DropInAdvisor.advisors_for_dept_code(dept_code)[0] user = AuthorizedUser.find_by_id(advisor.authorized_user_id) fake_auth.login(user.uid) waiting = AppointmentTestUtil.create_appointment(client, dept_code) appointment = self._cancel_appointment(client, waiting['id'], 'Canceled by wolves') appointment_id = appointment['id'] assert appointment_id == waiting['id'] assert appointment['status'] == 'canceled' assert appointment['statusBy']['id'] == user.id assert appointment['statusBy']['uid'] == user.uid assert appointment['statusDate'] is not None Appointment.delete(appointment_id)
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 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 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 _create_cancelled_appointments(): coe_advisor_user_id = AuthorizedUser.get_id_per_uid('90412') scheduler_user_id = AuthorizedUser.get_id_per_uid('6972201') cancel_me = Appointment.create( appointment_type='Drop-in', created_by=coe_advisor_user_id, dept_code='COENG', details='We will cancel this appointment.', student_sid='7890123456', topics=['Whoops'], ) Appointment.cancel( appointment_id=cancel_me.id, cancelled_by=scheduler_user_id, cancel_reason='Just because', cancel_reason_explained='I felt like it.', )
def search_advising_appointments( search_phrase, advisor_csid=None, advisor_uid=None, student_csid=None, topic=None, datetime_from=None, datetime_to=None, offset=0, limit=20, ): benchmark = get_benchmarker('search_advising_appointments') benchmark('begin') if search_phrase: search_terms = list({t.group(0) for t in list(re.finditer(TEXT_SEARCH_PATTERN, search_phrase)) if t}) search_phrase = ' & '.join(search_terms) else: search_terms = [] advisor_uid = get_uid_for_csid(app, advisor_csid) if (not advisor_uid and advisor_csid) else advisor_uid benchmark('begin local appointments query') appointments_feed = Appointment.search( search_phrase=search_phrase, advisor_uid=advisor_uid, student_csid=student_csid, topic=topic, datetime_from=datetime_from, datetime_to=datetime_to, limit=limit, offset=offset, ) benchmark('end local appointments query') local_appointments_count = len(appointments_feed) if local_appointments_count == limit: return appointments_feed benchmark('begin loch appointments query') loch_results = data_loch.search_advising_appointments( search_phrase=search_phrase, advisor_uid=advisor_uid, advisor_csid=advisor_csid, student_csid=student_csid, topic=topic, datetime_from=datetime_from, datetime_to=datetime_to, offset=max(0, offset - local_appointments_count), limit=(limit - local_appointments_count), ) benchmark('end loch appointments query') benchmark('begin loch appointments parsing') appointments_feed += _get_loch_appointments_search_results(loch_results, search_terms) benchmark('end loch appointments parsing') return appointments_feed
def reserve_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 has_privilege and appointment.status in ('reserved', 'waiting'): appointment = Appointment.reserve( appointment_id=appointment_id, reserved_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) else: raise ForbiddenRequestError( f'You are unauthorized to manage appointment {appointment_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 not in _dept_codes_with_scheduler_privilege(): raise ForbiddenRequestError(f'You are unauthorized to manage {appointment.dept_code} appointments.') if not appointment.status_change_available(): raise BadRequestError(appointment.to_api_json(current_user.get_id())) params = request.get_json() advisor_attrs = _advisor_attrs_for_uid(params.get('advisorUid')) if not advisor_attrs: raise BadRequestError('Appointment reservation requires valid "advisorUid"') Appointment.check_in( appointment_id=appointment_id, checked_in_by=current_user.get_id(), advisor_attrs=advisor_attrs, ) return Response(status=200)
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 cancel_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.') if not appointment.status_change_available(): raise BadRequestError(appointment.to_api_json(current_user.get_id())) params = request.get_json() cancel_reason = params.get('cancelReason', None) cancel_reason_explained = params.get('cancelReasonExplained', None) Appointment.cancel( appointment_id=appointment_id, cancelled_by=current_user.get_id(), cancel_reason=cancel_reason, cancel_reason_explained=cancel_reason_explained, ) return Response(status=200)
def _appointments_search(search_phrase, params): appointment_options = util.get(params, 'appointmentOptions', {}) advisor_uid = appointment_options.get('advisorUid') advisor_csid = appointment_options.get('advisorCsid') student_csid = appointment_options.get('studentCsid') topic = appointment_options.get('topic') limit = int(util.get(appointment_options, 'limit', 20)) offset = int(util.get(appointment_options, 'offset', 0)) date_from = appointment_options.get('dateFrom') date_to = appointment_options.get('dateTo') if not len(search_phrase) and not (advisor_uid or advisor_csid or student_csid or topic or date_from or date_to): raise BadRequestError('Invalid or empty search input') if advisor_csid and not advisor_uid: advisor_uid = get_uid_for_csid(app, advisor_csid) if date_from: try: datetime_from = util.localized_timestamp_to_utc( f'{date_from}T00:00:00') except ValueError: raise BadRequestError('Invalid dateFrom value') else: datetime_from = None if date_to: try: datetime_to = util.localized_timestamp_to_utc( f'{date_to}T00:00:00') + timedelta(days=1) except ValueError: raise BadRequestError('Invalid dateTo value') else: datetime_to = None if datetime_from and datetime_to and datetime_to <= datetime_from: raise BadRequestError('dateFrom must be less than dateTo') appointment_results = Appointment.search( search_phrase=search_phrase, advisor_uid=advisor_uid, student_csid=student_csid, topic=topic, datetime_from=datetime_from, datetime_to=datetime_to, offset=offset, limit=limit, ) return { 'appointments': appointment_results, }
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 get_non_legacy_advising_appointments(sid): appointments_by_id = {} for row in Appointment.get_appointments_per_sid(sid): appointment = row.__dict__ appointment_id = appointment['id'] event = appointment_event_to_json(appointment_id, row.status) appointments_by_id[str(appointment_id)] = appointment_to_compatible_json( appointment=appointment, topics=[t.to_api_json() for t in row.topics if not t.deleted_at], event=event, ) return appointments_by_id
def cancel_appointment(appointment_id): appointment = Appointment.find_by_id(appointment_id) if not appointment: raise ResourceNotFoundError('Unknown path') if current_user.is_admin or appointment.dept_code in _dept_codes_with_scheduler_privilege( ): params = request.get_json() cancel_reason = params.get('cancelReason', None) cancel_reason_explained = params.get('cancelReasonExplained', None) appointment = Appointment.cancel( appointment_id=appointment_id, canceled_by=current_user.get_id(), cancel_reason=cancel_reason, cancel_reason_explained=cancel_reason_explained, ) 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 test_steal_appointment_reservation(self, app, client, fake_auth): """Reserve an appointment that another advisor has reserved.""" dept_code = 'COENG' advisor_1 = DropInAdvisor.advisors_for_dept_code(dept_code)[0] user_1 = AuthorizedUser.find_by_id(advisor_1.authorized_user_id) fake_auth.login(user_1.uid) waiting = AppointmentTestUtil.create_appointment(client, dept_code) appointment = self._reserve_appointment(client, waiting['id']) assert appointment['status'] == 'reserved' assert appointment['statusDate'] is not None assert appointment['statusBy']['id'] == user_1.id client.get('/api/auth/logout') # Another advisor comes along... advisor_2 = DropInAdvisor.advisors_for_dept_code(dept_code)[1] user_2 = AuthorizedUser.find_by_id(advisor_2.authorized_user_id) fake_auth.login(user_2.uid) appointment = self._reserve_appointment(client, waiting['id']) assert appointment['status'] == 'reserved' assert appointment['statusDate'] is not None assert appointment['statusBy']['id'] == user_2.id # Clean up Appointment.delete(appointment['id'])
def test_create_appointment_as_coe_scheduler(self, client, fake_auth): """Scheduler can create appointments.""" fake_auth.login(coe_scheduler_uid) details = 'Aloysius has some questions.' appointment = AppointmentTestUtil.create_appointment( client, 'COENG', details) appointment_id = appointment['id'] waitlist = self._get_waitlist(client, 'COENG') matching = next((a for a in waitlist if a['details'] == details), None) assert matching assert appointment_id == matching['id'] assert appointment['read'] is True assert appointment['status'] == 'waiting' assert appointment['student']['sid'] == '3456789012' assert appointment['student']['name'] == 'Paul Kerschen' assert appointment['student']['photoUrl'] assert appointment['appointmentType'] == 'Drop-in' assert len(appointment['topics']) == 2 # Verify that a deleted appointment is off the waitlist Appointment.delete(appointment_id) waitlist = self._get_waitlist(client, 'COENG') assert next( (a for a in waitlist if a['details'] == details), None) is None
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 has_privilege and appointment.status == 'reserved': event = AppointmentEvent.get_most_recent_per_type( appointment.id, 'reserved') if event.user_id == current_user.get_id(): appointment = Appointment.unreserve( appointment_id=appointment_id, unreserved_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) else: raise ForbiddenRequestError( f'You did not reserve appointment {appointment_id}.') else: raise ForbiddenRequestError( f'You are unauthorized to manage appointment {appointment_id}.')
def find_appointment_advisors_by_name(): query = request.args.get('q') if not query: raise BadRequestError('Search query must be supplied') limit = request.args.get('limit') query_fragments = filter(None, query.upper().split(' ')) advisors = Appointment.find_advisors_by_name(query_fragments, limit=limit) def _advisor_feed(a): return { 'label': a.advisor_name, 'uid': a.advisor_uid, } return tolerant_jsonify([_advisor_feed(a) for a in advisors])
def test_search_respects_date_filters(self, app, coe_advisor, client): """Search results include appointments created within provided date range.""" from boac import std_commit appointment = Appointment.find_by_id(2) appointment.created_at = util.localized_timestamp_to_utc( '2017-10-31T00:00:00') std_commit(allow_test_environment=True) api_json = _api_search( client, 'pick me', appointments=True, appointment_options={ 'dateFrom': '2017-10-31', 'dateTo': '2017-11-01', }, ) self._assert(api_json, appointment_count=1)
def test_appointment_search_with_no_input_and_date(self, coe_advisor, client): """Appointments search needs no input when date options are set.""" from boac import db, std_commit appointment = Appointment.find_by_id(2) appointment.created_at = util.localized_timestamp_to_utc( '2017-11-01T00:00:00') std_commit() db.session.refresh(appointment) api_json = _api_search( client, '', appointments=True, appointment_options={ 'dateFrom': '2017-11-01', 'dateTo': '2017-11-02' }, ) self._assert(api_json, appointment_count=3)
def get_today_scheduled_appointments(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(): local_today = localize_datetime(utc_now()) advisor_uid = request.args.get('advisorUid') scheduled_for_today = Appointment.get_scheduled(dept_code, local_today, advisor_uid) appointments = [a.to_api_json(current_user.get_id()) for a in scheduled_for_today] openings = AppointmentAvailability.get_openings(dept_code, local_today, appointments) _put_student_profile_per_appointment(appointments) return tolerant_jsonify({ 'appointments': appointments, 'openings': openings, }) else: raise ForbiddenRequestError(f'You are unauthorized to manage {dept_code} appointments.')