def test(*args): if not isinstance(args[0], tuple): args = [(student, work_id, *args)] test_client.req('get', url, 200, result={ 'id': w_id, 'assignment_id': assignment['id'], 'student_submissions': { str(get_id(arg[0])): [{ 'id': get_id(arg[1]), 'created_at': str, 'grade': arg[2], 'assignee_id': arg[3] and get_id(arg[3]), }] for arg in args }, 'data_sources': [str, str], })
def test_delete_submission(assignment_real_works, session, monkeypatch, canvas_lti1p1_provider, stub_function_class, describe): assignment, submission = assignment_real_works passback = stub_function_class(lambda: True) monkeypatch.setattr(psef.lti.v1_1.LTI, '_passback_grade', passback) def do_delete(was_latest, new_latest=None): psef.signals.WORK_DELETED.send( psef.signals.WorkDeletedData( deleted_work=m.Work.query.get(helpers.get_id(submission)), was_latest=was_latest, new_latest=new_latest, )) with describe('deleting submission without lti should work'): do_delete(True) assert not passback.called canvas_lti1p1_provider._delete_submission( (helpers.get_id(submission), assignment.id)) assert not passback.called user_id = submission['user']['id'] assignment.assignment_results[user_id] = m.AssignmentResult( sourcedid='wow', user_id=user_id) m.CourseLTIProvider.create_and_add( lti_context_id=str(uuid.uuid4()), course=assignment.course, lti_provider=canvas_lti1p1_provider, deployment_id='', ) assignment.lti_grade_service_data = 'http://aaa' assignment.is_lti = True session.commit() with describe('deleting newest submission'): do_delete(was_latest=True) assert len(passback.all_args) == 1 assert passback.all_args[0]['grade'] is None passback.reset() with describe('deleting non newest should not delete grade'): sub_new = m.Work(user_id=user_id, assignment=assignment) session.add(sub_new) session.commit() do_delete(was_latest=False, new_latest=None) assert not passback.called with describe('deleting in non existing assignment'): canvas_lti1p1_provider._delete_submission( (helpers.get_id(submission), None)) assert not passback.called with describe('deleting in non existing submissions'): canvas_lti1p1_provider._delete_submission((-1, assignment.id)) assert not passback.called
def test_reply_to_a_comment(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) ta = helpers.create_user_with_role(session, 'TA', course) work_id = helpers.get_id( helpers.create_submission(test_client, assignment, for_user=student)) add_reply = make_add_reply(work_id) feedback_url = f'/api/v1/submissions/{work_id}/feedbacks/?with_replies' with logged_in(student): reply = add_reply('a reply') with describe('You set the in_reply_to of a comment'), logged_in(teacher): add_reply('a reply to', in_reply_to=reply) with describe('You cannot reply on a comment from a different line' ), logged_in(teacher): add_reply('a reply to', line=10, in_reply_to=reply, expect_error=404)
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 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} )
def assert_mailed(user, amount=1): if amount > 0: assert any_mails() and len(stubmailer.args) > 0, ( 'Nobody was mailed') user_id = helpers.get_id(user) user = m.User.query.get(user_id) assert user is not None, f'Given user {user_id} was not found' msgs = [] for arg, in stubmailer.args: message = arg._message() recipients = message['To'].split(', ') assert recipients, 'A mail was send to nobody' for recipient in recipients: print(recipient) if '<{}>'.format(user.email) in recipient: msgs.append( dotdict( orig=message, msg=arg.html, subject=message['Subject'], message_id=message['Message-ID'], in_reply_to=message['In-Reply-To'], references=(message['References'] and message['References'].split(' ')), )) amount -= 1 assert amount == 0, 'The given user was not mailed or mailed to much' return msgs
def get_all_connections(assignment, amount): 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 if amount == 0: assert pf_settings is None return assert pf_settings is not None, 'Not a PF assig' connections = sorted((conn.peer_user_id, conn.user_id) for conn in pf_settings.connections) seen_amount = {} res = {} for a, b in connections: assert a != b if a not in res: res[a] = [] if b not in seen_amount: seen_amount[b] = 0 seen_amount[b] += 1 assert b not in res[a] res[a].append(b) for a, conns in res.items(): assert len(conns) == amount assert seen_amount[a] == amount return res
def do_delete(was_latest, new_latest=None): psef.signals.WORK_DELETED.send( psef.signals.WorkDeletedData( deleted_work=m.Work.query.get(helpers.get_id(submission)), was_latest=was_latest, new_latest=new_latest, ))
def do_oidc_login(test_client, provider, with_id=False, redirect_to=None, override_data={}): provider = m.LTI1p3Provider.query.get(helpers.get_id(provider)) redirect_uri = str(uuid.uuid4()) login_hint = str(uuid.uuid4()) url = '/api/v1/lti1.3/login' if with_id: url += '/' + str(provider.id) redirect = test_client.post(url, data={ 'target_link_uri': redirect_uri, 'iss': provider.iss, 'client_id': provider.client_id, 'login_hint': login_hint, **override_data, }) assert redirect.status_code == 303 if redirect_to is None: assert redirect.headers['Location'].startswith( provider._auth_login_url) else: assert redirect.headers['Location'].startswith(redirect_to) return furl.furl(redirect.headers['Location'])
def slow_dfs(node, visited_nodes, max_depth): node_id = get_id(node) list_of_search_moves.append(f'{node.clicked_index} {node_id}') if node_id in visited_nodes: return False visited_nodes.append(node_id) if is_all_zeros(node.game_board): while node.parent is not None: list_of_solution_moves.append(f'{node.clicked_index} {get_id(node)}') node = node.parent return True if node.depth == max_depth: #loop until we hit parent (in case where all children were visited) while node is not None: node = node.parent #cant find non-visited child anywhere, exist if node is None: return False #get next non-visited child of parent next_node = get_next_nonvisited_child(node, visited_nodes) if next_node is not None: return slow_dfs(next_node, visited_nodes, max_depth) return False slow_build_boards(node) next_node = get_next_nonvisited_child(node, visited_nodes) if next_node is None: return False if slow_dfs(next_node, visited_nodes, max_depth): return True return False
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 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 add_reply( txt, line=0, include_response=False, include_base=False, in_reply_to=None, expect_error=False, ): in_reply_to_id = in_reply_to and get_id(in_reply_to) base = test_client.req('put', '/api/v1/comments/', 200, data={ 'file_id': code_id, 'line': line, }, result={ 'id': int, '__allow_extra__': True, }) res, rv = test_client.req( 'post', f'/api/v1/comments/{get_id(base)}/replies/', expect_error or 200, data={ 'comment': txt, 'reply_type': 'markdown', 'in_reply_to': in_reply_to_id, }, result=error_template if expect_error else { 'id': int, 'reply_type': 'markdown', 'comment': txt, 'in_reply_to_id': in_reply_to_id, 'last_edit': None, '__allow_extra__': True, }, include_response=True, ) reply = Reply(res) result = (reply, rv) if include_response else reply if include_base: base['replies'].append({**res}) base['replies'][-1].pop('author') base['replies'][-1].pop('comment_base_id') result = (result, base) return result
def test(items): test_client.req( 'get', url, 200, result={ 'name': 'rubric_data', 'data': { str(work_id): [{ 'item_id': get_id(item), 'multiplier': 1.0, } for item in items] }, }, )
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 make_launch_data(base, provider, override_replace={}): provider = m.LTI1p3Provider.query.get(helpers.get_id(provider)) return replace_values( base, { 'cg_username': '******', 'cg_deadline': '', 'cg_available_at': '', 'cg_resource_id': '', 'cg_lock_date': '', 'Assignment.name': 'Test Assignment 1', 'Course.label': 'Test Course 1', 'Course.id': str(uuid.uuid4()), 'User.id': str(uuid.uuid4()), 'User.username': '******', 'Assignment.id': str(uuid.uuid4()), 'iss': provider.iss, 'client_id': provider.client_id, **override_replace, }, )
def test_maybe_open_assignment(describe, session, test_client, logged_in, admin_user, tomorrow, stub_function): with describe('setup'), logged_in(admin_user): assig = helpers.create_assignment(test_client) stub_apply = stub_function(psef.tasks._maybe_open_assignment_at_1, 'apply_async') assig_id = helpers.get_id(assig) with describe('Can call without available_at set'): psef.tasks._maybe_open_assignment_at_1(assig_id) assert m.Assignment.query.get(assig_id).state.is_hidden assert not stub_apply.called with describe('Can call with non existant id'): psef.tasks._maybe_open_assignment_at_1(1000000000) assert not stub_apply.called with describe('Can call with non existant id'): m.Assignment.query.filter_by(id=assig_id).update( {'_available_at': tomorrow.isoformat()}) psef.tasks._maybe_open_assignment_at_1(assig_id) assert stub_apply.called_amount == 1 assert stub_apply.kwargs[0]['eta'] == tomorrow
def test_reply(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) feedback_url = f'/api/v1/submissions/{work_id}/feedbacks/?with_replies' with describe('Teachers can give feedback'), logged_in(teacher): _, rv = add_reply('base comment', include_response=True) assert 'Warning' not in rv.headers assert not mail_functions.any_mails(), ( 'Nobody should be emailed for the first reply') with describe('Students may not see feedback by default'), logged_in( student): test_client.req( 'get', feedback_url, 200, result={ 'general': '', 'linter': {}, 'authors': [], 'user': [] }, ) with describe('But students can place comments'), logged_in(student): add_reply('A reply') # The teacher should be mailed as a reply was posted mail_functions.assert_mailed(teacher, amount=1) # A direct mail should be send assert mail_functions.direct.called assert not mail_functions.digest.called with describe('Students may see own comments'), logged_in(student): test_client.req( 'get', feedback_url, 200, result={ 'general': '', 'linter': {}, 'authors': [student], 'user': [{ '__allow_extra__': True, 'replies': [{ 'comment': 'A reply', '__allow_extra__': True }], }], }, ) with describe('Teacher may see all comments'), logged_in(teacher): test_client.req( 'get', feedback_url, 200, result={ 'general': '', 'linter': {}, 'authors': [student, teacher], 'user': [{ '__allow_extra__': True, 'replies': [{ 'comment': 'base comment', '__allow_extra__': True }, { 'comment': 'A reply', '__allow_extra__': True }], }], }, ) with describe('A teacher can reply and should get a warning'), logged_in( teacher): _, rv = add_reply('another comment', include_response=True) assert 'Warning' in rv.headers assert 'have sufficient permissions' in rv.headers['Warning']
def inner(work_id): code_id = session.query(m.File.id).filter( m.File.work_id == get_id(work_id), m.File.parent_id.isnot(None), m.File.name != '__init__', ).first()[0] def add_reply(txt, line=0, include_response=False, include_base=False, in_reply_to=None, expect_error=False, expect_peer_feedback=False, base_id=None): in_reply_to_id = in_reply_to and get_id(in_reply_to) base = base_id or test_client.req('put', '/api/v1/comments/', 200, data={ 'file_id': code_id, 'line': line, }, result={ 'id': int, '__allow_extra__': True, }) res, rv = test_client.req( 'post', f'/api/v1/comments/{get_id(base)}/replies/', expect_error or 200, data={ 'comment': txt, 'reply_type': 'markdown', 'in_reply_to': in_reply_to_id, }, result=error_template if expect_error else { 'id': int, 'reply_type': 'markdown', 'comment': txt, 'in_reply_to_id': in_reply_to_id, 'last_edit': None, 'comment_base_id': get_id(base), 'comment_type': ('peer_feedback' if expect_peer_feedback else 'normal'), '__allow_extra__': True, }, include_response=True, ) if expect_error: return (res, rv) if include_response else res reply = Reply(res) result = (reply, rv) if include_response else reply if include_base: base['replies'].append({**res}) base['replies'][-1].pop('author') result = (result, base) return result return add_reply
def test_getting_analytics_workspace(logged_in, test_client, session, admin_user, describe, tomorrow): 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)) base_url = ( f'/api/v1/analytics/{assignment["analytics_workspace_ids"][0]}') url = base_url + '/data_sources/rubric_data' def test(items): test_client.req( 'get', url, 200, result={ 'name': 'rubric_data', 'data': { str(work_id): [{ 'item_id': get_id(item), 'multiplier': 1.0, } for item in items] }, }, ) with describe('by default there should be not rubric 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: 'rubric_data' not in x, }) test_client.req('get', url, 404) with describe('After adding a rubric it should have the data source' ), logged_in(teacher): rubric = test_client.req( 'put', f'/api/v1/assignments/{get_id(assignment)}/rubrics/', 200, data=RUBRIC) 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: 'rubric_data' in x, }) test([]) with describe('After selecting there are returned'), logged_in(teacher): to_select = [ get_rubric_item(rubric, 'My header', '10points'), get_rubric_item(rubric, 'My header2', '2points'), ] for idx, item in enumerate(to_select): test_client.req( 'patch', f'/api/v1/submissions/{work_id}/rubricitems/{item["id"]}', 204, ) test(to_select[:idx + 1]) with describe('Changing item in header works'), logged_in(teacher): item = get_rubric_item(rubric, 'My header', '5points') to_select[0] = item test_client.req( 'patch', f'/api/v1/submissions/{work_id}/rubricitems/{item["id"]}', 204, ) test(to_select) with describe('students cannot access it'), logged_in(student): test_client.req('get', url, 403)
def test_enabling_login_links( describe, test_client, logged_in, admin_user, yesterday, session, stub_function, app, monkeypatch, tomorrow, error_template ): with describe('setup'), logged_in(admin_user): send_mail_stub = stub_function(psef.mail, 'send_login_link_mail') assig = helpers.create_assignment(test_client) assig_id = helpers.get_id(assig) course = helpers.get_id(assig['course']) url = f'/api/v1/assignments/{assig_id}?no_course_in_assignment=t' teacher = helpers.create_user_with_perms( session, [ CPerm.can_see_assignments, CPerm.can_see_hidden_assignments, CPerm.can_view_analytics, CPerm.can_edit_assignment_info, ], course ) no_perm = helpers.create_user_with_perms( session, [ CPerm.can_see_assignments, CPerm.can_see_hidden_assignments, CPerm.can_view_analytics, ], course ) # Make sure there are users to email for _ in range(10): helpers.create_user_with_role(session, 'Student', course) with describe('cannot enable login links for wrong kind' ), logged_in(teacher): test_client.req('patch', url, 409, data={'send_login_links': True}) _, rv = test_client.req( 'patch', url, 200, data={ 'deadline': yesterday.isoformat(), 'available_at': (yesterday - timedelta(minutes=1)).isoformat(), 'kind': 'exam', 'send_login_links': True, }, result={ '__allow_extra__': True, 'send_login_links': True, }, include_response=True, ) warning = rv.headers['warning'] assert re.match(r'.*deadline.*expired.*not send', warning) assert not send_mail_stub.called with describe('cannot enable login links for wrong kind' ), logged_in(teacher): test_client.req('patch', url, 409, data={'kind': 'normal'}) assert not send_mail_stub.called with describe('cannot change login links with incorrect perms' ), logged_in(no_perm): test_client.req('patch', url, 403, data={'send_login_links': False}) assert not send_mail_stub.called with describe('Setting again does nothing'), logged_in(teacher): old_token = m.Assignment.query.get(assig_id).send_login_links_token test_client.req( 'patch', url, 200, data={ 'send_login_links': True, 'available_at': (yesterday - timedelta(minutes=1)).isoformat(), } ) new_token = m.Assignment.query.get(assig_id).send_login_links_token assert new_token == old_token assert not send_mail_stub.called with describe('Changing available at reschedules links' ), logged_in(teacher): old_token = m.Assignment.query.get(assig_id).send_login_links_token test_client.req( 'patch', url, 200, data={ 'send_login_links': True, 'available_at': (yesterday - timedelta(minutes=2)).isoformat(), } ) new_token = m.Assignment.query.get(assig_id).send_login_links_token assert new_token != old_token assert new_token is not None assert not send_mail_stub.called with describe('Disabling clears token'), logged_in(teacher): test_client.req('patch', url, 200, data={'send_login_links': False}) new_token = m.Assignment.query.get(assig_id).send_login_links_token assert new_token is None assert not send_mail_stub.called with describe('Cannot enable login links with large availability window' ), logged_in(teacher): psef.site_settings.Opt.EXAM_LOGIN_MAX_LENGTH.set_and_commit_value( 'P1D' ) test_client.req( 'patch', url, 409, data={ 'send_login_links': True, 'deadline': tomorrow.isoformat(), 'available_at': yesterday.isoformat(), }, result={ **error_template, 'message': re.compile( 'Login links can only be enabled if the deadline is at' ' most 24 hours after.*' ) } )
def test_edit_feedback(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) ta = helpers.create_user_with_role(session, 'TA', course) work_id = helpers.get_id( helpers.create_submission(test_client, assignment, for_user=student)) add_reply = make_add_reply(work_id) feedback_url = f'/api/v1/submissions/{work_id}/feedbacks/?with_replies' with logged_in(student): reply = add_reply('a reply') reply_url = (f'/api/v1/comments/{reply["comment_base_id"]}/' f'replies/{reply["id"]}') with describe('Editing with the same content does not create an edit' ), logged_in(teacher): test_client.req('patch', reply_url, 200, data={'comment': 'a reply'}) test_client.req( 'get', feedback_url, 200, result={ '__allow_extra__': True, 'user': [{ '__allow_extra__': True, 'replies': [{ 'id': reply['id'], 'comment': 'a reply', 'last_edit': None, '__allow_extra__': True, }], }] }, ) with describe('teachers and own user can edit reply'): with logged_in(student): test_client.req('patch', reply_url, 200, data={'comment': 'updated1'}) with logged_in(teacher): test_client.req('patch', reply_url, 200, data={'comment': 'updated2'}) with logged_in(ta): test_client.req('patch', reply_url, 403, data={'comment': 'updated3'}) with logged_in(teacher): test_client.req( 'get', feedback_url, 200, result={ 'general': '', 'linter': {}, 'authors': [student], 'user': [{ '__allow_extra__': True, 'replies': [{ 'id': reply['id'], 'comment': 'updated2', 'last_edit': str, '__allow_extra__': True, }], }] }, ) with describe('Students can only see own edits'): with logged_in(teacher): other_reply = add_reply('A reply jooo') other_reply_url = ( f'/api/v1/comments/{other_reply["comment_base_id"]}/' f'replies/{other_reply["id"]}') with logged_in(student): edits = test_client.req('get', reply_url + '/edits/', 200, result=[ { 'id': int, 'old_text': 'updated1', 'created_at': str, 'new_text': 'updated2', 'editor': teacher, }, { 'id': int, 'old_text': 'a reply', 'created_at': str, 'new_text': 'updated1', 'editor': student, }, ]) test_client.req('get', other_reply_url + '/edits/', 403) with describe('teachers can see others edits'), logged_in(teacher): test_client.req('get', reply_url + '/edits/', 200, result=edits) test_client.req('get', other_reply_url + '/edits/', 200, result=[]) with describe('last_edit should really be the last edit'), logged_in( student): last_edit = test_client.req('get', feedback_url, 200)['user'][0]['replies'][0]['last_edit'] now = cg_dt_utils.DatetimeWithTimezone.utcnow() with freeze_time(now): test_client.req('patch', reply_url, 200, data={'comment': 'updated4'}) new_last_edit = test_client.req( 'get', feedback_url, 200)['user'][0]['replies'][0]['last_edit'] assert new_last_edit > last_edit assert new_last_edit == now.isoformat()
def test_delete_feedback(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) ta = helpers.create_user_with_role(session, 'TA', course) work_id = helpers.get_id( helpers.create_submission(test_client, assignment, for_user=student)) add_reply = make_add_reply(work_id) feedback_url = f'/api/v1/submissions/{work_id}/feedbacks/?with_replies' with describe('Users can delete own replies'): for user in [teacher, student, ta]: with logged_in(user): reply = add_reply('a reply') test_client.req( 'delete', (f'/api/v1/comments/{reply["comment_base_id"]}/' f'replies/{reply["id"]}'), 204) with logged_in(teacher): test_client.req( 'get', feedback_url, 200, result={ 'general': '', 'linter': {}, 'authors': [], 'user': [] }, ) with describe('Only teachers can delete replies of others'): with logged_in(student): reply, base = add_reply('a reply', include_base=True) with logged_in(ta): test_client.req('delete', (f'/api/v1/comments/{reply["comment_base_id"]}/' f'replies/{reply["id"]}'), 403) with logged_in(teacher): test_client.req( 'get', feedback_url, 200, result={ 'general': '', 'linter': {}, 'authors': [student], 'user': [base], }, ) with logged_in(teacher): test_client.req('delete', (f'/api/v1/comments/{reply["comment_base_id"]}/' f'replies/{reply["id"]}'), 204) test_client.req( 'get', feedback_url, 200, result={ 'general': '', 'linter': {}, 'authors': [], 'user': [] }, ) with describe('Cannot delete a reply twice'), logged_in(student): reply = add_reply('To delete') test_client.req('delete', (f'/api/v1/comments/{reply["comment_base_id"]}/' f'replies/{reply["id"]}'), 204) test_client.req('delete', (f'/api/v1/comments/{reply["comment_base_id"]}/' f'replies/{reply["id"]}'), 404)
# miscellanous functions import helpers as hp # hash of hash. # the first indice is the time difference between two # coordinated objects. the second one is referring to a # session. if a session refered by i has a value of 5, it # means that it has 5 correlated objects with a predecessor at # exactly i times steps behind. session_sequences = {} # keeps track of the number of events taken into account for one # session in order to normalize session_norm = {} for file_path in hp.get_csv(): # iterating over the files (sessions) session_id = hp.get_id(file_path) # The Id in an integer session_id = int(session_id) session_norm[session_id] = 0 # the name of the session is used as a table name in the # database session_name = hp.get_name(file_path) # array of arrays, stores for each coordinated object the # list of timespans between student switches event_time_differences = [] con = lite.connect('mysteries.db') with con: cur = con.cursor() # Gets the list of all distinct objects cur.execute("\
def test_get_entire_workspace(logged_in, test_client, session, admin_user, describe, tomorrow): 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) w_id, = assignment["analytics_workspace_ids"] work_id = helpers.get_id( helpers.create_submission(test_client, assignment, for_user=student)) url = f'/api/v1/analytics/{w_id}' rubric = test_client.req( 'put', f'/api/v1/assignments/{get_id(assignment)}/rubrics/', 200, data=RUBRIC) def test(*args): if not isinstance(args[0], tuple): args = [(student, work_id, *args)] test_client.req('get', url, 200, result={ 'id': w_id, 'assignment_id': assignment['id'], 'student_submissions': { str(get_id(arg[0])): [{ 'id': get_id(arg[1]), 'created_at': str, 'grade': arg[2], 'assignee_id': arg[3] and get_id(arg[3]), }] for arg in args }, 'data_sources': [str, str], }) with describe('should send the current grade'), logged_in(teacher): test(None, None) for item in [ get_rubric_item(rubric, 'My header', '10points'), get_rubric_item(rubric, 'My header2', '2points'), ]: test_client.req( 'patch', f'/api/v1/submissions/{work_id}/rubricitems/{item["id"]}', 204, ) test(10, None) test_client.req('patch', f'/api/v1/submissions/{work_id}', 200, data={'grade': 0.5}) test(0.5, None) test_client.req('patch', f'/api/v1/submissions/{work_id}', 200, data={'grade': None}) test(10, None) with describe('should show the assignee'), logged_in(teacher): test_client.req('patch', f'/api/v1/submissions/{work_id}/grader', 204, data={'user_id': get_id(teacher)}) test(10, teacher) with describe('should not show test students'), logged_in(teacher): helpers.create_submission(test_client, assignment, is_test_submission=True) test(10, teacher) with describe('Should show multiple students'), logged_in(teacher): student2 = helpers.create_user_with_role(session, 'Student', course) work2 = helpers.create_submission(test_client, assignment, for_user=student2) test((student, work_id, 10, teacher), (student2, work2, None, None)) with describe('students cannot access it'), logged_in(student): test_client.req('get', url, 403)
def test_send_login_links(describe, session, test_client, logged_in, admin_user, tomorrow, stub_function, yesterday): with describe('setup'), logged_in(admin_user): stub_apply = stub_function(psef.tasks._send_login_links_to_users_1, 'apply_async') stub_mail = stub_function(psef.mail, 'send_login_link_mail') assig = helpers.create_assignment(test_client) assig_id = helpers.get_id(assig) login_links_token = uuid.uuid4() m.Assignment.query.filter_by(id=assig_id).update({ '_available_at': tomorrow.isoformat(), '_deadline': (tomorrow + timedelta(minutes=1)).isoformat(), '_send_login_links_token': login_links_token, 'kind': 'exam', }) task_result = m.TaskResult(user=None) session.add(task_result) session.commit() student = helpers.create_user_with_role(session, 'Student', assig['course']) with describe('does not crash if task does not exist'): psef.tasks._send_login_links_to_users_1(assig_id, uuid.uuid4().hex, tomorrow.isoformat(), login_links_token, 1) assert task_result.state.is_not_started with describe('reschedules when called too early'): psef.tasks._send_login_links_to_users_1(assig_id, task_result.id.hex, tomorrow.isoformat(), login_links_token.hex, 1) assert stub_apply.called_amount == 1 assert task_result.state.is_not_started with describe('Does nothing when token is different'): psef.tasks._send_login_links_to_users_1(assig_id, task_result.id.hex, yesterday.isoformat(), uuid.uuid4().hex, 1) assert not stub_mail.called assert task_result.state.is_skipped task_result.state = m.TaskResultState.not_started session.commit() with describe('Does nothing when deadline expired'): del flask.g.request_start_time with freeze_time(tomorrow + timedelta(days=1)): psef.tasks._send_login_links_to_users_1(assig_id, task_result.id.hex, yesterday.isoformat(), login_links_token.hex, 1) assert not stub_mail.called assert task_result.state.is_skipped task_result.state = m.TaskResultState.not_started session.commit() with describe('Does nothing when task was already finished'): task_result.state = m.TaskResultState.finished session.commit() psef.tasks._send_login_links_to_users_1(assig_id, task_result.id.hex, yesterday.isoformat(), login_links_token.hex, 1) assert not stub_mail.called assert task_result.state.is_finished
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_delete_submission_passback( lti1p3_provider, describe, logged_in, admin_user, watch_signal, stub_function, test_client, session, tomorrow ): with describe('setup'), logged_in(admin_user): watch_signal(signals.WORK_CREATED, clear_all_but=[]) watch_signal(signals.GRADE_UPDATED, clear_all_but=[]) watch_signal(signals.USER_ADDED_TO_COURSE, clear_all_but=[]) stub_function( pylti1p3.service_connector.ServiceConnector, 'get_access_token', lambda: '' ) stub_passback = stub_function( pylti1p3.assignments_grades.AssignmentsGradesService, 'put_grade' ) course, course_conn = helpers.create_lti1p3_course( test_client, session, lti1p3_provider ) assig = helpers.create_lti1p3_assignment( session, course, state='done', deadline=tomorrow ) user = helpers.create_lti1p3_user(session, lti1p3_provider) lti_user_id = m.UserLTIProvider.query.filter_by( user=m.User.resolve(user) ).one().lti_user_id course_conn.maybe_add_user_to_course(user, ['Learner']) sub_oldest, sub_older, sub_middle, sub_newest = [ helpers.to_db_object( helpers.create_submission(test_client, assig, for_user=user), m.Work ) for _ in range(4) ] signal = watch_signal( signals.WORK_DELETED, clear_all_but=[m.LTI1p3Provider._delete_submission] ) def do_delete(sub): with logged_in(admin_user): test_client.req( 'delete', f'/api/v1/submissions/{helpers.get_id(sub)}', 204 ) with describe('Delete non newest'): do_delete(sub_older) assert signal.was_send_once assert not stub_passback.called with describe('Calling method for non existing work simply does nothing'): m.LTI1p3Provider._delete_submission(1000000) assert not stub_passback.called with describe('Calling method for non deleted work does nothing'): m.LTI1p3Provider._delete_submission(helpers.get_id(sub_newest)) assert not stub_passback.called with describe('Delete newest should passback grade of new newest'): sub_middle.set_grade(5.0, m.User.resolve(admin_user)) session.commit() # We should have removed the grade_updated signal assert not stub_passback.called do_delete(sub_newest) assert signal.was_send_once assert stub_passback.called_amount == 1 grade, = stub_passback.all_args[0].values() assert grade.get_score_given() == 5.0 assert grade.get_user_id() == lti_user_id with describe('Deleting new newest should passback next non deleted'): sub_older.set_grade(6.0, m.User.resolve(admin_user)) sub_oldest.set_grade(8.0, m.User.resolve(admin_user)) session.commit() assert not m.GradeHistory.query.filter_by(work=sub_oldest ).one().passed_back do_delete(sub_middle) assert signal.was_send_once assert stub_passback.called_amount == 1 grade, = stub_passback.all_args[0].values() # Should passback oldest as we deleted older in an earlier block assert grade.get_score_given() == 8.0 assert grade.get_user_id() == lti_user_id # Should update history assert m.GradeHistory.query.filter_by(work=sub_oldest ).one().passed_back with describe('Deleting without any existing submission should passback'): do_delete(sub_oldest) assert signal.was_send_once assert stub_passback.called_amount == 1 grade, = stub_passback.all_args[0].values() assert grade.get_score_given() is None assert grade.get_grading_progress() == 'NotReady' assert grade.get_user_id() == lti_user_id with describe('Passing back deleted sub should do nothing'): lti1p3_provider._passback_grade( assignment=assig, sub=sub_newest, timestamp=DatetimeWithTimezone.utcnow() ) assert not stub_passback.called
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_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_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)