def test_peer_feedback_and_group_assignments(test_client, logged_in, describe, admin_user): with describe('setup'), logged_in(admin_user): course = helpers.create_course(test_client) gset = helpers.create_group_set(test_client, course, 1, 2) def enable_group(assig, *, err=False, gset_id=helpers.get_id(gset)): return test_client.req( 'patch', f'/api/v1/assignments/{helpers.get_id(assig)}', err or 200, data={'group_set_id': gset_id}) with describe('Cannot enable peer feedback on group assignment' ), logged_in(admin_user): assig = helpers.create_assignment(test_client, course) enable_group(assig) err = helpers.enable_peer_feedback(test_client, assig, err=400) assert 'This is a group assignment' in err['message'] enable_group(assig, gset_id=None) helpers.enable_peer_feedback(test_client, assig) with describe('Cannot make group assignment if peer feedback is enabled' ), logged_in(admin_user): assig = helpers.create_assignment(test_client, course) helpers.enable_peer_feedback(test_client, assig) err = enable_group(assig, err=400) assert 'This assignment has peer feedback enabled' in err['message'] url = (f'/api/v1/assignments/{helpers.get_id(assig)}' '/peer_feedback_settings') test_client.req('delete', url, 204) enable_group(assig)
def test_enabling_peer_feedback(test_client, session, describe, admin_user, logged_in): with describe('setup'), logged_in(admin_user): course = helpers.create_course(test_client) assignment = helpers.create_assignment(test_client, course, state='open') user_with_perm = helpers.create_user_with_perms( session, [CPerm.can_edit_peer_feedback_settings], course) user_without_perm = helpers.create_user_with_perms(session, [], course) with describe('User without perm cannot enable'), logged_in( user_without_perm): helpers.enable_peer_feedback(test_client, assignment, err=403) with describe('User with perm can enable'), logged_in(user_with_perm): helpers.enable_peer_feedback(test_client, assignment) with describe('Amount should be >= 1'), logged_in(user_with_perm): helpers.enable_peer_feedback(test_client, assignment, amount=-1, err=400) helpers.enable_peer_feedback(test_client, assignment, amount=0, err=400) helpers.enable_peer_feedback(test_client, assignment, amount=110) with describe('Time should be > 0'), logged_in(user_with_perm): helpers.enable_peer_feedback(test_client, assignment, days=-1, err=400) helpers.enable_peer_feedback(test_client, assignment, days=0, err=400) helpers.enable_peer_feedback(test_client, assignment, days=0.005)
def test_searching_test_student( logged_in, session, error_template, test_client, request, admin_user, ta_user, tomorrow, assignment ): with logged_in(admin_user): course = helpers.create_course(test_client) teacher = helpers.create_user_with_role(session, 'Teacher', course) actual_student_name = f'TEST_STUDENT_{str(uuid.uuid4())}' student = helpers.create_user_with_role( session, 'Student', course, name=actual_student_name ) with logged_in(teacher): assig_id = helpers.create_assignment( test_client, course, 'open', deadline=tomorrow )['id'] res = helpers.create_submission( test_client, assignment_id=assig_id, is_test_submission=True, ) test_user_id = res['user']['id'] res = test_client.req( 'get', '/api/v1/users/?q=TEST_STUDENT', 200, ) assert res for user in res: assert user['id'] != test_user_id assert student.id in set(user['id'] for user in res)
def test_peer_feedback_and_test_student( test_client, logged_in, describe, admin_user, session, tomorrow, enable_after, student_amount ): with describe('setup'), logged_in(admin_user): course = helpers.create_course(test_client) assignment = helpers.create_assignment( test_client, course, deadline=tomorrow ) if not enable_after: helpers.enable_peer_feedback(test_client, assignment) users = [ helpers.get_id( helpers.create_user_with_role(session, 'Student', course) ) for _ in range(student_amount) ] helpers.create_submission( test_client, assignment, is_test_submission=True ) for user in users: helpers.create_submission(test_client, assignment, for_user=user) if enable_after: helpers.enable_peer_feedback(test_client, assignment) with describe('nobody should be connected to the test student' ), logged_in(admin_user): conns = get_all_connections(assignment, 1) assert len(conns) == student_amount for user in users: assert user in conns assert all(conn in users for conn in users)
def test_proxy_with_submission(test_client, logged_in, describe, session, admin_user): with describe('setup'), logged_in(admin_user): course = helpers.create_course(test_client) assig = helpers.create_assignment(test_client, course, state='open', deadline='tomorrow') stud = helpers.create_user_with_role(session, 'Student', [course]) stud2 = helpers.create_user_with_role(session, 'Student', [course]) sub_data = ( (f'{os.path.dirname(__file__)}/../test_data/test_submissions/' 'html.tar.gz'), 'f.tar.gz', ) s1 = helpers.create_submission(test_client, assig, submission_data=sub_data, for_user=stud)['id'] s2 = helpers.create_submission(test_client, assig, submission_data=sub_data, for_user=stud2)['id'] data = { 'allow_remote_resources': True, 'allow_remote_scripts': True, 'teacher_revision': False, } with describe('Students can only create proxy when they may see files' ), logged_in(stud): test_client.req('post', f'/api/v1/submissions/{s1}/proxy', 200, data=data) test_client.req('post', f'/api/v1/submissions/{s2}/proxy', 403, data=data) # Student has no permission for teacher files data['teacher_revision'] = True test_client.req('post', f'/api/v1/submissions/{s1}/proxy', 403, data=data) data['teacher_revision'] = False with describe('remote_scripts and remote_resources connection'), logged_in( stud): data['allow_remote_resources'] = False err = test_client.req('post', f'/api/v1/submissions/{s1}/proxy', 400, data=data) assert 'The value "allow_remote_scripts" can only be true' in err[ 'message']
def test_getting_peer_feedback_connections(test_client, admin_user, session, describe, logged_in, yesterday, tomorrow): 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, user2, *_ = users for user in users: helpers.create_submission(test_client, assignment, for_user=user) helpers.enable_peer_feedback(test_client, assignment, amount=1) url = (f'/api/v1/assignments/{helpers.get_id(assignment)}/users' f'/{helpers.get_id(user1)}/peer_feedback_subjects/') conns = get_all_connections(assignment, 1)[user1] with describe('Can get connections for own user'): with logged_in(user1): api_conns = test_client.req( 'get', url, 200, result=[{ 'peer': helpers.to_db_object(user1, m.User), 'subject': helpers.to_db_object(conns[0], m.User), }]) with describe('Cannot get connections for other user as student'): with logged_in(user2): test_client.req('get', url, 403) with describe('Can get connections for other user as teacher'): with logged_in(teacher): test_client.req('get', url, 200, result=api_conns) with describe( 'No connections if assignments deadline has not expired just yet'): with logged_in(teacher): test_client.req( 'patch', f'/api/v1/assignments/{helpers.get_id(assignment)}', 200, data={'deadline': tomorrow.isoformat()}) with logged_in(user1): test_client.req('get', url, 200, result=[]) # Not even as teacher with logged_in(teacher): test_client.req('get', url, 200, result=[])
def basic(logged_in, admin_user, test_client, session): print(m.User.query.all()) with logged_in(admin_user): course = helpers.create_course(test_client) assig_id = helpers.create_assignment(test_client, course, deadline=datetime.utcnow() + timedelta(days=30))['id'] teacher = helpers.create_user_with_role(session, 'Teacher', [course]) student = helpers.create_user_with_role(session, 'Student', [course]) yield course, assig_id, teacher, student
def test_division_larger_assignment( test_client, admin_user, session, describe, logged_in, yesterday, students ): with describe('setup'), logged_in(admin_user): course = helpers.create_course(test_client) assignment = helpers.create_assignment( test_client, course, deadline=yesterday ) all_users = [ helpers.get_id( helpers.create_user_with_role(session, 'Student', course) ) for _ in range(students) ] user1, *rest_users = all_users rest_subs = [( user, helpers.create_submission(test_client, assignment, for_user=user) ) for user in rest_users] with describe('Enabling peer feedback should do divide' ), logged_in(admin_user): helpers.enable_peer_feedback(test_client, assignment, amount=3) conns = get_all_connections(assignment, 3) assert len(conns) == len(rest_users) with describe('Submitting should still be possible' ), logged_in(admin_user): helpers.create_submission(test_client, assignment, for_user=user1) conns = get_all_connections(assignment, 3) assert len(conns) == len(all_users) assert len(conns[user1]) == 3 with describe('Can delete all rest users'), logged_in(admin_user): warning_amount = 0 for idx, (user_id, sub) in enumerate(rest_subs): _, rv = test_client.req( 'delete', f'/api/v1/submissions/{helpers.get_id(sub)}', 204, include_response=True, ) had_warning = 'warning' in rv.headers if had_warning: warning_amount += 1 print('Deleting submission of', user_id, 'warning:', had_warning) conns = get_all_connections(assignment, 3) left = len(all_users) - (idx + 1) if left > 3: assert len(conns) == left, f'Got wrong amount of conns {conns}' else: assert len(conns) == 0, f'Got wrong amount of conns {conns}' assert warning_amount < len(all_users)
def test_has_permission_filter(describe, test_client, session, admin_user, logged_in, perm): with describe('setup'), logged_in(admin_user): course = helpers.to_db_object(helpers.create_course(test_client), m.Course) rol1 = m.CourseRole('rol1', course, hidden=False) rol2 = m.CourseRole('rol2', course, hidden=False) session.add(rol1) session.add(rol2) session.commit() user1 = helpers.create_user_with_role(session, 'rol1', course) user2 = helpers.create_user_with_role(session, 'rol2', course) rol1.set_permission(perm, True) rol2.set_permission(perm, False) session.commit() with describe('Role is include if (and only if) the permission is true'): roles = m.CourseRole.get_roles_with_permission(perm).all() assert rol1 in roles assert rol2 not in roles with describe('Can be filtered further'): roles = m.CourseRole.get_roles_with_permission(perm).filter( m.CourseRole.course == course).all() assert all(r.course_id == course.id for r in roles) assert 'rol1' in [r.name for r in roles] assert 'Teacher' in [r.name for r in roles] with describe('User show up if permission is true not otherwise'): query = course.get_all_users_in_course( include_test_students=False, with_permission=perm, ) assert sorted([user1.id, admin_user.id]) == sorted(user.id for user, _ in query)
def test_get_self_from_course( describe, admin_user, test_client, session, app, logged_in ): with describe('setup'), logged_in(admin_user): course = helpers.to_db_object( helpers.create_course(test_client), m.Course ) lti_course = helpers.to_db_object( helpers.create_lti_course(session, app, admin_user), m.Course ) with describe('Getting without course should return none'): assert m.LTI1p1Provider._get_self_from_course(None) is None with describe('Getting for non LTI course should return none'): assert m.LTI1p1Provider._get_self_from_course(course) is None with describe('Getting with LTI course should return instance'): prov = m.LTI1p1Provider._get_self_from_course(lti_course) assert prov is not None assert lti_course.lti_provider.id == prov.id with describe('Getting for for other LTI class should return none'): assert m.LTI1p3Provider._get_self_from_course(lti_course) is None
def basic(logged_in, admin_user, test_client, session): with logged_in(admin_user): course = helpers.create_course(test_client) assig_id = helpers.create_assignment( test_client, course, deadline=DatetimeWithTimezone.utcnow() + timedelta(days=30), state='open', )['id'] assig = m.Assignment.query.get(assig_id) assert assig is not None teacher = helpers.create_user_with_role(session, 'Teacher', [course]) student = helpers.create_user_with_role(session, 'Student', [course]) with logged_in(teacher): test_client.req( 'patch', f'/api/v1/assignments/{assig_id}', 200, data={ 'files_upload_enabled': True, 'webhook_upload_enabled': True, }, ) yield course, assig, teacher, student
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_delete_sub_with_cycle( test_client, admin_user, session, describe, logged_in, yesterday ): with describe('setup'), logged_in(admin_user): course = helpers.create_course(test_client) assignment = helpers.create_assignment( test_client, course, deadline=yesterday ) users = [ helpers.get_id( helpers.create_user_with_role(session, 'Student', course) ) for _ in range(5) ] user1, user2, user3, user4, user5 = users subs = { user: helpers.create_submission( test_client, assignment, for_user=user, ) for user in users } helpers.enable_peer_feedback(test_client, assignment, amount=1) with describe('setup cycle'): assig_id = helpers.get_id(assignment) assignment = m.Assignment.query.get(assig_id) assert assignment is not None, 'Could not find assignment' pf_settings = assignment.peer_feedback_settings pf_settings.connections = [] for reviewer, subject in [ (user1, user2), (user2, user1), (user3, user4), (user4, user5), (user5, user3), ]: reviewer = m.User.query.get(reviewer) subject = m.User.query.get(subject) conn = m.AssignmentPeerFeedbackConnection( pf_settings, user=subject, peer_user=reviewer ) assert str(reviewer.id) in repr(conn) assert str(subject.id) in repr(conn) session.commit() # Make sure the just setup connections are valid get_all_connections(assignment, 1) with describe('delete sub of user in small cycle'), logged_in(admin_user): _, rv = test_client.req( 'delete', f'/api/v1/submissions/{helpers.get_id(subs[user2])}', 204, include_response=True, ) assert ( 'All connections for peer feedback were redivided because of this' ' deletion.' ) in rv.headers['warning'] conns = get_all_connections(assignment, 1) assert set(conns[user1]) & {user3, user4, user5}
def test_getting_files_from_proxy(test_client, logged_in, describe, session, admin_user, monkeypatch): with describe('setup'), logged_in(admin_user): course = helpers.create_course(test_client) assig = helpers.create_assignment(test_client, course, state='open', deadline='tomorrow') stud = helpers.create_user_with_role(session, 'Student', [course]) sub_data = ( (f'{os.path.dirname(__file__)}/../test_data/test_submissions/' 'html.tar.gz'), 'f.tar.gz', ) s1 = helpers.create_submission(test_client, assig, submission_data=sub_data, for_user=stud)['id'] with describe('Proxy allows views without active user'): with logged_in(stud): proxy = test_client.req('post', f'/api/v1/submissions/{s1}/proxy', 200, data={ 'allow_remote_resources': True, 'allow_remote_scripts': True, 'teacher_revision': False, })['id'] res = test_client.post(f'/api/v1/proxies/{proxy}/nested/index.html', follow_redirects=False) assert res.status_code == 303 res = test_client.get(res.headers['Location']) assert res.headers['Content-Type'] == 'text/html; charset=utf-8' assert res.headers[ 'Content-Security-Policy'] == "default-src * data: 'unsafe-eval' 'unsafe-inline'" with describe('Proxy should have correct content type'): res = test_client.get(f'/api/v1/proxies/fiets.jpg') assert res.status_code == 200 assert res.headers['Content-Type'] == 'image/jpeg' with describe('Not found files should return a 404'): for f in ['nope', 'non_existing_dir/file', 'nested/nope']: res = test_client.get(f'/api/v1/proxies/{f}') assert res.status_code == 404 with describe('A directory will try to retrieve the index.html'): res1 = test_client.get(f'/api/v1/proxies/nested/index.html') assert res1.status_code == 200 res2 = test_client.get(f'/api/v1/proxies/nested') assert res2.status_code == 200 res3 = test_client.get(f'/api/v1/proxies/nested') assert res3.status_code == 200 assert len(res1.get_data()) > 1 assert res1.get_data() == res2.get_data() assert res1.get_data() == res3.get_data() with describe('A path is required'): res = test_client.get(f'/api/v1/proxies/') assert res.status_code == 404 with describe('When we find a dir 404 is returned'): monkeypatch.setattr(psef.v1.proxies, '_INDEX_FILES', ['not_in_dir.html']) res = test_client.get(f'/api/v1/proxies/nested') assert res.status_code == 404 with describe('A proxy should stop working when expired'): url = f'/api/v1/proxies/fiets.jpg' assert test_client.get(url).status_code == 200 with freeze_time(datetime.utcnow() + timedelta(days=1)): assert test_client.get(url).status_code == 400 with describe('A proxy cannot be used when the stat is deleted'): url = f'/api/v1/proxies/fiets.jpg' assert test_client.get(url).status_code == 200 psef.models.Proxy.query.filter_by(id=proxy).update({ 'state': psef.models.ProxyState.deleted, }) assert test_client.get(url).status_code == 404 psef.models.Proxy.query.filter_by(id=proxy).update({ 'state': psef.models.ProxyState.in_use, }) assert test_client.get(url).status_code == 200 with describe('We cannot reused the proxy'): assert test_client.get('/api/v1/proxies/fiets.jpg').status_code == 200 assert test_client.get( f'/api/v1/proxies/{proxy}/fiets.jpg').status_code == 200 # After clearing cookies we cannot use the endpoint anymore test_client.cookie_jar.clear() assert test_client.get('/api/v1/proxies/fiets.jpg').status_code == 400 assert test_client.get( f'/api/v1/proxies/{proxy}/fiets.jpg').status_code == 400 # Cannot do a new post to the proxy endpoint res = test_client.post(f'/api/v1/proxies/{proxy}/nested/index.html', follow_redirects=True) assert res.status_code == 404 assert test_client.get('/api/v1/proxies/fiets.jpg').status_code == 400
def test_teacher_revision_in_proxy(test_client, logged_in, describe, session, admin_user, app): with describe('setup'), logged_in(admin_user): course = helpers.create_course(test_client) assig = helpers.create_assignment(test_client, course, state='done', deadline='tomorrow') stud = helpers.create_user_with_role(session, 'Student', [course]) sub_data = ( (f'{os.path.dirname(__file__)}/../test_data/test_submissions/' 'html.tar.gz'), 'f.tar.gz', ) s1 = helpers.create_submission(test_client, assig, submission_data=sub_data, for_user=stud)['id'] files = test_client.req( 'get', f'/api/v1/submissions/{s1}/files/', 200, ) # Get the file nested/index.html student_file_id = [ n['id'] for f in files['entries'] for n in f.get('entries', []) if f['name'] == 'nested' and n['name'] == 'index.html' ][0] test_client.req( 'patch', f'/api/v1/code/{student_file_id}', 200, real_data='TEACHER FILE', ) test_client.req( 'post', f'/api/v1/submissions/{s1}/files/?path=f.tar.gz/nested/woo', 200, ) student_proxy = test_client.req('post', f'/api/v1/submissions/{s1}/proxy', 200, data={ 'allow_remote_resources': True, 'allow_remote_scripts': True, 'teacher_revision': False, })['id'] teacher_proxy = test_client.req('post', f'/api/v1/submissions/{s1}/proxy', 200, data={ 'allow_remote_resources': True, 'allow_remote_scripts': True, 'teacher_revision': True, })['id'] with describe( 'Teacher revision proxy should return teacher rev' ), app.test_client() as t_client, app.test_client() as s_client: res = t_client.post( f'/api/v1/proxies/{teacher_proxy}/nested/index.html', follow_redirects=True) assert res.status_code == 200 assert res.get_data() == b'TEACHER FILE' res = s_client.post( f'/api/v1/proxies/{student_proxy}/nested/index.html', follow_redirects=True) assert res.status_code == 200 assert res.get_data() != b'TEACHER FILE' res = t_client.get(f'/api/v1/proxies/nested/woo') assert res.status_code == 200 assert res.get_data() == b'' res = s_client.get(f'/api/v1/proxies/nested/woo') assert res.status_code == 404
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_division( test_client, admin_user, session, describe, logged_in, yesterday, iteration ): with describe('setup'), logged_in(admin_user): course = helpers.create_course(test_client) assignment = helpers.create_assignment( test_client, course, deadline=yesterday ) helpers.enable_peer_feedback(test_client, assignment) user1, user2, user3, user4 = [ helpers.get_id( helpers.create_user_with_role(session, 'Student', course) ) for _ in range(4) ] with describe('Single submission no connections'), logged_in(admin_user): assert get_all_connections(assignment, 1) == {} helpers.create_submission(test_client, assignment, for_user=user1) assert get_all_connections(assignment, 1) == {} with describe('Second submission does initial division' ), logged_in(admin_user): helpers.create_submission(test_client, assignment, for_user=user2) assert get_all_connections(assignment, 1) == {user1: [user2], user2: [user1]} with describe('Third submission also gets divided'), logged_in(admin_user): helpers.create_submission(test_client, assignment, for_user=user3) connections = get_all_connections(assignment, 1) assert len(connections) == 3 if connections[user3] == [user1]: assert connections[user2] == [user3] assert connections[user1] == [user2] else: assert connections[user3] == [user2] assert connections[user1] == [user3] assert connections[user2] == [user1] with describe('Submitting again does not change division' ), logged_in(admin_user): old_connections = get_all_connections(assignment, 1) for _ in range(10): last_sub3 = helpers.create_submission( test_client, assignment, for_user=user3 ) assert get_all_connections(assignment, 1) == old_connections with describe('Deleting not last submission does not change division' ), logged_in(admin_user): old_connections = get_all_connections(assignment, 1) _, rv = test_client.req( 'delete', f'/api/v1/submissions/{helpers.get_id(last_sub3)}', 204, include_response=True, ) assert 'warning' not in rv.headers assert get_all_connections(assignment, 1) == old_connections with describe('Test submission does not change anything' ), logged_in(admin_user): old_connections = get_all_connections(assignment, 1) for _ in range(10): helpers.create_submission( test_client, assignment, is_test_submission=True ) assert get_all_connections(assignment, 1) == old_connections with describe('user gets assigned to different user every time' ), logged_in(admin_user): conns = set() for _ in range(40): sub = helpers.create_submission( test_client, assignment, for_user=user4 ) new_conns = get_all_connections(assignment, 1)[user4] conns.add(new_conns[0]) _, rv = test_client.req( 'delete', f'/api/v1/submissions/{helpers.get_id(sub)}', 204, include_response=True, ) assert 'warning' not in rv.headers assert len(conns) == 3
def test_retrieve_users_in_course( describe, lti1p3_provider, stub_function_class, monkeypatch, test_client, admin_user, logged_in, session, app, watch_signal ): with describe('setup'), logged_in(admin_user): course = helpers.to_db_object( helpers.create_course(test_client), m.Course ) membership_url = f'http://{uuid.uuid4()}' lti_course, _ = helpers.create_lti1p3_course( test_client, session, lti1p3_provider, membership_url, ) return_value = [] stub_get = stub_function_class(lambda: copy.deepcopy(return_value)) monkeypatch.setattr( pylti1p3.names_roles.NamesRolesProvisioningService, 'get_members', stub_get ) assig_created_signal = watch_signal( # Make sure we flush the db as we expect that the created # assignment can be found by doing queries. signals.ASSIGNMENT_CREATED, flush_db=True ) user_added_signal = watch_signal( signals.USER_ADDED_TO_COURSE, clear_all_but=[m.LTI1p3Provider._retrieve_users_in_course] ) new_user_id1 = str(uuid.uuid4()) new_user_id2 = str(uuid.uuid4()) do_poll_again = True stub_poll_again = stub_function_class(lambda: do_poll_again) monkeypatch.setattr( m.CourseLTIProvider, 'can_poll_names_again', stub_poll_again ) with describe('make sure it is connected to the necessary signals'): assert signals.USER_ADDED_TO_COURSE.is_connected( m.LTI1p3Provider._retrieve_users_in_course ) assert signals.ASSIGNMENT_CREATED.is_connected( m.LTI1p3Provider._retrieve_users_in_course ) with describe('non lti courses should be ignored'): m.LTI1p3Provider._retrieve_users_in_course(course.id) assert not stub_get.called with describe('should work when no members are returned'): assig = helpers.create_lti1p3_assignment(session, lti_course) assert assig_created_signal.was_send_once assert stub_get.called assert user_added_signal.was_not_send assert stub_poll_again.called with describe('Should be possible to add members'): return_value = [ { 'status': 'Active', # Not correct at all, but the function should still not crash. 'message': object(), 'user_id': new_user_id1, 'email': '*****@*****.**', 'name': 'USER1', }, { 'status': 'Active', 'message': { claims.CUSTOM: {'cg_username_0': 'username_user2'} }, 'user_id': new_user_id2, 'email': '*****@*****.**', 'name': 'USER2', 'roles': ['Student'], }, ] signals.ASSIGNMENT_CREATED.send(assig) assert stub_poll_again.called assert user_added_signal.was_send_once # USER1 should not be added and recursion should not happen assert user_added_signal.called_amount == 1 assert m.User.query.filter_by(username='******' ).one().is_enrolled(lti_course) assert m.User.query.filter_by(email='*****@*****.**' ).one_or_none() is None with describe('Can add known users to new courses, even without username'): # Remove the message claim return_value = [{**r, 'message': {}} for r in return_value] with logged_in(admin_user): lti_course2, _ = helpers.create_lti1p3_course( test_client, session, lti1p3_provider ) helpers.create_lti1p3_assignment(session, lti_course2) assert user_added_signal.was_send_once assert m.User.query.filter_by(username='******' ).one().is_enrolled(lti_course2) assert m.User.query.filter_by(email='*****@*****.**' ).one_or_none() is None with describe('if can poll return no poll should be done'): do_poll_again = False signals.ASSIGNMENT_CREATED.send(assig) assert not stub_get.called
def test_sending_login_links( app, monkeypatch, describe, test_client, logged_in, admin_user, tomorrow, session, stub_function, error_template ): with describe('setup'), logged_in(admin_user): external_url = f'https://{uuid.uuid4()}.com' monkeypatch.setitem(app.config, 'EXTERNAL_URL', external_url) psef.site_settings.Opt.LOGIN_TOKEN_BEFORE_TIME.set_and_commit_value([ timedelta(hours=1), timedelta(minutes=5) ]) orig_send_links = psef.tasks.send_login_links_to_users stub_send_links = stub_function( psef.tasks, 'send_login_links_to_users' ) stub_function(psef.tasks, 'maybe_open_assignment_at') other_course = helpers.create_course(test_client) course = helpers.create_course(test_client) assig = helpers.create_assignment(test_client, course) url = f'/api/v1/assignments/{helpers.get_id(assig)}' test_client.req( 'patch', url, 200, data={ 'deadline': (tomorrow + timedelta(hours=1)).isoformat(), 'available_at': tomorrow.isoformat(), 'kind': 'exam', }, ) teacher = admin_user students = [ helpers.create_user_with_role( session, 'Student', [course, other_course] ) for _ in range(10) ] with logged_in(students[0]): test_client.req( 'get', '/api/v1/courses/', 200, [{'__allow_extra__': True, 'id': helpers.get_id(course)}, {'__allow_extra__': True, 'id': helpers.get_id(other_course)}] ) with describe('can send link and each user receives a unique link' ), logged_in(teacher): test_client.req('patch', url, 200, data={'send_login_links': True}) assert stub_send_links.called_amount == 2 # We safe the arguments here as the stub function will be reset at the # end of the describe block. do_send_links = [ lambda args=args, kwargs=kwargs: orig_send_links(*args, **kwargs) for args, kwargs in zip(stub_send_links.args, stub_send_links.kwargs) ] with psef.mail.mail.record_messages() as outbox: with freeze_time(tomorrow - timedelta(hours=1)): del flask.g.request_start_time do_send_links[0]() assert len(outbox) == len(students) for mail in outbox: assert len(mail.recipients) == 1 outbox_by_email = {mail.recipients[0]: mail for mail in outbox} users_by_link = {} link_ids = [] for student in students: student = student._get_current_object() mail = outbox_by_email.pop((student.name, student.email)) match = re.search( rf'a href="({external_url}/[^"]+)"', str(mail) ) link, = match.groups(1) assert link.startswith(external_url) assert link not in users_by_link users_by_link[link] = student link_id = link.split('/')[-1] link_ids.append((link_id, student)) assert not outbox_by_email, 'No extra mails should be sent' with describe('second email send the same link'), logged_in(teacher): with psef.mail.mail.record_messages() as outbox: with freeze_time(tomorrow - timedelta(minutes=1)): del flask.g.request_start_time do_send_links[1]() assert len(outbox) == len(students) for mail in outbox: assert len(mail.recipients) == 1 outbox_by_email = {mail.recipients[0]: mail for mail in outbox} for student in students: mail = outbox_by_email.pop((student.name, student.email)) match = re.search( rf'a href="({external_url}/[^"]+)"', str(mail) ) link, = match.groups(1) assert link.startswith(external_url) assert users_by_link[link] == student assert not outbox_by_email, 'No extra mails should be sent' with describe('can see link before available_at, but no login' ), freeze_time(tomorrow - timedelta(hours=1)): for link_id, student in link_ids: test_client.req( 'get', f'/api/v1/login_links/{link_id}', 200, { 'id': link_id, 'user': student, # Assignment is already visible 'assignment': { '__allow_extra__': True, 'id': helpers.get_id(assig), 'state': 'hidden', } } ) test_client.req( 'post', f'/api/v1/login_links/{link_id}/login', 409, { **error_template, 'message': re.compile('.*wait for an hour'), } ) with describe( 'can login when assignment has started, token is scoped to the course' ), freeze_time(tomorrow + timedelta(seconds=15)): for idx, (link_id, student) in enumerate(link_ids): test_client.req( 'get', f'/api/v1/login_links/{link_id}', 200, { 'id': link_id, 'user': student, # Assignment is already visible 'assignment': { '__allow_extra__': True, 'id': helpers.get_id(assig), 'state': 'hidden' if idx == 0 else 'submitting', } } ) token = test_client.req( 'post', f'/api/v1/login_links/{link_id}/login', 200, { 'user': { '__allow_extra__': True, 'id': student.id, }, 'access_token': str, } )['access_token'] # Make sure we are logged in for the correct user. headers = {'Authorization': f'Bearer {token}'} test_client.req( 'get', '/api/v1/login', 200, headers=headers, result={'id': student.id, '__allow_extra__': True}, ) test_client.req( 'get', '/api/v1/courses/', 200, headers=headers, result=[{ 'id': helpers.get_id(course), '__allow_extra__': True }], ) test_client.req( 'get', f'/api/v1/courses/{helpers.get_id(other_course)}', 403, headers=headers, ) with describe('cannot login if deadline has expired' ), freeze_time(tomorrow + timedelta(hours=2)): for idx, (link_id, student) in enumerate(link_ids): token = test_client.req( 'post', f'/api/v1/login_links/{link_id}/login', 400, { **error_template, 'message': re.compile( 'The deadline for this assignment has already' ' expired' ), 'code': 'OBJECT_EXPIRED', }, )
def test_ensure_may_see_filter(logged_in, test_client, describe, admin_user, session): with describe('setup'), logged_in(admin_user): course1 = helpers.create_course(test_client) course2 = helpers.create_course(test_client) course3 = helpers.create_course(test_client) archived_course1 = helpers.create_course(test_client) archived_course1 = m.Course.query.get(helpers.get_id(archived_course1)) archived_course1.state = m.CourseState.archived session.commit() archived_course2 = helpers.create_course(test_client) archived_course2 = m.Course.query.get(helpers.get_id(archived_course2)) archived_course2.state = m.CourseState.archived session.commit() deleted_course = helpers.create_course(test_client) deleted_course = m.Course.query.get(helpers.get_id(deleted_course)) deleted_course.state = m.CourseState.deleted session.commit() user = m.User.resolve( helpers.create_user_with_role( session, 'Student', [course1, course2, archived_course1, deleted_course])) user.courses[archived_course2.id] = m.CourseRole.query.filter_by( name='Teacher', course=archived_course2).one() with describe('Returns no courses when not logged in'): assert not m.Course.query.filter( a.CoursePermissions.ensure_may_see_filter()).all() with describe('Returns all archived course as teacher when logged in'): with a.as_current_user(admin_user): query = m.Course.query.filter( a.CoursePermissions.ensure_may_see_filter()).with_entities( m.Course.id) course_ids = sorted(c_id for c_id, in query) assert course_ids == sorted([ helpers.get_id(c) for c in [ course1, course2, course3, archived_course1, archived_course2 ] ]) with describe('Returns all courses when logged in'): with a.as_current_user(user): query = m.Course.query.filter( a.CoursePermissions.ensure_may_see_filter()).with_entities( m.Course.id) course_ids = sorted(c_id for c_id, in query) assert course_ids == sorted([ # We can see archived_course2, but not archived_course1 helpers.get_id(c) for c in [course1, course2, archived_course2] ]) with describe('Returns single courses when logged in for a single course'): with a.as_current_user( user, jwt_claims={'for_course': helpers.get_id(course2)}): course_id = m.Course.query.filter( a.CoursePermissions.ensure_may_see_filter()).with_entities( m.Course.id).all() assert len(course_id) == 1 assert course_id[0] == (helpers.get_id(course2), ) with describe( 'Returns no courses when logged in for course that the user is not' ' enrolled in'): with a.as_current_user( user, jwt_claims={'for_course': helpers.get_id(course3)}): assert not m.Course.query.filter( a.CoursePermissions.ensure_may_see_filter()).with_entities( m.Course.id).all()