def test_search_advising_notes_narrowed_by_author(self, app, fake_auth): """Narrows results for both new and legacy advising notes by author SID.""" joni = { 'name': 'Joni Mitchell', 'uid': '1133399', 'sid': '800700600', } not_joni = { 'name': 'Oliver Heyer', 'uid': '2040', } for author in [joni, not_joni]: Note.create( author_uid=author['uid'], author_name=author['name'], author_role='Advisor', author_dept_codes='COENG', sid='11667051', subject='Futher on France', body='Brigitte has been molded to middle class circumstance', ) fake_auth.login(coe_advisor) wide_response = search_advising_notes(search_phrase='Brigitte') assert len(wide_response) == 4 narrow_response = search_advising_notes(search_phrase='Brigitte', author_csid=joni['sid']) assert len(narrow_response) == 2 new_note, legacy_note = narrow_response[0], narrow_response[1] assert new_note['advisorUid'] == joni['uid'] assert legacy_note['advisorSid'] == joni['sid']
def test_search_advising_notes_paginates_new_and_old(self, app, fake_auth): fake_auth.login(coe_advisor) for i in range(0, 5): Note.create( author_uid=coe_advisor, author_name='Balloon Man', author_role='Spherical', author_dept_codes='COENG', sid='11667051', subject='Planned redundancy', body=f'Confounded note {i + 1}', ) response = search_advising_notes(search_phrase='confound', offset=0, limit=4) assert len(response) == 4 assert response[0][ 'noteSnippet'] == 'Planned redundancy - <strong>Confounded</strong> note 1' assert response[1][ 'noteSnippet'] == 'Planned redundancy - <strong>Confounded</strong> note 2' assert response[2][ 'noteSnippet'] == 'Planned redundancy - <strong>Confounded</strong> note 3' assert response[3][ 'noteSnippet'] == 'Planned redundancy - <strong>Confounded</strong> note 4' response = search_advising_notes(search_phrase='confound', offset=4, limit=4) assert len(response) == 3 assert response[0][ 'noteSnippet'] == 'Planned redundancy - <strong>Confounded</strong> note 5' assert response[1]['noteSnippet'].startswith( 'I am <strong>confounded</strong>') assert response[2]['noteSnippet'].startswith('...pity the founder')
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 mock_advising_note(app, db): """Create advising note with attachment (mock s3).""" with mock_advising_note_s3_bucket(app): note_author_uid = '90412' base_dir = app.config['BASE_DIR'] path_to_file = f'{base_dir}/fixtures/mock_advising_note_attachment_1.txt' with open(path_to_file, 'r') as file: note = Note.create( author_uid=note_author_uid, author_name='Joni Mitchell', author_role='Director', author_dept_codes=['UWASC'], sid='11667051', subject='In France they kiss on main street', body=""" My darling dime store thief, in the War of Independence Rock 'n Roll rang sweet as victory, under neon signs """, attachments=[ { 'name': path_to_file.rsplit('/', 1)[-1], 'byte_stream': file.read(), }, ], ) db.session.add(note) std_commit(allow_test_environment=True) yield note Note.delete(note_id=note.id) std_commit(allow_test_environment=True)
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 create_notes(): benchmark = get_benchmarker('create_notes') params = request.form sids = _get_sids_for_note_creation() benchmark(f'SID count: {len(sids)}') body = params.get('body', None) is_private = to_bool_or_none(params.get('isPrivate', False)) subject = params.get('subject', None) topics = get_note_topics_from_http_post() if not sids or not subject: benchmark('end (BadRequest)') raise BadRequestError( 'Note creation requires \'subject\' and \'sids\'') dept_codes = dept_codes_where_advising(current_user) if current_user.is_admin or not len(dept_codes): benchmark('end (Forbidden)') raise ForbiddenRequestError( 'Sorry, only advisors can create advising notes') if is_private and not current_user.can_access_private_notes: benchmark('end (Forbidden)') raise ForbiddenRequestError( 'Sorry, you are not authorized to manage note privacy.') attachments = get_note_attachments_from_http_post(tolerate_none=True) benchmark(f'Attachment count: {len(attachments)}') body = process_input_from_rich_text_editor(body) template_attachment_ids = get_template_attachment_ids_from_http_post() if len(sids) == 1: note = Note.create( **_get_author_profile(), attachments=attachments, body=body, is_private=is_private, sid=sids[0], subject=subject, template_attachment_ids=template_attachment_ids, topics=topics, ) response = tolerant_jsonify( _boa_note_to_compatible_json(note, note_read=True)) else: response = tolerant_jsonify( Note.create_batch( **_get_author_profile(), attachments=attachments, author_id=current_user.to_api_json()['id'], body=body, is_private=is_private, sids=sids, subject=subject, template_attachment_ids=template_attachment_ids, topics=topics, ), ) benchmark('end') return response
def test_admin_delete(self, client, fake_auth, mock_coe_advising_note): """Admin can delete another user's note.""" original_count_per_sid = len(Note.get_notes_by_sid(mock_coe_advising_note.sid)) fake_auth.login(admin_uid) note_id = mock_coe_advising_note.id response = client.delete(f'/api/notes/delete/{note_id}') assert response.status_code == 200 assert not Note.find_by_id(note_id) assert 1 == original_count_per_sid - len(Note.get_notes_by_sid(mock_coe_advising_note.sid)) assert not Note.update(note_id=note_id, subject='Deleted note cannot be updated')
def batch_create_notes(): params = request.form sids = _get_sids_for_note_creation() subject = params.get('subject', None) body = params.get('body', None) topics = get_note_topics_from_http_post() if not sids or not subject: raise BadRequestError('Note creation requires \'subject\' and \'sid\'') user_dept_codes = dept_codes_where_advising(current_user) if current_user.is_admin or not len(user_dept_codes): raise ForbiddenRequestError( 'Sorry, only advisors can create advising notes') author_profile = _get_author_profile() attachments = get_note_attachments_from_http_post(tolerate_none=True) note_ids_per_sid = Note.create_batch( author_id=current_user.to_api_json()['id'], **author_profile, subject=subject, body=process_input_from_rich_text_editor(body), topics=topics, sids=sids, attachments=attachments, template_attachment_ids=get_template_attachment_ids_from_http_post(), ) return tolerant_jsonify(note_ids_per_sid)
def create_note(): params = request.form sid = params.get('sid', None) subject = params.get('subject', None) body = params.get('body', None) topics = get_note_topics_from_http_post() if not sid or not subject: raise BadRequestError('Note creation requires \'subject\' and \'sid\'') user_dept_codes = dept_codes_where_advising(current_user) if current_user.is_admin or not len(user_dept_codes): raise ForbiddenRequestError( 'Sorry, only advisors can create advising notes.') author_profile = _get_author_profile() attachments = get_note_attachments_from_http_post(tolerate_none=True) note = Note.create( **author_profile, subject=subject, body=process_input_from_rich_text_editor(body), topics=topics, sid=sid, attachments=attachments, template_attachment_ids=get_template_attachment_ids_from_http_post(), ) 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 remove_attachment(note_id, attachment_id): existing_note = Note.find_by_id(note_id=note_id) if not existing_note: raise BadRequestError('Note id not found.') if existing_note.author_uid != current_user.get_uid() and not current_user.is_admin: raise ForbiddenRequestError('You are not authorized to remove attachments from this note.') note = Note.delete_attachment( note_id=note_id, attachment_id=int(attachment_id), ) return tolerant_jsonify( _boa_note_to_compatible_json( note=note, note_read=NoteRead.find_or_create(current_user.get_id(), note_id), ), )
def test_unauthorized(self, client, fake_auth, mock_coe_advising_note): """Advisor cannot delete the note of another.""" fake_auth.login('6446') response = client.delete( f'/api/notes/delete/{mock_coe_advising_note.id}') assert response.status_code == 403 assert Note.find_by_id(mock_coe_advising_note.id)
def test_search_new_advising_notes_narrowed_by_date(self, app, fake_auth): today = datetime.now().replace( hour=0, minute=0, second=0, tzinfo=pytz.timezone(app.config['TIMEZONE'])).astimezone(pytz.utc) yesterday = today - timedelta(days=1) tomorrow = today + timedelta(days=1) fake_auth.login(coe_advisor) Note.create( author_uid=coe_advisor, author_name='Balloon Man', author_role='Spherical', author_dept_codes='COENG', sid='11667051', subject='Bryant Park', body='There were loads of them', ) assert len(search_advising_notes(search_phrase='Bryant')) == 1 assert len( search_advising_notes(search_phrase='Bryant', datetime_from=yesterday)) == 1 assert len( search_advising_notes(search_phrase='Bryant', datetime_to=yesterday)) == 0 assert len( search_advising_notes(search_phrase='Bryant', datetime_from=yesterday, datetime_to=yesterday)) == 0 assert len( search_advising_notes(search_phrase='Bryant', datetime_from=tomorrow)) == 0 assert len( search_advising_notes(search_phrase='Bryant', datetime_to=tomorrow)) == 1 assert len( search_advising_notes(search_phrase='Bryant', datetime_from=tomorrow, datetime_to=tomorrow)) == 0 assert len( search_advising_notes(search_phrase='Bryant', datetime_from=yesterday, datetime_to=tomorrow)) == 1
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 test_advisor_cannot_delete(self, client, fake_auth, mock_coe_advising_note): """Advisor cannot delete her own note.""" fake_auth.login(mock_coe_advising_note.author_uid) response = client.delete( f'/api/notes/delete/{mock_coe_advising_note.id}') assert response.status_code == 403 assert Note.find_by_id(mock_coe_advising_note.id)
def test_user_without_advising_data_access(self, client, fake_auth, mock_coe_advising_note): """Denies access to a user who cannot access notes and appointments.""" fake_auth.login(coe_advisor_no_advising_data_uid) response = client.delete( f'/api/notes/delete/{mock_coe_advising_note.id}') assert response.status_code == 401 assert Note.find_by_id(mock_coe_advising_note.id)
def update_note(): params = request.form note_id = params.get('id', None) subject = params.get('subject', None) body = params.get('body', None) topics = get_note_topics_from_http_post() if not note_id or not subject: raise BadRequestError('Note requires \'id\' and \'subject\'') if Note.find_by_id(note_id=note_id).author_uid != current_user.get_uid(): raise ForbiddenRequestError('Sorry, you are not the author of this note.') note = Note.update( note_id=note_id, subject=subject, body=process_input_from_rich_text_editor(body), 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 test_search_advising_notes_narrowed_by_topic(self, app, fake_auth): for topic in ['Good Show', 'Bad Show']: Note.create( author_uid='1133399', author_name='Joni Mitchell', author_role='Advisor', author_dept_codes='COENG', sid='11667051', topics=[topic], subject='Brigitte', body='', ) fake_auth.login(coe_advisor) wide_response = search_advising_notes(search_phrase='Brigitte') assert len(wide_response) == 4 narrow_response = search_advising_notes(search_phrase='Brigitte', topic='Good Show') assert len(narrow_response) == 2
def add_attachments(note_id): note = Note.find_by_id(note_id=note_id) if note.author_uid != current_user.get_uid(): raise ForbiddenRequestError('Sorry, you are not the author of this note.') attachments = get_note_attachments_from_http_post() attachment_limit = app.config['NOTES_ATTACHMENTS_MAX_PER_NOTE'] if len(attachments) + len(note.attachments) > attachment_limit: raise BadRequestError(f'No more than {attachment_limit} attachments may be uploaded at once.') for attachment in attachments: note = Note.add_attachment( note_id=note_id, attachment=attachment, ) return tolerant_jsonify( _boa_note_to_compatible_json( note=note, note_read=NoteRead.find_or_create(current_user.get_id(), note_id), ), )
def get_non_legacy_advising_notes(sid): notes_by_id = {} for note in [n.to_api_json() for n in Note.get_notes_by_sid(sid)]: note_id = note['id'] notes_by_id[str(note_id)] = note_to_compatible_json( note=note, attachments=note.get('attachments'), topics=note.get('topics'), ) return notes_by_id
def test_search_advising_notes_includes_newly_created( self, app, fake_auth): fake_auth.login(coe_advisor) Note.create( author_uid=coe_advisor, author_name='Balloon Man', author_role='Spherical', author_dept_codes='COENG', sid='11667051', subject='Confound this note', body='and its successors and assigns', ) response = search_advising_notes(search_phrase='confound') assert len(response) == 3 assert response[0][ 'noteSnippet'] == '<strong>Confound</strong> this note - and its successors and assigns' assert response[1]['noteSnippet'].startswith( 'I am <strong>confounded</strong>') assert response[2]['noteSnippet'].startswith('...pity the founder')
def mock_coe_advising_note(): return Note.create( author_uid=coe_advisor_uid, author_name='Balloon Man', author_role='Spherical', author_dept_codes='COENG', sid=coe_student['sid'], subject='I was walking up Sixth Avenue', body='He spattered me with tomatoes, Hummus, chick peas', )
def add_attachment(note_id): if Note.find_by_id(note_id=note_id).author_uid != current_user.get_uid(): raise ForbiddenRequestError('Sorry, you are not the author of this note.') attachments = _get_attachments(request.files) if len(attachments) != 1: raise BadRequestError('A single attachment file must be supplied.') note = Note.add_attachment( note_id=note_id, attachment=attachments[0], ) note_json = note.to_api_json() return tolerant_jsonify( note_to_compatible_json( note=note_json, note_read=NoteRead.find_or_create(current_user.get_id(), note_id), attachments=note_json.get('attachments'), topics=note_json.get('topics'), ), )
def _create_coe_advisor_note( sid, subject, body='', topics=(), author_uid=coe_advisor, author_name='Balloon Man', author_role='Spherical', author_dept_codes='COENG', ): Note.create( author_uid=author_uid, author_name=author_name, author_role=author_role, author_dept_codes=author_dept_codes, topics=topics, sid=sid, subject=subject, body=body, )
def test_search_advising_notes_narrowed_by_student(self, app, fake_auth): """Narrows results for both new and legacy advising notes by student SID.""" for sid in ['9100000000', '9100000001']: Note.create( author_uid='1133399', author_name='Joni Mitchell', author_role='Advisor', author_dept_codes='COENG', sid=sid, subject='Case load', body='Another day, another student', ) fake_auth.login(coe_advisor) wide_response = search_advising_notes(search_phrase='student') assert len(wide_response) == 5 narrow_response = search_advising_notes(search_phrase='student', student_csid='9100000000') assert len(narrow_response) == 2 new_note, legacy_note = narrow_response[0], narrow_response[1] assert new_note['studentSid'] == '9100000000' assert legacy_note['studentSid'] == '9100000000'
def test_unauthorized_update_note(self, app, client, fake_auth, mock_coe_advising_note): """Deny user's attempt to edit someone else's note.""" original_subject = mock_coe_advising_note.subject fake_auth.login(asc_advisor_uid) assert self._api_note_update( app, client, note_id=mock_coe_advising_note.id, subject='Hack someone else\'s subject!', body='Hack someone else\'s body!', expected_status_code=403, ) assert Note.find_by_id(note_id=mock_coe_advising_note.id).subject == original_subject
def get_non_legacy_advising_notes(sid): notes_by_id = {} for row in Note.get_notes_by_sid(sid): note = row.__dict__ note_id = note['id'] notes_by_id[str(note_id)] = note_to_compatible_json( note=note, attachments=[ a.to_api_json() for a in row.attachments if not a.deleted_at ], topics=[t.to_api_json() for t in row.topics if not t.deleted_at], ) return notes_by_id
def update_note(): params = request.form note_id = params.get('id', None) subject = params.get('subject', None) body = params.get('body', None) topics = _get_topics(params) 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_] if not note_id or not subject: raise BadRequestError('Note requires \'id\' and \'subject\'') if Note.find_by_id(note_id=note_id).author_uid != current_user.get_uid(): raise ForbiddenRequestError('Sorry, you are not the author of this note.') note = Note.update( note_id=note_id, subject=subject, body=process_input_from_rich_text_editor(body), topics=topics, attachments=_get_attachments(request.files, tolerate_none=True), delete_attachment_ids=delete_attachment_ids, ) 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 mock_asc_advising_note(app, db): return Note.create( author_uid='1133399', author_name='Roberta Joan Anderson', author_role='Advisor', author_dept_codes=['COENG'], sid='3456789012', subject='The hissing of summer lawns', body=""" She could see the valley barbecues from her window sill. See the blue pools in the squinting sun. Hear the hissing of summer lawns """, topics=['darkness', 'no color no contrast'], )
def test_update_note_with_raw_url_in_body(self, app, client, fake_auth, mock_coe_advising_note): """Updates subject and body of note.""" fake_auth.login(mock_coe_advising_note.author_uid) expected_subject = 'There must have been a plague of them' body = '<p>They were <a href="http://www.guzzle.com">www.guzzle.com</a> at <b>https://marsh.mallows.com</b> and <a href="http://www.foxnews.com">FOX news</a></p>' # noqa: E501 expected_body = '<p>They were <a href="http://www.guzzle.com">www.guzzle.com</a> at <b><a href="https://marsh.mallows.com" target="_blank">https://marsh.mallows.com</a></b> and <a href="http://www.foxnews.com">FOX news</a></p>' # noqa: E501 updated_note_response = self._api_note_update( app, client, note_id=mock_coe_advising_note.id, subject=expected_subject, body=body, ) assert updated_note_response['read'] is True updated_note = Note.find_by_id(note_id=mock_coe_advising_note.id) assert updated_note.subject == expected_subject assert updated_note.body == expected_body
def test_remove_all_topics(self, app, client, fake_auth, new_coe_note, asc_advising_note): """Update a note: delete existing topic, leaving none behind.""" fake_auth.login(asc_advising_note.author_uid) expected_topics = [] updated_note_response = self._api_note_update( app, client, note_id=asc_advising_note.id, subject=asc_advising_note.subject, body=asc_advising_note.body, topics=expected_topics, ) assert updated_note_response['read'] is True assert len(updated_note_response['topics']) == 0 updated_note = Note.find_by_id(note_id=asc_advising_note.id) assert len(updated_note.topics) == 0