def test_direct_emails_in_thread(logged_in, test_client, session, admin_user, mail_functions, describe, tomorrow, make_add_reply): with describe('setup'), logged_in(admin_user): assignment = helpers.create_assignment(test_client, state='open', deadline=tomorrow) course = assignment['course'] teacher = admin_user student = helpers.create_user_with_role(session, 'Student', course) work_id = helpers.get_id( helpers.create_submission(test_client, assignment, for_user=student)) add_reply = make_add_reply(work_id) add_reply('base comment', include_response=True) with describe('Initial reply should not be send in a thread'), logged_in( student): add_reply('first reply') first_msg, = mail_functions.assert_mailed(teacher) assert 'first reply' in first_msg.msg assert student.name in first_msg.msg assert first_msg.in_reply_to is None with describe('Own replies should not be mailed'), logged_in(teacher): add_reply('own reply') mail_functions.assert_mailed(teacher, amount=0) with describe('Second reply should be send as reply to first message' ), logged_in(student): add_reply('second reply') second_msg, = mail_functions.assert_mailed(teacher) assert 'first reply' not in second_msg.msg assert 'second reply' in second_msg.msg assert student.name in second_msg.msg assert second_msg.in_reply_to == first_msg.message_id assert second_msg.references == [first_msg.message_id] with describe('Third reply should be send as reply to first message' ), logged_in(student): add_reply('third reply') third_msg, = mail_functions.assert_mailed(teacher) assert third_msg.in_reply_to == first_msg.message_id assert third_msg.references == [first_msg.message_id]
def test_getting_inline_feedback_analytics(logged_in, test_client, session, admin_user, describe, tomorrow, make_add_reply): with describe('setup'), logged_in(admin_user): assignment = helpers.create_assignment(test_client, state='open', deadline=tomorrow) course = assignment['course'] teacher = admin_user student = helpers.create_user_with_role(session, 'Student', course) work_id = helpers.get_id( helpers.create_submission(test_client, assignment, for_user=student)) add_reply = make_add_reply(work_id) base_url = ( f'/api/v1/analytics/{assignment["analytics_workspace_ids"][0]}') url = base_url + '/data_sources/inline_feedback' def test(amount): test_client.req( 'get', url, 200, result={ 'name': 'inline_feedback', 'data': { str(work_id): { 'total_amount': amount, } }, }, ) with describe('should be includes as data source'), logged_in(teacher): test_client.req('get', base_url, 200, result={ 'id': assignment['analytics_workspace_ids'][0], 'assignment_id': assignment['id'], 'student_submissions': { str(get_id(student)): [{ 'id': work_id, 'created_at': str, 'grade': None, 'assignee_id': None, }], }, 'data_sources': lambda x: 'inline_feedback' in x, }) with describe('standard no comments'), logged_in(teacher): test(0) with describe('after adding reply it should return 1'), logged_in(teacher): r1 = add_reply('A reply', line=1) test(1) with describe('after two replies on 1 line should return 1'), logged_in( teacher): r2 = add_reply('A reply on the reply', line=1) test(1) with describe('a reply on another line should return more'), logged_in( teacher): r3 = add_reply('A new line reply', line=2) r4 = add_reply('WOW MUCH REPLIES', line=1000) test(3) with describe('Updating should not change a thing'), logged_in(teacher): test(3) r4 = r4.update('wow new text') test(3) with describe('after deleting replies it should be back at 0'), logged_in( teacher): # Should begin with 3 test(3) # There is another comment on this line so it should stay the same r1.delete() test(3) r3.delete() test(2) r4.delete() test(1) r2.delete() test(0) with describe('students cannot access it'), logged_in(student): test_client.req('get', url, 403)
def test_disabling_peer_feedback( test_client, admin_user, session, describe, logged_in, yesterday, make_add_reply ): with describe('setup'), logged_in(admin_user): course = helpers.create_course(test_client) assignment = helpers.create_assignment( test_client, course, deadline=yesterday, state='open' ) teacher = helpers.create_user_with_role(session, 'Teacher', course) users = [ helpers.get_id( helpers.create_user_with_role(session, 'Student', course) ) for _ in range(5) ] user1, *other_users = users for user in users: helpers.create_submission(test_client, assignment, for_user=user) helpers.enable_peer_feedback(test_client, assignment) conns = get_all_connections(assignment, 1)[user1] base_url = f'/api/v1/assignments/{helpers.get_id(assignment)}' pf_sub = m.Work.query.filter_by( assignment_id=helpers.get_id(assignment), user_id=helpers.get_id(conns[0]), ).one() pf_url = f'{base_url}/peer_feedback_settings' add_reply = make_add_reply(pf_sub) with logged_in(user1): reply = add_reply( 'A peer feedback comment', expect_peer_feedback=True ) with describe('Students cannot disable peer feedback'), logged_in(user1): test_client.req('delete', pf_url, 403) with describe('Teachers can disable peer feedback'), logged_in(teacher): test_client.req('delete', pf_url, 204) get_all_connections(assignment, 0) with describe('Old feedback still exists after disabling' ), logged_in(teacher): test_client.req( 'get', f'/api/v1/submissions/{helpers.get_id(pf_sub)}/feedbacks/', 200, query={'with_replies': '1'}, result={ 'general': '', 'linter': {}, 'authors': [helpers.to_db_object(user1, m.User)], 'user': [{ 'id': int, '__allow_extra__': True, 'replies': [helpers.dict_without(reply, 'author')], }], } ) # Students can no longer see their peer feedback subs with describe('Student can no longer place new comments' ), logged_in(user1): add_reply( 'No peer feedback', expect_error=403, base_id=reply['comment_base_id'], ) with describe('deleting again does nothing'), logged_in(teacher): test_client.req('delete', pf_url, 204) get_all_connections(assignment, 0)
def test_giving_peer_feedback_comments( test_client, admin_user, session, describe, logged_in, yesterday, tomorrow, auto_approved, make_add_reply ): with describe('setup'), logged_in(admin_user): course = helpers.create_course(test_client) assignment = helpers.create_assignment( test_client, course, deadline=tomorrow, state='open' ) teacher = helpers.create_user_with_role(session, 'Teacher', course) users = [ helpers.get_id( helpers.create_user_with_role(session, 'Student', course) ) for _ in range(5) ] user1, *other_users = users subs_by_user = {} for user in users: helpers.create_submission(test_client, assignment, for_user=user) # Every user has two submissions subs_by_user[user] = helpers.create_submission( test_client, assignment, for_user=user ) helpers.enable_peer_feedback( test_client, assignment, amount=1, auto_approved=auto_approved ) conns = get_all_connections(assignment, 1)[user1] other_user = next(u for u in other_users if u not in conns) base_url = f'/api/v1/assignments/{helpers.get_id(assignment)}' pf_sub = subs_by_user[conns[0]] own_sub = subs_by_user[user1] add_reply = make_add_reply(pf_sub) with logged_in(teacher): teacher_rep = add_reply('Hello') base_comment_id = teacher_rep['comment_base_id'] teacher_rep.delete() with describe( 'Before deadline we have no connections (and cannot place feedback)' ), logged_in(user1): test_client.req( 'get', f'{base_url}/submissions/?latest_only', 200, result=[{ 'id': int, 'user': { 'id': user1, '__allow_extra__': True, }, '__allow_extra__': True, }] ) add_reply( 'Too early for a reply', expect_error=403, base_id=base_comment_id ) with logged_in(teacher): test_client.req( 'patch', base_url, 200, data={'deadline': yesterday.isoformat()} ) with describe( 'Will receive peer feedback submissions when getting all submissions' ), logged_in(user1): test_client.req( 'get', f'{base_url}/submissions/?latest_only&extended', 200, result=[pf_sub, own_sub], ) # Can also get older subs of a assigned user test_client.req( 'get', f'{base_url}/users/{conns[0]}/submissions/?extended', 200, result=[ pf_sub, { '__allow_extra__': True, 'user': {'id': conns[0], '__allow_extra__': True}, } ], ) with describe('Can comment on other submission'), logged_in(user1): reply = add_reply('A peer feedback comment', expect_peer_feedback=True) assert reply['approved'] == auto_approved with describe('Cannot change approval status yourself'), logged_in(user1): reply.set_approval(not auto_approved, err=403) with describe('Teacher can change approval status'), logged_in(teacher): reply = reply.set_approval(not auto_approved) with describe('Editing feedback resets approval status'): with logged_in(user1): # We don't reset it back to ``True`` if ``auto_approved`` is # ``True`` but we do set it back to ``False``. reply = reply.update('New content', approved=False) if auto_approved: # If reply is approved an ``auto_approved`` is ``True`` the # approval state should not change. with logged_in(teacher): reply.set_approval(True) with logged_in(user1): reply = reply.update('New content2', approved=True) with describe( 'Cannot place or edit peer feedback after pf deadline has expired' ): with logged_in(teacher): helpers.enable_peer_feedback( test_client, assignment, amount=1, auto_approved=auto_approved, days=0.5 ) assert get_all_connections(assignment, 1)[user1] == conns with logged_in(user1): add_reply('Another peer feedback comment', expect_error=403) reply.update('Cannot update!', err=403) with describe('Can always place peer feedback if pf time is None'): with logged_in(teacher): helpers.enable_peer_feedback( test_client, assignment, amount=1, auto_approved=auto_approved, days=None, ) with freezegun.freeze_time( DatetimeWithTimezone.utcnow() + timedelta(days=365 * 20) ), logged_in(user1): reply = reply.update('Can update way after the deadline!') with describe('Cannot add peer feedback to a non assigned sub' ), logged_in(other_user): add_reply( 'Not possible to add this', expect_error=403, base_id=reply['comment_base_id'] ) with describe('Student can see approved peer feedback after done'): with logged_in(teacher): reply = reply.set_approval(not auto_approved) test_client.req( 'get', f'/api/v1/submissions/{helpers.get_id(pf_sub)}/feedbacks/', 200, query={'with_replies': '1'}, result={ 'general': '', 'linter': {}, 'user': [{ 'id': int, '__allow_extra__': True, 'replies': [helpers.dict_without(reply, 'author'), ], }], 'authors': [helpers.to_db_object(user1, m.User)], }, ) test_client.req('patch', base_url, 200, data={'state': 'done'}) with logged_in(conns[0]): test_client.req( 'get', f'/api/v1/submissions/{helpers.get_id(pf_sub)}/feedbacks/', 200, query={'with_replies': '1'}, result={ 'general': '', 'linter': {}, 'authors': ([helpers.to_db_object(user1, m.User)] if reply['approved'] else []), 'user': ([{ 'id': int, '__allow_extra__': True, 'replies': [helpers.dict_without(reply, 'author')], }] if reply['approved'] else []), }, )
def test_send_mails(logged_in, test_client, session, admin_user, mail_functions, describe, tomorrow, make_add_reply): with describe('setup'), logged_in(admin_user): assignment = helpers.create_assignment(test_client, state='open', deadline=tomorrow) course = assignment['course'] teacher = admin_user student = helpers.create_user_with_role(session, 'Student', course) work_id = helpers.get_id( helpers.create_submission(test_client, assignment, for_user=student)) url = '/api/v1/settings/notification_settings/' add_reply = make_add_reply(work_id) add_reply('base comment', include_response=True) with describe('default should send mail'): with logged_in(teacher): test_client.req('get', url, 200, result={ 'options': [ { 'reason': 'assignee', 'explanation': str, 'value': 'direct', }, { 'reason': 'author', 'explanation': str, 'value': 'off', }, { 'reason': 'replied', 'explanation': str, 'value': 'direct', }, ], 'possible_values': ['direct', 'daily', 'weekly', 'off'], }) with logged_in(student): add_reply('first reply') msg, = mail_functions.assert_mailed(teacher) with describe('Preferences can be changed as logged in user'): with logged_in(teacher): test_client.req('patch', url, 204, data={ 'reason': 'replied', 'value': 'daily' }) with logged_in(student): add_reply('first reply') mail_functions.assert_mailed(teacher, amount=0) psef.tasks._send_daily_notifications() mail_functions.assert_mailed(teacher, amount=1) assert mail_functions.digest.called assert not mail_functions.direct.called with describe('Mails should not be send multiple times'): psef.tasks._send_daily_notifications() mail_functions.assert_mailed(teacher, amount=0) with describe('Fastest setting should be used'): m.Work.query.filter_by(id=work_id).update({ 'assigned_to': teacher.id, }) session.commit() with logged_in(teacher): for reason, value in [('replied', 'daily'), ('assignee', 'weekly')]: test_client.req('patch', url, 204, data={ 'reason': reason, 'value': value }) with logged_in(student): add_reply('first reply') psef.tasks._send_weekly_notifications() # Should not be send as a more specific is available mail_functions.assert_mailed(teacher, amount=0) # Should be send as this is the most specific enabled setting psef.tasks._send_daily_notifications() mail_functions.assert_mailed(teacher, amount=1) with describe('should not send when comment is deleted'): with logged_in(student): reply = add_reply('reply that will be deleted') with logged_in(teacher): reply.delete() psef.tasks._send_daily_notifications() mail_functions.assert_mailed(teacher, amount=0) with describe('Preferences can be changed using the mailed token'): token = re.search(r'unsubscribe/email_notifications/\?token=([^"]+)"', msg.msg).group(1) assert '"' not in token test_client.req('patch', url + '?token=' + token, 204, data={ 'reason': 'replied', 'value': 'direct' }) with logged_in(student): add_reply('third reply') mail_functions.assert_mailed(teacher, amount=1) with describe('Tokens can be reused for a few days'): test_client.req('patch', url + '?token=' + token, 204, data={ 'reason': 'assignee', 'value': 'direct' }) with freeze_time(datetime.datetime.utcnow() + datetime.timedelta(days=10)): test_client.req('patch', url + '?token=' + token, 403, data={ 'reason': 'assignee', 'value': 'direct' }) with describe('Tokens should not be random garbage'): test_client.req('patch', url + '?token=' + 'random_garbage', 403, data={ 'reason': 'assignee', 'value': 'direct' })
def test_updating_notifications(logged_in, test_client, session, admin_user, mail_functions, describe, tomorrow, make_add_reply): with describe('setup'), logged_in(admin_user): assignment = helpers.create_assignment(test_client, state='open', deadline=tomorrow) course = assignment['course'] teacher = admin_user student = helpers.create_user_with_role(session, 'Student', course) work_id = helpers.get_id( helpers.create_submission(test_client, assignment, for_user=student)) add_reply = make_add_reply(work_id) add_reply('base comment', include_response=True) with logged_in(student): r1 = add_reply('first reply') r2 = add_reply('second reply') r3 = add_reply('third reply') r4 = add_reply('fourth reply') with describe('Can get all notifications'), logged_in(teacher): notis = test_client.req('get', '/api/v1/notifications/?has_unread', 200, result={'has_unread': True}) notis = test_client.req( 'get', '/api/v1/notifications/', 200, { 'notifications': [ { 'read': False, '__allow_extra__': True, 'comment_reply': r4, }, { 'read': False, '__allow_extra__': True, 'comment_reply': r3, }, { 'read': False, '__allow_extra__': True, 'comment_reply': r2, }, { 'read': False, '__allow_extra__': True, 'comment_reply': r1, }, ] })['notifications'] with describe('Can update single notification'), logged_in(teacher): noti = notis.pop(0) noti = test_client.req('patch', f'/api/v1/notifications/{get_id(noti)}', 200, data={ 'read': True, }) notis.append(noti) print(notis) notis = test_client.req('get', '/api/v1/notifications/', 200, {'notifications': notis})['notifications'] with describe('Can update bulk notifications'), logged_in(teacher): # Update the first two notifications (those sould now become the last # two) notis += test_client.req( 'patch', f'/api/v1/notifications/', 200, data={ 'notifications': [{ 'id': n['id'], 'read': True, } for n in [notis.pop(0), notis.pop(0)]], })['notifications'] notis = test_client.req('get', '/api/v1/notifications/', 200, {'notifications': notis})['notifications'] with describe('cannot update notifications for deleted replies' ), logged_in(teacher): # Make sure the notification we are going to update is that of the # deleted reply assert notis[0]['comment_reply']['id'] == r1['id'] r1.delete() test_client.req('patch', f'/api/v1/notifications/', 404, data={ 'notifications': [{ 'id': notis[0]['id'], 'read': True }], }) test_client.req('patch', f'/api/v1/notifications/{notis[0]["id"]}', 404, data={'read': True}) with describe('Cannot update notifications of others'), logged_in(student): # Make sure this not the notification of the deleted reply assert notis[-1]['comment_reply']['id'] != r1['id'] test_client.req('patch', f'/api/v1/notifications/', 403, data={ 'notifications': [{ 'id': notis[-1]['id'], 'read': True }], }) test_client.req('patch', f'/api/v1/notifications/{notis[-1]["id"]}', 403, data={'read': True})