def test_submit_with_small_group( test_client, session, logged_in, teacher_user, course, error_template, assignment ): user_no_group = create_user_with_perms( session, [CPerm.can_submit_own_work, CPerm.can_see_assignments], course ) user_with_group = create_user_with_perms( session, [CPerm.can_submit_own_work, CPerm.can_see_assignments], course ) with logged_in(teacher_user): g_set = create_group_set(test_client, course.id, 1, 1) test_client.req( 'patch', f'/api/v1/assignments/{assignment.id}', 200, data={'group_set_id': g_set['id']} ) g1 = create_group( test_client, g_set['id'], [user_with_group.id], ) with logged_in(user_no_group): # Can submit without group, you are simply the author (not a group) sub = create_submission(test_client, assignment.id) assert sub['user']['id'] == user_no_group.id with logged_in(user_with_group): # But when submitting as a user in a group the group should still be # the author sub = create_submission(test_client, assignment.id) assert sub['user']['group']['id'] == g1['id']
def test_archive_generated_files(basic, test_client, describe, logged_in): with describe('setup'): course, assig, teacher, student = basic with logged_in(student): helpers.create_submission( test_client, assignment_id=assig.id, submission_data=( (f'{os.path.dirname(__file__)}/../test_data/test_submissions/' 'single_symlink_archive.tar.gz'), 'f.tar.gz', ), ) sub = next(sub for sub in assig.get_all_latest_submissions() if sub.user == student) with tempfile.TemporaryDirectory() as tmpdir: p.files.restore_directory_structure(sub, tmpdir) root = f'{tmpdir}/{os.listdir(tmpdir)[0]}' with describe('symbolic link replacement files'), open( f'{root}/test_file') as f: assert not os.path.islink(f'{root}/test_file') content = f.read() assert content.endswith('\n') assert 'symbolic link' in content assert 'non_existing' in content
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 test_delete_submission_of_group( 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 ) # Create some users and let them have individual submissions g_user1 = helpers.create_lti1p3_user(session, lti1p3_provider) g_user2 = helpers.create_lti1p3_user(session, lti1p3_provider) for user in [g_user1, g_user2]: course_conn.maybe_add_user_to_course(user, ['Learner']) sub = helpers.create_submission(test_client, assig, for_user=user) helpers.to_db_object(sub, m.Work).set_grade( 5.0, m.User.resolve(admin_user) ) # Place the users in a group, with a submission with a different grade # than their individual submission. gset = helpers.create_group_set(test_client, course, 1, 2, [assig]) helpers.create_group(test_client, gset, [g_user1, g_user2]) gsub = helpers.create_submission(test_client, assig, for_user=g_user2) helpers.to_db_object(gsub, m.Work).set_grade( 6.0, m.User.resolve(admin_user) ) session.commit() with describe( 'deleting group submission passes back individual submissions' ): with logged_in(admin_user): test_client.req( 'delete', f'/api/v1/submissions/{helpers.get_id(gsub)}', 204 ) assert stub_passback.called_amount == 2 # The grade passed back is that of their individual submission assert stub_passback.args[0][0].get_score_given() == 5.0 assert stub_passback.args[1][0].get_score_given() == 5.0
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_searching_test_student_in_course(logged_in, test_client, assignment, session, teacher_user, tomorrow): c_id = assignment.course_id with logged_in(teacher_user): actual_student_name = f'TEST_STUDENT_{str(uuid.uuid4())}' student = create_user_with_role( session, 'Student', c_id, name=actual_student_name, ) assig_id = create_assignment(test_client, c_id, 'open', deadline=tomorrow)['id'] res = create_submission( test_client, assig_id, is_test_submission=True, ) test_user_id = res['user']['id'] res = test_client.req( 'get', f'/api/v1/courses/{c_id}/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_passback_single_submission( 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.USER_ADDED_TO_COURSE, clear_all_but=[]) signal = watch_signal( signals.GRADE_UPDATED, clear_all_but=[m.LTI1p3Provider._passback_submission] ) stub_function( pylti1p3.service_connector.ServiceConnector, 'get_access_token', lambda: '' ) stub_passback = stub_function( pylti1p3.assignments_grades.AssignmentsGradesService, 'put_grade', raise_pylti1p3_exc ) 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) course_conn.maybe_add_user_to_course(user, []) older_sub, newest_sub = [ helpers.to_db_object( helpers.create_submission(test_client, assig, for_user=user), m.Work ) for _ in range(2) ] with describe('calling directly with non existing assignment is a noop'): m.LTI1p3Provider._passback_submission((newest_sub.id, 100000)) assert not stub_passback.called with describe('changing grade of non newest sub does not passback'): older_sub.set_grade(5.0, admin_user) assert signal.was_send_once assert not stub_passback.called with describe('changing grade on newest sub passes the new grade back'): newest_sub.set_grade(9.5, m.User.resolve(admin_user)) assert signal.was_send_once assert stub_passback.called_amount == 1 assert stub_passback.args[0][0].get_score_given() == 9.5 with describe('unsetting grade should passback something without grade'): newest_sub.set_grade(None, m.User.resolve(admin_user)) assert signal.was_send_once assert stub_passback.called_amount == 1 assert stub_passback.args[0][0].get_score_given() is None assert stub_passback.args[0][0].get_activity_progress() == 'Submitted'
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_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_add_test_student_to_group( session, test_client, logged_in, assignment, teacher_user, error_template, describe, monkeypatch_celery ): c_id = assignment.course.id with logged_in(teacher_user): g_set = create_group_set(test_client, c_id, 1, 4) res = create_submission( test_client, assignment.id, is_test_submission=True, ) test_student = res['user'] with describe('new group with a test student cannot be created'): res = test_client.req( 'post', f'/api/v1/group_sets/{g_set["id"]}/group', 400, result=error_template, data={ 'member_ids': [test_student['id']], }, ) with describe( 'new group with a test student and other students cannot be created' ): u1 = create_user_with_perms( session, [CPerm.can_edit_own_groups], assignment.course ) res = test_client.req( 'post', f'/api/v1/group_sets/{g_set["id"]}/group', 400, result=error_template, data={ 'member_ids': [test_student['id'], u1.id], }, ) with describe('test student can not be added to existing group'): g1 = create_group( test_client, g_set['id'], [], ) res = test_client.req( 'post', f'/api/v1/groups/{g1["id"]}/member', 400, result=error_template, data={ 'username': test_student['username'], }, )
def test_passback_with_bonus_points( 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.USER_ADDED_TO_COURSE, clear_all_but=[]) signal = watch_signal( signals.GRADE_UPDATED, clear_all_but=[m.LTI1p3Provider._passback_submission] ) stub_function( pylti1p3.service_connector.ServiceConnector, 'get_access_token', lambda: '' ) stub_passback = stub_function( pylti1p3.assignments_grades.AssignmentsGradesService, 'put_grade', raise_pylti1p3_exc ) course, course_conn = helpers.create_lti1p3_course( test_client, session, lti1p3_provider ) assig = helpers.create_lti1p3_assignment( session, course, state='done', deadline=tomorrow ) test_client.req( 'patch', f'/api/v1/assignments/{helpers.get_id(assig)}', 200, data={'max_grade': 15} ) user = helpers.create_lti1p3_user(session, lti1p3_provider) course_conn.maybe_add_user_to_course(user, []) sub = helpers.to_db_object( helpers.create_submission(test_client, assig, for_user=user), m.Work ) with describe('can passback bonus points'), logged_in(admin_user): test_client.req( 'patch', f'/api/v1/submissions/{helpers.get_id(sub)}', 200, data={'grade': 14}, ) assert signal.was_send_once assert stub_passback.called_amount == 1 assert stub_passback.args[0][0].get_score_given() == 14 assert stub_passback.args[0][0].get_score_maximum() == 10
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_failing_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', raise_pylti1p3_exc ) 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) course_conn.maybe_add_user_to_course(user, []) sub = helpers.to_db_object( helpers.create_submission(test_client, assig, for_user=user), m.Work ) sub.set_grade(10.0, m.User.resolve(admin_user)) session.commit() hist = m.GradeHistory.query.filter_by(work=sub).one() assert hist assert not hist.passed_back with describe('failing passback should not update history'): m.LTI1p3Provider._passback_grades(assig.id) assert stub_passback.called hist = m.GradeHistory.query.filter_by(work=sub).one() assert hist assert not hist.passed_back
def make_assig(*args, **kwargs): hidden = kwargs.pop('has_hidden', False) with logged_in(admin_user): assig_id = create_assignment(*args, **kwargs)['id'] create_auto_test(test_client, assig_id, has_hidden_steps=hidden) assig = m.Assignment.query.get(assig_id) run = m.AutoTestRun(_job_id=uuid.uuid4(), auto_test=assig.auto_test, batch_run_done=False) session.add(run) session.commit() with logged_in(admin_user): sub_id = create_submission(test_client, assig.id)['id'] assert run.id is not None session.add( m.AutoTestResult(final_result=False, work_id=sub_id, auto_test_run_id=run.id)) session.commit() return assig
def test_update_test_student_role( teacher_user, logged_in, test_client, describe, tomorrow, assignment, error_template, ): c_id = assignment.course.id with logged_in(teacher_user): test_sub = create_submission( test_client, assignment.id, is_test_submission=True, ) test_user = test_sub['user'] roles = test_client.req( 'get', f'/api/v1/courses/{c_id}/roles/', 200, ) role = next(r for r in roles if not r['hidden']) test_client.req( 'put', f'/api/v1/courses/{assignment.course.id}/users/', 400, result=error_template, data={ 'user_id': test_user['id'], 'role_id': role['id'], }, )
plt.boxplot(results) ax.set_xticklabels(names, fontsize=9, rotation='vertical') plt.subplots_adjust(bottom=0.15) plt.savefig(pltfname) # Block: find the best model print("Determining best model") best = dict(name=None, mean=0, estimator=None) for name, result, estimator in zip(names, results, estimators): if (result.mean() > best['mean']): best = dict(name=name, mean=result.mean(), estimator=estimator) print("Best model: %s, %f" % (best['name'], best['mean'])) # Block: use the best estimator on validation data print("Evaluating valdiation data with %s" % (best['name'])) model = best['estimator'] model.fit(Xtrn, Ytrn) predictions = model.predict(Xval) print("accuracy = %f" % (accuracy_score(Yval, predictions))) print("classification report") print(classification_report(Yval, predictions)) # Block: use the estimator on holdout data print("Making predictions for test (holdout) data with: %s" % (best['name'])) _, Xtest = helpers.get_test_data() predictions = model.predict(Xtest) submissionfname = 'tmp-submission.csv' print("Saving predictions to %s" % (submissionfname)) helpers.create_submission(predictions, submissionfname=submissionfname)
def test_passing_back_all_grades( lti1p3_provider, describe, logged_in, admin_user, watch_signal, stub_function, test_client, session, tomorrow, make_function_spy ): 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=[]) watch_signal(signals.WORK_DELETED, clear_all_but=[]) signal = watch_signal( signals.ASSIGNMENT_STATE_CHANGED, clear_all_but=[m.LTI1p3Provider._passback_grades] ) stub_get_acccess_token = stub_function( pylti1p3.service_connector.ServiceConnector, 'get_access_token', lambda: '' ) stub_passback = make_function_spy( pylti1p3.assignments_grades.AssignmentsGradesService, 'put_grade', pass_self=True ) # Make a session with a response that returns json when the `json` # method is called req_session = requests_stubs.session_maker()() req_session.Response.json = lambda _=None: {} stub_requests_post = stub_function(requests, 'post', req_session.post) course, course_conn = helpers.create_lti1p3_course( test_client, session, lti1p3_provider ) assig = helpers.create_lti1p3_assignment( session, course, state='hidden', deadline=tomorrow ) user1 = helpers.create_lti1p3_user(session, lti1p3_provider) user2 = helpers.create_lti1p3_user(session, lti1p3_provider) user3 = helpers.create_lti1p3_user(session, lti1p3_provider) all_students = [user1, user2, user3] for u in all_students: course_conn.maybe_add_user_to_course(u, ['Learner']) lti_user_ids = [ m.UserLTIProvider.query.filter_by(user=u).one().lti_user_id for u in all_students ] gset = helpers.create_group_set(test_client, course, 1, 2, [assig]) helpers.create_group(test_client, gset, [user1, user3]) sub = helpers.to_db_object( helpers.create_submission(test_client, assig, for_user=user1), m.Work ) sub.set_grade(2.5, admin_user) # Make sure user2 does not have a non deleted submission user2_sub = helpers.create_submission( test_client, assig, for_user=user2 ) test_client.req( 'delete', f'/api/v1/submissions/{helpers.get_id(user2_sub)}', 204 ) with describe('changing assignment state to "open" does not passback'): assig.set_state_with_string('open') assert signal.was_send_once assert not stub_passback.called assert not stub_get_acccess_token.called with describe('changing to done does passback'): assig.set_state_with_string('done') assert signal.was_send_once assert stub_passback.called_amount == len(all_students) # Calls should be cached assert stub_get_acccess_token.called_amount == 1 p1, p2, p3 = stub_passback.args assert p1[0] != p2[0] != p3[0] assert p1[0].get_score_given() == 2.5 assert p2[0].get_score_given() == 2.5 assert {p1[0].get_user_id(), p2[0].get_user_id()} == {lti_user_ids[0], lti_user_ids[2]} # Does not have a submission assert p3[0].get_score_given() is None assert p3[0].get_user_id() == lti_user_ids[1] with describe('toggling to open and done should do a new passback'): assig.set_state_with_string('open') assert signal.was_send_once assert not stub_passback.called assert not stub_get_acccess_token.called assig.set_state_with_string('done') assert signal.was_send_n_times(2) assert stub_passback.called # access token should still be cached assert not stub_get_acccess_token.called
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_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)
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_group( test_client, logged_in, prog_course, session, teacher_user, error_template, app, monkeypatch_celery ): user_only_own = create_user_with_perms( session, [ CPerm.can_edit_own_groups, CPerm.can_submit_own_work, CPerm.can_see_assignments ], prog_course ) user_other_too = create_user_with_perms( session, [CPerm.can_edit_others_groups, CPerm.can_see_assignments], prog_course ) with logged_in(teacher_user): assig_id = [ a for a in test_client. req('get', f'/api/v1/courses/{prog_course.id}/assignments/', 200) if a['state'] == 'submitting' ][0]['id'] with logged_in(teacher_user): group_set = create_group_set(test_client, prog_course.id, 2, 4) test_client.req( 'patch', f'/api/v1/assignments/{assig_id}', 200, data={'group_set_id': group_set['id']} ) group_own_id = create_group( test_client, group_set["id"], [ create_user_with_perms(session, [], prog_course).id, user_only_own.id, ] )['id'] group_others_id = create_group( test_client, group_set["id"], [ create_user_with_perms(session, [], prog_course).id, create_user_with_perms(session, [], prog_course).id, ] )['id'] group_empty_id = create_group(test_client, group_set["id"], [])['id'] with logged_in(user_only_own): sub = create_submission(test_client, assig_id) assert sub['user']['group'] with logged_in(user_only_own): # Cannot delete with submission test_client.req( 'delete', f'/api/v1/groups/{group_own_id}', 400, result=error_template ) # Cannot delete non empty group of other members test_client.req( 'delete', f'/api/v1/groups/{group_others_id}', 403, result=error_template ) # Can delete empty group test_client.req('delete', f'/api/v1/groups/{group_empty_id}', 204) with logged_in(user_other_too): # Cannot delete with submission test_client.req( 'delete', f'/api/v1/groups/{group_own_id}', 400, result=error_template ) # Can delete non empty group of other members test_client.req('delete', f'/api/v1/groups/{group_others_id}', 204) with logged_in(teacher_user): test_client.req('delete', f'/api/v1/submissions/{sub["id"]}', 204) with logged_in(user_only_own): # Can delete now submission has been deleted test_client.req('delete', f'/api/v1/groups/{group_own_id}', 204)
def test_seeing_teacher_revision_in_group( test_client, session, logged_in, teacher_user, course, error_template, assignment, describe ): with describe('setup'): user_other_group = create_user_with_perms( session, [ CPerm.can_submit_own_work, CPerm.can_view_own_teacher_files, CPerm.can_see_assignments ], course ) user_with_perm = create_user_with_perms( session, [ CPerm.can_submit_own_work, CPerm.can_view_own_teacher_files, CPerm.can_see_assignments ], course ) user_without_perm = create_user_with_perms( session, [CPerm.can_submit_own_work, CPerm.can_see_assignments], course ) with logged_in(teacher_user): g_set = create_group_set(test_client, course.id, 1, 2) test_client.req( 'patch', f'/api/v1/assignments/{assignment.id}', 200, data={'group_set_id': g_set['id']} ) g1 = create_group( test_client, g_set['id'], [user_with_perm, user_without_perm], ) with logged_in(user_with_perm): sub = create_submission(test_client, assignment.id) # Submission is by the group assert sub['user']['group']['id'] == g1['id'] student_files = test_client.req( 'get', f'/api/v1/submissions/{sub["id"]}/files/', 200 ) with describe('Cannot view teacher files while assig is open' ), logged_in(user_with_perm): test_client.req( 'get', f'/api/v1/submissions/{sub["id"]}/files/?owner=teacher', 403, result=error_template, ) with describe('Can see teacher files after assig is done' ), logged_in(user_with_perm): assignment.set_state_with_string('done') session.commit() test_client.req( 'get', f'/api/v1/submissions/{sub["id"]}/files/?owner=teacher', 200, result=student_files, ) # This marks all files as from the student revision, i.e. there are no # teacher files. m.File.query.filter( m.File.work_id == sub['id'], m.File.parent_id.isnot(None) ).update({'fileowner': m.FileOwner.student}) test_client.req( 'get', f'/api/v1/submissions/{sub["id"]}/files/?owner=teacher', 200, result={ **student_files, 'entries': [], } ) with describe( 'User without permission to see own teacher files gets an error' ), logged_in(user_without_perm): test_client.req( 'get', f'/api/v1/submissions/{sub["id"]}/files/?owner=teacher', 403, result=error_template, ) with describe('User from other group cannot see teacher files' ), logged_in(user_other_group): test_client.req( 'get', f'/api/v1/submissions/{sub["id"]}/files/?owner=teacher', 403, result=error_template, )
def test_update_group_set( test_client, logged_in, bs_course, prog_course, request, error_template, teacher_user, ta_user, assignment, session ): with logged_in(teacher_user): group_set = test_client.req( 'put', f'/api/v1/courses/{bs_course.id}/group_sets/', data={ 'minimum_size': 1, 'maximum_size': 3, }, status_code=200, ) # Update should be done in place group_set = test_client.req( 'put', f'/api/v1/courses/{bs_course.id}/group_sets/', data={ 'minimum_size': 3, 'maximum_size': 10, 'id': group_set['id'], }, status_code=200, result={ 'id': group_set['id'], 'minimum_size': 3, 'maximum_size': 10, 'assignment_ids': [], }, ) test_client.req( 'patch', f'/api/v1/assignments/{assignment.id}', 200, data={'group_set_id': group_set['id']} ) group_set['assignment_ids'].append(assignment.id) test_client.req( 'get', f'/api/v1/courses/{bs_course.id}/group_sets/', 200, result=[group_set] ) # You should not be able to change the course id test_client.req( 'put', f'/api/v1/courses/{prog_course.id}/group_sets/', data={ 'minimum_size': 3, 'maximum_size': 10, 'id': group_set['id'], }, status_code=400, result=error_template, ) # The group set now has a group with 3 members user = create_user_with_perms(session, [], bs_course) group = test_client.req( 'post', f'/api/v1/group_sets/{group_set["id"]}/group', data={'member_ids': [teacher_user.id, ta_user.id, user.id]}, status_code=200 ) test_client.req( 'get', f'/api/v1/assignments/{assignment.id}/groups/{group["id"]}/member_states/', 200, result={ str(teacher_user.id): True, str(ta_user.id): True, str(user.id): True, } ) # You should not be able to set the max size to lower than 3 test_client.req( 'put', f'/api/v1/courses/{bs_course.id}/group_sets/', data={ 'minimum_size': 1, 'maximum_size': 2, 'id': group_set['id'], }, status_code=400, result=error_template, ) # You SHOULD be able to set the min size to higher than 3 as there was # no submission by any group test_client.req( 'put', f'/api/v1/courses/{bs_course.id}/group_sets/', data={ 'minimum_size': 4, 'maximum_size': 10, 'id': group_set['id'], }, status_code=200, result={ 'minimum_size': 4, 'maximum_size': 10, 'id': group_set['id'], 'assignment_ids': [assignment.id], } ) # Reset back to minimum size of 3 test_client.req( 'put', f'/api/v1/courses/{bs_course.id}/group_sets/', data={ 'minimum_size': 3, 'maximum_size': 4, 'id': group_set['id'], }, status_code=200, ) test_client.req( 'patch', f'/api/v1/assignments/{assignment.id}', 200, data={'group_set_id': group_set['id']} ) submission = create_submission(test_client, assignment.id) assert submission['user']['group'] assert submission['user']['group']['id'] == group['id'] # Now you should not be able to set the min size to higher than 3 as # there IS a submission by a group. test_client.req( 'put', f'/api/v1/courses/{bs_course.id}/group_sets/', data={ 'minimum_size': 4, 'maximum_size': 10, 'id': group_set['id'], }, status_code=400, result=error_template, ) # You should also not be able to disconnect the group set for this # assignment test_client.req( 'patch', f'/api/v1/assignments/{assignment.id}', 400, data={'group_set_id': None}, result=error_template, ) new_group_set = create_group_set(test_client, prog_course.id, 2, 4) # You should also not be able to change the group set for this # assignment test_client.req( 'patch', f'/api/v1/assignments/{assignment.id}', 400, data={'group_set_id': new_group_set['id']}, result=error_template, )
def test_get_all_comments_of_user(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'] teacher1 = admin_user teacher2 = helpers.create_user_with_role(session, 'Teacher', course) student1 = helpers.create_user_with_role(session, 'Student', course) student2 = helpers.create_user_with_role(session, 'Student', course) sub1 = helpers.create_submission(test_client, assignment, for_user=student1) sub2 = helpers.create_submission(test_client, assignment, for_user=student2) add_reply1 = make_add_reply(sub1) add_reply2 = make_add_reply(sub2) with logged_in(teacher1): rep1_1 = add_reply1('Hello a reply', line=1) rep1_2 = add_reply1('Hello a reply', line=1) rep2 = add_reply1('Hello a reply', line=10) rep3 = add_reply2('Hello a reply', line=5) def get_url(user): return (f'/api/v1/assignments/{get_id(assignment)}/users' f'/{get_id(user)}/comments/') with describe('Can get all comments by a user'), logged_in(teacher1): result = [ { '__allow_extra__': True, 'replies': [ dict_without(rep1_1, 'author'), dict_without(rep1_2, 'author') ], 'line': 1, }, { '__allow_extra__': True, 'replies': [dict_without(rep2, 'author')], 'line': 10, }, { '__allow_extra__': True, 'replies': [dict_without(rep3, 'author')], 'line': 5, }, ] test_client.req('get', get_url(teacher1), 200, result) with describe('Does not get threads without own reply'): with logged_in(teacher2): # rep1_3 is allowed to exist in the reply, however this is not the # case with the current implementation. rep5 should never be in the # reply. rep1_3 = add_reply1('A reply by somebody else', line=1) rep5 = add_reply1('A reply by somebody else', line=100) test_client.req( 'get', get_url(teacher2), 200, [ { '__allow_extra__': True, 'replies': [ # rep1_1 and rep1_2 are allowed to be present dict_without(rep1_3, 'author') ], 'line': 1, }, { '__allow_extra__': True, 'replies': [dict_without(rep5, 'author')], 'line': 100, }, ], ) with logged_in(teacher1): test_client.req('get', get_url(teacher1), 200, result)
def test_submit_with_group( test_client, session, logged_in, teacher_user, course, error_template, assignment, monkeypatch_celery ): user_full_group = create_user_with_perms( session, [CPerm.can_submit_own_work, CPerm.can_see_assignments], course ) user_empty_group = create_user_with_perms( session, [CPerm.can_submit_own_work, CPerm.can_see_assignments], course ) user_no_group = create_user_with_perms( session, [CPerm.can_submit_own_work, CPerm.can_see_assignments], course ) user_no_perms = create_user_with_perms( session, [CPerm.can_see_assignments], course ) with logged_in(teacher_user): g_set = create_group_set(test_client, course.id, 2, 4) test_client.req( 'patch', f'/api/v1/assignments/{assignment.id}', 200, data={'group_set_id': g_set['id']} ) g1 = create_group( test_client, g_set['id'], [user_full_group.id, user_no_perms.id], ) create_group(test_client, g_set['id'], [user_empty_group.id]) with logged_in(user_empty_group): err = create_submission(test_client, assignment.id, err=400) assert 'enough members' in err['message'] with logged_in(user_no_perms): # Can't submit even if some users in the group can err = create_submission(test_client, assignment.id, err=403) with logged_in(user_no_group): # Can't submit as a user without group as the minimum size is 2 err = create_submission(test_client, assignment.id, err=404) assert 'group was found' in err['message'] with logged_in(user_full_group): # User can submit submission sub = create_submission(test_client, assignment.id) # Make sure submission is done as the group, not as the user assert sub['user']['group']['id'] == g1['id'] # Make sure the user can see the just submitted submission test_client.req( 'get', f'/api/v1/assignments/{assignment.id}/submissions/?extended', 200, result=[sub] ) test_client.req( 'get', f'/api/v1/submissions/{sub["id"]}?extended', 200, result=sub ) files = test_client.req( 'get', f'/api/v1/submissions/{sub["id"]}/files/', 200 ) while 'entries' in files: files = files['entries'][0] code_id = files['id'] response = test_client.get(f'/api/v1/code/{code_id}') assert response.status_code == 200 with logged_in(teacher_user): test_client.req( 'put', f'/api/v1/code/{code_id}/comments/3', 204, data={'comment': 'Lekker gewerkt pik!'} ) # Set state to done so the student can see its feedback test_client.req( 'patch', f'/api/v1/assignments/{assignment.id}', 200, data={'state': 'done'} ) with logged_in(user_full_group): # Make sure we can see comments on the files test_client.req( 'get', f'/api/v1/code/{code_id}?type=feedback', 200, result={'3': {'line': 3, 'msg': 'Lekker gewerkt pik!'}} ) with logged_in(teacher_user): # Reset state back test_client.req( 'patch', f'/api/v1/assignments/{assignment.id}', 200, data={'state': 'open'} )
def test_add_feedback(request, logged_in, test_client, session, data, error_template, admin_user, mail_functions, 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_perms(session, [CPerm.can_submit_own_work], course) work = helpers.create_submission(test_client, assignment, for_user=student) data_err = request.node.get_closest_marker('data_error') is not None code_id = session.query(m.File.id).filter( m.File.work_id == work['id'], m.File.parent_id.isnot(None), m.File.name != '__init__', ).first()[0] if data_err: code = 400 else: code = 204 def get_result(has_author=True): if data_err: return {} msg = data.get('expected_result', data['comment']) res = { '0': { 'line': 0, 'msg': msg, 'author': { 'id': teacher.id, '__allow_extra__': True }, } } if not has_author: res['0'].pop('author') return res with describe('teacher can place comments'), logged_in(teacher): test_client.req('put', f'/api/v1/code/{code_id}/comments/0', code, data=data, result=None if code == 204 else error_template) if data_err: assert not mail_functions.any_mails() return assert not mail_functions.any_mails(), 'Student should not be mailed' res = test_client.req('get', f'/api/v1/code/{code_id}', 200, query={'type': 'feedback'}, result=get_result()) data = { 'comment': 'bye', 'expected_result': 'bye', } test_client.req( 'put', f'/api/v1/code/{code_id}/comments/0', code, data=data, ) assert not mail_functions.any_mails(), 'No mails for updates' test_client.req('get', f'/api/v1/code/{code_id}', 200, query={'type': 'feedback'}, result=get_result()) with describe( 'Students cannot place comments if they do not have the permission' ), logged_in(student): test_client.req('put', f'/api/v1/code/{code_id}/comments/1', 403, data=data) with describe('Students can get comments when assignment is done'): with logged_in(student): test_client.req( 'get', f'/api/v1/code/{code_id}', 200, query={'type': 'feedback'}, result={}, ) # We don't have the permission to see the notification test_client.req('get', '/api/v1/notifications/', 200, result={'notifications': []}) test_client.req('get', '/api/v1/notifications/?has_unread', 200, result={'has_unread': False}) with logged_in(teacher): test_client.req( 'patch', f'/api/v1/assignments/{assignment["id"]}', 200, data={'state': 'done'}, ) assert not mail_functions.any_mails(), ( 'Should not send old notifications when state changes') with logged_in(student): test_client.req( 'get', f'/api/v1/code/{code_id}', 200, query={'type': 'feedback'}, result=get_result(has_author=False), ) # Now we do have this permission test_client.req('get', '/api/v1/notifications/', 200, result={ 'notifications': [{ 'id': int, 'reasons': [['author', str]], '__allow_extra__': True, 'read': False, 'file_id': code_id, }] }) test_client.req('get', '/api/v1/notifications/?has_unread', 200, result={'has_unread': True}) with describe('Cannot get or update feedback after deleting submission' ), logged_in(teacher): test_client.req('delete', f'/api/v1/submissions/{work["id"]}', 204) test_client.req( 'get', f'/api/v1/code/{code_id}', 404, query={'type': 'feedback'}, result=error_template, ) test_client.req( 'put', f'/api/v1/code/{code_id}/comments/0', 404, data={'comment': 'Any value'}, result=error_template, ) with describe('After delete notification should be gone'), logged_in( student): test_client.req('get', '/api/v1/notifications/', 200, result={'notifications': []}) test_client.req('get', '/api/v1/notifications/?has_unread', 200, result={'has_unread': False})
def test_remove_user_from_group( test_client, session, logged_in, teacher_user, prog_course, error_template, assignment, monkeypatch_celery ): def make_user(): return create_user_with_perms( session, [ CPerm.can_edit_own_groups, CPerm.can_submit_own_work, CPerm.can_see_assignments ], prog_course ) u1 = make_user() u2 = make_user() u3 = make_user() u4 = make_user() with logged_in(teacher_user): g_set = create_group_set(test_client, prog_course.id, 2, 4) g1 = create_group( test_client, g_set['id'], [u1.id, u2.id, u3.id, u4.id], ) test_client.req( 'patch', f'/api/v1/assignments/{assignment.id}', 200, data={'group_set_id': g_set['id']} ) with logged_in(u3) as me: # Cannot remove other users from own group test_client.req( 'delete', f'/api/v1/groups/{g1["id"]}/members/{u1.id}', 403, result=error_template ) # Can remove self from group g1 = test_client.req( 'delete', f'/api/v1/groups/{g1["id"]}/members/{me.id}', 200, ) # Cannot remove self twice test_client.req( 'delete', f'/api/v1/groups/{g1["id"]}/members/{me.id}', 404, ) with logged_in(u2) as me: # Make sure group has a submission sub = create_submission(test_client, assignment.id) assert sub['user']['group']['id'] == g1['id'] # Cannot remove self from group as the group has submission test_client.req( 'delete', f'/api/v1/groups/{g1["id"]}/members/{me.id}', 403, result=error_template ) with logged_in(teacher_user): # Group is has three members so this is possible g1 = test_client.req( 'delete', f'/api/v1/groups/{g1["id"]}/members/{u1.id}', 200, ) # Group only has two member with and has a submission so this is not # possible test_client.req( 'delete', f'/api/v1/groups/{g1["id"]}/members/{u2.id}', 400, result=error_template ) test_client.req('delete', f'/api/v1/submissions/{sub["id"]}', 204) # The submission is deleted so this is now possible g1 = test_client.req( 'delete', f'/api/v1/groups/{g1["id"]}/members/{u4.id}', 200, ) g1 = test_client.req( 'delete', f'/api/v1/groups/{g1["id"]}/members/{u2.id}', 200, ) assert g1['members'] == []
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']