Example #1
0
def test_peer_feedback_and_group_assignments(test_client, logged_in, describe,
                                             admin_user):
    with describe('setup'), logged_in(admin_user):
        course = helpers.create_course(test_client)
        gset = helpers.create_group_set(test_client, course, 1, 2)

        def enable_group(assig, *, err=False, gset_id=helpers.get_id(gset)):
            return test_client.req(
                'patch',
                f'/api/v1/assignments/{helpers.get_id(assig)}',
                err or 200,
                data={'group_set_id': gset_id})

    with describe('Cannot enable peer feedback on group assignment'
                  ), logged_in(admin_user):
        assig = helpers.create_assignment(test_client, course)
        enable_group(assig)
        err = helpers.enable_peer_feedback(test_client, assig, err=400)
        assert 'This is a group assignment' in err['message']
        enable_group(assig, gset_id=None)
        helpers.enable_peer_feedback(test_client, assig)

    with describe('Cannot make group assignment if peer feedback is enabled'
                  ), logged_in(admin_user):
        assig = helpers.create_assignment(test_client, course)
        helpers.enable_peer_feedback(test_client, assig)
        err = enable_group(assig, err=400)
        assert 'This assignment has peer feedback enabled' in err['message']
        url = (f'/api/v1/assignments/{helpers.get_id(assig)}'
               '/peer_feedback_settings')
        test_client.req('delete', url, 204)
        enable_group(assig)
Example #2
0
def test_enabling_peer_feedback(test_client, session, describe, admin_user,
                                logged_in):
    with describe('setup'), logged_in(admin_user):
        course = helpers.create_course(test_client)
        assignment = helpers.create_assignment(test_client,
                                               course,
                                               state='open')
        user_with_perm = helpers.create_user_with_perms(
            session, [CPerm.can_edit_peer_feedback_settings], course)
        user_without_perm = helpers.create_user_with_perms(session, [], course)

    with describe('User without perm cannot enable'), logged_in(
            user_without_perm):
        helpers.enable_peer_feedback(test_client, assignment, err=403)

    with describe('User with perm can enable'), logged_in(user_with_perm):
        helpers.enable_peer_feedback(test_client, assignment)

    with describe('Amount should be >= 1'), logged_in(user_with_perm):
        helpers.enable_peer_feedback(test_client,
                                     assignment,
                                     amount=-1,
                                     err=400)
        helpers.enable_peer_feedback(test_client,
                                     assignment,
                                     amount=0,
                                     err=400)
        helpers.enable_peer_feedback(test_client, assignment, amount=110)

    with describe('Time should be > 0'), logged_in(user_with_perm):
        helpers.enable_peer_feedback(test_client, assignment, days=-1, err=400)
        helpers.enable_peer_feedback(test_client, assignment, days=0, err=400)
        helpers.enable_peer_feedback(test_client, assignment, days=0.005)
Example #3
0
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)
Example #4
0
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)
Example #5
0
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']
Example #6
0
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=[])
Example #7
0
def basic(logged_in, admin_user, test_client, session):
    print(m.User.query.all())
    with logged_in(admin_user):
        course = helpers.create_course(test_client)
        assig_id = helpers.create_assignment(test_client,
                                             course,
                                             deadline=datetime.utcnow() +
                                             timedelta(days=30))['id']
        teacher = helpers.create_user_with_role(session, 'Teacher', [course])
        student = helpers.create_user_with_role(session, 'Student', [course])
    yield course, assig_id, teacher, student
Example #8
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_has_permission_filter(describe, test_client, session, admin_user,
                               logged_in, perm):
    with describe('setup'), logged_in(admin_user):
        course = helpers.to_db_object(helpers.create_course(test_client),
                                      m.Course)

        rol1 = m.CourseRole('rol1', course, hidden=False)
        rol2 = m.CourseRole('rol2', course, hidden=False)
        session.add(rol1)
        session.add(rol2)
        session.commit()

        user1 = helpers.create_user_with_role(session, 'rol1', course)
        user2 = helpers.create_user_with_role(session, 'rol2', course)

        rol1.set_permission(perm, True)
        rol2.set_permission(perm, False)
        session.commit()

    with describe('Role is include if (and only if) the permission is true'):
        roles = m.CourseRole.get_roles_with_permission(perm).all()
        assert rol1 in roles
        assert rol2 not in roles

    with describe('Can be filtered further'):
        roles = m.CourseRole.get_roles_with_permission(perm).filter(
            m.CourseRole.course == course).all()
        assert all(r.course_id == course.id for r in roles)
        assert 'rol1' in [r.name for r in roles]
        assert 'Teacher' in [r.name for r in roles]

    with describe('User show up if permission is true not otherwise'):

        query = course.get_all_users_in_course(
            include_test_students=False,
            with_permission=perm,
        )
        assert sorted([user1.id,
                       admin_user.id]) == sorted(user.id for user, _ in query)
def test_get_self_from_course(
    describe, admin_user, test_client, session, app, logged_in
):
    with describe('setup'), logged_in(admin_user):
        course = helpers.to_db_object(
            helpers.create_course(test_client), m.Course
        )
        lti_course = helpers.to_db_object(
            helpers.create_lti_course(session, app, admin_user), m.Course
        )

    with describe('Getting without course should return none'):
        assert m.LTI1p1Provider._get_self_from_course(None) is None

    with describe('Getting for non LTI course should return none'):
        assert m.LTI1p1Provider._get_self_from_course(course) is None

    with describe('Getting with LTI course should return instance'):
        prov = m.LTI1p1Provider._get_self_from_course(lti_course)
        assert prov is not None
        assert lti_course.lti_provider.id == prov.id

    with describe('Getting for for other LTI class should return none'):
        assert m.LTI1p3Provider._get_self_from_course(lti_course) is None
Example #11
0
def basic(logged_in, admin_user, test_client, session):
    with logged_in(admin_user):
        course = helpers.create_course(test_client)
        assig_id = helpers.create_assignment(
            test_client,
            course,
            deadline=DatetimeWithTimezone.utcnow() + timedelta(days=30),
            state='open',
        )['id']
        assig = m.Assignment.query.get(assig_id)
        assert assig is not None
        teacher = helpers.create_user_with_role(session, 'Teacher', [course])
        student = helpers.create_user_with_role(session, 'Student', [course])
        with logged_in(teacher):
            test_client.req(
                'patch',
                f'/api/v1/assignments/{assig_id}',
                200,
                data={
                    'files_upload_enabled': True,
                    'webhook_upload_enabled': True,
                },
            )
    yield course, assig, teacher, student
Example #12
0
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)
Example #13
0
def test_delete_sub_with_cycle(
    test_client, admin_user, session, describe, logged_in, yesterday
):
    with describe('setup'), logged_in(admin_user):
        course = helpers.create_course(test_client)
        assignment = helpers.create_assignment(
            test_client, course, deadline=yesterday
        )
        users = [
            helpers.get_id(
                helpers.create_user_with_role(session, 'Student', course)
            ) for _ in range(5)
        ]
        user1, user2, user3, user4, user5 = users
        subs = {
            user: helpers.create_submission(
                test_client,
                assignment,
                for_user=user,
            )
            for user in users
        }
        helpers.enable_peer_feedback(test_client, assignment, amount=1)

    with describe('setup cycle'):
        assig_id = helpers.get_id(assignment)
        assignment = m.Assignment.query.get(assig_id)
        assert assignment is not None, 'Could not find assignment'
        pf_settings = assignment.peer_feedback_settings
        pf_settings.connections = []

        for reviewer, subject in [
            (user1, user2),
            (user2, user1),
            (user3, user4),
            (user4, user5),
            (user5, user3),
        ]:
            reviewer = m.User.query.get(reviewer)
            subject = m.User.query.get(subject)
            conn = m.AssignmentPeerFeedbackConnection(
                pf_settings, user=subject, peer_user=reviewer
            )
            assert str(reviewer.id) in repr(conn)
            assert str(subject.id) in repr(conn)

        session.commit()

        # Make sure the just setup connections are valid
        get_all_connections(assignment, 1)

    with describe('delete sub of user in small cycle'), logged_in(admin_user):
        _, rv = test_client.req(
            'delete',
            f'/api/v1/submissions/{helpers.get_id(subs[user2])}',
            204,
            include_response=True,
        )
        assert (
            'All connections for peer feedback were redivided because of this'
            ' deletion.'
        ) in rv.headers['warning']

        conns = get_all_connections(assignment, 1)
        assert set(conns[user1]) & {user3, user4, user5}
Example #14
0
def test_getting_files_from_proxy(test_client, logged_in, describe, session,
                                  admin_user, monkeypatch):
    with describe('setup'), logged_in(admin_user):
        course = helpers.create_course(test_client)
        assig = helpers.create_assignment(test_client,
                                          course,
                                          state='open',
                                          deadline='tomorrow')
        stud = helpers.create_user_with_role(session, 'Student', [course])

        sub_data = (
            (f'{os.path.dirname(__file__)}/../test_data/test_submissions/'
             'html.tar.gz'),
            'f.tar.gz',
        )
        s1 = helpers.create_submission(test_client,
                                       assig,
                                       submission_data=sub_data,
                                       for_user=stud)['id']

    with describe('Proxy allows views without active user'):
        with logged_in(stud):
            proxy = test_client.req('post',
                                    f'/api/v1/submissions/{s1}/proxy',
                                    200,
                                    data={
                                        'allow_remote_resources': True,
                                        'allow_remote_scripts': True,
                                        'teacher_revision': False,
                                    })['id']

        res = test_client.post(f'/api/v1/proxies/{proxy}/nested/index.html',
                               follow_redirects=False)
        assert res.status_code == 303
        res = test_client.get(res.headers['Location'])
        assert res.headers['Content-Type'] == 'text/html; charset=utf-8'
        assert res.headers[
            'Content-Security-Policy'] == "default-src * data: 'unsafe-eval' 'unsafe-inline'"

    with describe('Proxy should have correct content type'):
        res = test_client.get(f'/api/v1/proxies/fiets.jpg')
        assert res.status_code == 200
        assert res.headers['Content-Type'] == 'image/jpeg'

    with describe('Not found files should return a 404'):
        for f in ['nope', 'non_existing_dir/file', 'nested/nope']:
            res = test_client.get(f'/api/v1/proxies/{f}')
            assert res.status_code == 404

    with describe('A directory will try to retrieve the index.html'):
        res1 = test_client.get(f'/api/v1/proxies/nested/index.html')
        assert res1.status_code == 200

        res2 = test_client.get(f'/api/v1/proxies/nested')
        assert res2.status_code == 200

        res3 = test_client.get(f'/api/v1/proxies/nested')
        assert res3.status_code == 200

        assert len(res1.get_data()) > 1
        assert res1.get_data() == res2.get_data()
        assert res1.get_data() == res3.get_data()

    with describe('A path is required'):
        res = test_client.get(f'/api/v1/proxies/')
        assert res.status_code == 404

    with describe('When we find a dir 404 is returned'):
        monkeypatch.setattr(psef.v1.proxies, '_INDEX_FILES',
                            ['not_in_dir.html'])
        res = test_client.get(f'/api/v1/proxies/nested')
        assert res.status_code == 404

    with describe('A proxy should stop working when expired'):
        url = f'/api/v1/proxies/fiets.jpg'
        assert test_client.get(url).status_code == 200

        with freeze_time(datetime.utcnow() + timedelta(days=1)):
            assert test_client.get(url).status_code == 400

    with describe('A proxy cannot be used when the stat is deleted'):
        url = f'/api/v1/proxies/fiets.jpg'
        assert test_client.get(url).status_code == 200

        psef.models.Proxy.query.filter_by(id=proxy).update({
            'state':
            psef.models.ProxyState.deleted,
        })
        assert test_client.get(url).status_code == 404

        psef.models.Proxy.query.filter_by(id=proxy).update({
            'state':
            psef.models.ProxyState.in_use,
        })
        assert test_client.get(url).status_code == 200

    with describe('We cannot reused the proxy'):
        assert test_client.get('/api/v1/proxies/fiets.jpg').status_code == 200
        assert test_client.get(
            f'/api/v1/proxies/{proxy}/fiets.jpg').status_code == 200

        # After clearing cookies we cannot use the endpoint anymore
        test_client.cookie_jar.clear()
        assert test_client.get('/api/v1/proxies/fiets.jpg').status_code == 400
        assert test_client.get(
            f'/api/v1/proxies/{proxy}/fiets.jpg').status_code == 400

        # Cannot do a new post to the proxy endpoint
        res = test_client.post(f'/api/v1/proxies/{proxy}/nested/index.html',
                               follow_redirects=True)
        assert res.status_code == 404
        assert test_client.get('/api/v1/proxies/fiets.jpg').status_code == 400
Example #15
0
def test_teacher_revision_in_proxy(test_client, logged_in, describe, session,
                                   admin_user, app):
    with describe('setup'), logged_in(admin_user):
        course = helpers.create_course(test_client)
        assig = helpers.create_assignment(test_client,
                                          course,
                                          state='done',
                                          deadline='tomorrow')
        stud = helpers.create_user_with_role(session, 'Student', [course])

        sub_data = (
            (f'{os.path.dirname(__file__)}/../test_data/test_submissions/'
             'html.tar.gz'),
            'f.tar.gz',
        )
        s1 = helpers.create_submission(test_client,
                                       assig,
                                       submission_data=sub_data,
                                       for_user=stud)['id']
        files = test_client.req(
            'get',
            f'/api/v1/submissions/{s1}/files/',
            200,
        )

        # Get the file nested/index.html
        student_file_id = [
            n['id'] for f in files['entries'] for n in f.get('entries', [])
            if f['name'] == 'nested' and n['name'] == 'index.html'
        ][0]

        test_client.req(
            'patch',
            f'/api/v1/code/{student_file_id}',
            200,
            real_data='TEACHER FILE',
        )
        test_client.req(
            'post',
            f'/api/v1/submissions/{s1}/files/?path=f.tar.gz/nested/woo',
            200,
        )

        student_proxy = test_client.req('post',
                                        f'/api/v1/submissions/{s1}/proxy',
                                        200,
                                        data={
                                            'allow_remote_resources': True,
                                            'allow_remote_scripts': True,
                                            'teacher_revision': False,
                                        })['id']
        teacher_proxy = test_client.req('post',
                                        f'/api/v1/submissions/{s1}/proxy',
                                        200,
                                        data={
                                            'allow_remote_resources': True,
                                            'allow_remote_scripts': True,
                                            'teacher_revision': True,
                                        })['id']

    with describe(
            'Teacher revision proxy should return teacher rev'
    ), app.test_client() as t_client, app.test_client() as s_client:
        res = t_client.post(
            f'/api/v1/proxies/{teacher_proxy}/nested/index.html',
            follow_redirects=True)
        assert res.status_code == 200
        assert res.get_data() == b'TEACHER FILE'

        res = s_client.post(
            f'/api/v1/proxies/{student_proxy}/nested/index.html',
            follow_redirects=True)
        assert res.status_code == 200
        assert res.get_data() != b'TEACHER FILE'

        res = t_client.get(f'/api/v1/proxies/nested/woo')
        assert res.status_code == 200
        assert res.get_data() == b''

        res = s_client.get(f'/api/v1/proxies/nested/woo')
        assert res.status_code == 404
Example #16
0
def test_giving_peer_feedback_comments(
    test_client, admin_user, session, describe, logged_in, yesterday, tomorrow,
    auto_approved, make_add_reply
):
    with describe('setup'), logged_in(admin_user):
        course = helpers.create_course(test_client)
        assignment = helpers.create_assignment(
            test_client, course, deadline=tomorrow, state='open'
        )
        teacher = helpers.create_user_with_role(session, 'Teacher', course)
        users = [
            helpers.get_id(
                helpers.create_user_with_role(session, 'Student', course)
            ) for _ in range(5)
        ]
        user1, *other_users = users
        subs_by_user = {}
        for user in users:
            helpers.create_submission(test_client, assignment, for_user=user)
            # Every user has two submissions
            subs_by_user[user] = helpers.create_submission(
                test_client, assignment, for_user=user
            )

        helpers.enable_peer_feedback(
            test_client, assignment, amount=1, auto_approved=auto_approved
        )
        conns = get_all_connections(assignment, 1)[user1]
        other_user = next(u for u in other_users if u not in conns)
        base_url = f'/api/v1/assignments/{helpers.get_id(assignment)}'
        pf_sub = subs_by_user[conns[0]]
        own_sub = subs_by_user[user1]
        add_reply = make_add_reply(pf_sub)

        with logged_in(teacher):
            teacher_rep = add_reply('Hello')
            base_comment_id = teacher_rep['comment_base_id']
            teacher_rep.delete()

    with describe(
        'Before deadline we have no connections (and cannot place feedback)'
    ), logged_in(user1):
        test_client.req(
            'get',
            f'{base_url}/submissions/?latest_only',
            200,
            result=[{
                'id': int,
                'user': {
                    'id': user1,
                    '__allow_extra__': True,
                },
                '__allow_extra__': True,
            }]
        )
        add_reply(
            'Too early for a reply', expect_error=403, base_id=base_comment_id
        )

    with logged_in(teacher):
        test_client.req(
            'patch', base_url, 200, data={'deadline': yesterday.isoformat()}
        )

    with describe(
        'Will receive peer feedback submissions when getting all submissions'
    ), logged_in(user1):
        test_client.req(
            'get',
            f'{base_url}/submissions/?latest_only&extended',
            200,
            result=[pf_sub, own_sub],
        )
        # Can also get older subs of a assigned user
        test_client.req(
            'get',
            f'{base_url}/users/{conns[0]}/submissions/?extended',
            200,
            result=[
                pf_sub,
                {
                    '__allow_extra__': True,
                    'user': {'id': conns[0], '__allow_extra__': True},
                }
            ],
        )

    with describe('Can comment on other submission'), logged_in(user1):
        reply = add_reply('A peer feedback comment', expect_peer_feedback=True)
        assert reply['approved'] == auto_approved

    with describe('Cannot change approval status yourself'), logged_in(user1):
        reply.set_approval(not auto_approved, err=403)

    with describe('Teacher can change approval status'), logged_in(teacher):
        reply = reply.set_approval(not auto_approved)

    with describe('Editing feedback resets approval status'):
        with logged_in(user1):
            # We don't reset it back to ``True`` if ``auto_approved`` is
            # ``True`` but we do set it back to ``False``.
            reply = reply.update('New content', approved=False)

        if auto_approved:
            # If reply is approved an ``auto_approved`` is ``True`` the
            # approval state should not change.
            with logged_in(teacher):
                reply.set_approval(True)
            with logged_in(user1):
                reply = reply.update('New content2', approved=True)

    with describe(
        'Cannot place or edit peer feedback after pf deadline has expired'
    ):
        with logged_in(teacher):
            helpers.enable_peer_feedback(
                test_client,
                assignment,
                amount=1,
                auto_approved=auto_approved,
                days=0.5
            )
            assert get_all_connections(assignment, 1)[user1] == conns
        with logged_in(user1):
            add_reply('Another peer feedback comment', expect_error=403)

            reply.update('Cannot update!', err=403)

    with describe('Can always place peer feedback if pf time is None'):
        with logged_in(teacher):
            helpers.enable_peer_feedback(
                test_client,
                assignment,
                amount=1,
                auto_approved=auto_approved,
                days=None,
            )

        with freezegun.freeze_time(
            DatetimeWithTimezone.utcnow() + timedelta(days=365 * 20)
        ), logged_in(user1):
            reply = reply.update('Can update way after the deadline!')

    with describe('Cannot add peer feedback to a non assigned sub'
                  ), logged_in(other_user):
        add_reply(
            'Not possible to add this',
            expect_error=403,
            base_id=reply['comment_base_id']
        )

    with describe('Student can see approved peer feedback after done'):
        with logged_in(teacher):
            reply = reply.set_approval(not auto_approved)
            test_client.req(
                'get',
                f'/api/v1/submissions/{helpers.get_id(pf_sub)}/feedbacks/',
                200,
                query={'with_replies': '1'},
                result={
                    'general': '',
                    'linter': {},
                    'user': [{
                        'id': int,
                        '__allow_extra__': True,
                        'replies': [helpers.dict_without(reply, 'author'), ],
                    }],
                    'authors': [helpers.to_db_object(user1, m.User)],
                },
            )
            test_client.req('patch', base_url, 200, data={'state': 'done'})

        with logged_in(conns[0]):
            test_client.req(
                'get',
                f'/api/v1/submissions/{helpers.get_id(pf_sub)}/feedbacks/',
                200,
                query={'with_replies': '1'},
                result={
                    'general': '',
                    'linter': {},
                    'authors': ([helpers.to_db_object(user1, m.User)]
                                if reply['approved'] else []),
                    'user': ([{
                        'id': int,
                        '__allow_extra__': True,
                        'replies': [helpers.dict_without(reply, 'author')],
                    }] if reply['approved'] else []),
                },
            )
Example #17
0
def test_division(
    test_client, admin_user, session, describe, logged_in, yesterday, iteration
):
    with describe('setup'), logged_in(admin_user):
        course = helpers.create_course(test_client)
        assignment = helpers.create_assignment(
            test_client, course, deadline=yesterday
        )
        helpers.enable_peer_feedback(test_client, assignment)
        user1, user2, user3, user4 = [
            helpers.get_id(
                helpers.create_user_with_role(session, 'Student', course)
            ) for _ in range(4)
        ]

    with describe('Single submission no connections'), logged_in(admin_user):
        assert get_all_connections(assignment, 1) == {}
        helpers.create_submission(test_client, assignment, for_user=user1)
        assert get_all_connections(assignment, 1) == {}

    with describe('Second submission does initial division'
                  ), logged_in(admin_user):
        helpers.create_submission(test_client, assignment, for_user=user2)
        assert get_all_connections(assignment,
                                   1) == {user1: [user2], user2: [user1]}

    with describe('Third submission also gets divided'), logged_in(admin_user):
        helpers.create_submission(test_client, assignment, for_user=user3)
        connections = get_all_connections(assignment, 1)
        assert len(connections) == 3
        if connections[user3] == [user1]:
            assert connections[user2] == [user3]
            assert connections[user1] == [user2]
        else:
            assert connections[user3] == [user2]
            assert connections[user1] == [user3]
            assert connections[user2] == [user1]

    with describe('Submitting again does not change division'
                  ), logged_in(admin_user):
        old_connections = get_all_connections(assignment, 1)
        for _ in range(10):
            last_sub3 = helpers.create_submission(
                test_client, assignment, for_user=user3
            )
            assert get_all_connections(assignment, 1) == old_connections

    with describe('Deleting not last submission does not change division'
                  ), logged_in(admin_user):
        old_connections = get_all_connections(assignment, 1)
        _, rv = test_client.req(
            'delete',
            f'/api/v1/submissions/{helpers.get_id(last_sub3)}',
            204,
            include_response=True,
        )
        assert 'warning' not in rv.headers
        assert get_all_connections(assignment, 1) == old_connections

    with describe('Test submission does not change anything'
                  ), logged_in(admin_user):
        old_connections = get_all_connections(assignment, 1)
        for _ in range(10):
            helpers.create_submission(
                test_client, assignment, is_test_submission=True
            )
            assert get_all_connections(assignment, 1) == old_connections

    with describe('user gets assigned to different user every time'
                  ), logged_in(admin_user):
        conns = set()
        for _ in range(40):
            sub = helpers.create_submission(
                test_client, assignment, for_user=user4
            )
            new_conns = get_all_connections(assignment, 1)[user4]
            conns.add(new_conns[0])
            _, rv = test_client.req(
                'delete',
                f'/api/v1/submissions/{helpers.get_id(sub)}',
                204,
                include_response=True,
            )
            assert 'warning' not in rv.headers
        assert len(conns) == 3
def test_retrieve_users_in_course(
    describe, lti1p3_provider, stub_function_class, monkeypatch, test_client,
    admin_user, logged_in, session, app, watch_signal
):
    with describe('setup'), logged_in(admin_user):
        course = helpers.to_db_object(
            helpers.create_course(test_client), m.Course
        )
        membership_url = f'http://{uuid.uuid4()}'
        lti_course, _ = helpers.create_lti1p3_course(
            test_client,
            session,
            lti1p3_provider,
            membership_url,
        )
        return_value = []

        stub_get = stub_function_class(lambda: copy.deepcopy(return_value))
        monkeypatch.setattr(
            pylti1p3.names_roles.NamesRolesProvisioningService, 'get_members',
            stub_get
        )

        assig_created_signal = watch_signal(
            # Make sure we flush the db as we expect that the created
            # assignment can be found by doing queries.
            signals.ASSIGNMENT_CREATED,
            flush_db=True
        )
        user_added_signal = watch_signal(
            signals.USER_ADDED_TO_COURSE,
            clear_all_but=[m.LTI1p3Provider._retrieve_users_in_course]
        )

        new_user_id1 = str(uuid.uuid4())
        new_user_id2 = str(uuid.uuid4())

        do_poll_again = True
        stub_poll_again = stub_function_class(lambda: do_poll_again)
        monkeypatch.setattr(
            m.CourseLTIProvider, 'can_poll_names_again', stub_poll_again
        )

    with describe('make sure it is connected to the necessary signals'):
        assert signals.USER_ADDED_TO_COURSE.is_connected(
            m.LTI1p3Provider._retrieve_users_in_course
        )
        assert signals.ASSIGNMENT_CREATED.is_connected(
            m.LTI1p3Provider._retrieve_users_in_course
        )

    with describe('non lti courses should be ignored'):
        m.LTI1p3Provider._retrieve_users_in_course(course.id)
        assert not stub_get.called

    with describe('should work when no members are returned'):
        assig = helpers.create_lti1p3_assignment(session, lti_course)
        assert assig_created_signal.was_send_once
        assert stub_get.called
        assert user_added_signal.was_not_send
        assert stub_poll_again.called

    with describe('Should be possible to add members'):
        return_value = [
            {
                'status': 'Active',
                # Not correct at all, but the function should still not crash.
                'message': object(),
                'user_id': new_user_id1,
                'email': '*****@*****.**',
                'name': 'USER1',
            },
            {
                'status': 'Active',
                'message': {
                    claims.CUSTOM: {'cg_username_0': 'username_user2'}
                },
                'user_id': new_user_id2,
                'email': '*****@*****.**',
                'name': 'USER2',
                'roles': ['Student'],
            },
        ]
        signals.ASSIGNMENT_CREATED.send(assig)
        assert stub_poll_again.called
        assert user_added_signal.was_send_once

        # USER1 should not be added and recursion should not happen
        assert user_added_signal.called_amount == 1
        assert m.User.query.filter_by(username='******'
                                      ).one().is_enrolled(lti_course)
        assert m.User.query.filter_by(email='*****@*****.**'
                                      ).one_or_none() is None

    with describe('Can add known users to new courses, even without username'):
        # Remove the message claim
        return_value = [{**r, 'message': {}} for r in return_value]

        with logged_in(admin_user):
            lti_course2, _ = helpers.create_lti1p3_course(
                test_client, session, lti1p3_provider
            )

        helpers.create_lti1p3_assignment(session, lti_course2)

        assert user_added_signal.was_send_once
        assert m.User.query.filter_by(username='******'
                                      ).one().is_enrolled(lti_course2)
        assert m.User.query.filter_by(email='*****@*****.**'
                                      ).one_or_none() is None

    with describe('if can poll return no poll should be done'):
        do_poll_again = False
        signals.ASSIGNMENT_CREATED.send(assig)
        assert not stub_get.called
Example #19
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',
                },
            )
Example #20
0
def test_ensure_may_see_filter(logged_in, test_client, describe, admin_user,
                               session):
    with describe('setup'), logged_in(admin_user):
        course1 = helpers.create_course(test_client)
        course2 = helpers.create_course(test_client)
        course3 = helpers.create_course(test_client)

        archived_course1 = helpers.create_course(test_client)
        archived_course1 = m.Course.query.get(helpers.get_id(archived_course1))
        archived_course1.state = m.CourseState.archived
        session.commit()

        archived_course2 = helpers.create_course(test_client)
        archived_course2 = m.Course.query.get(helpers.get_id(archived_course2))
        archived_course2.state = m.CourseState.archived
        session.commit()

        deleted_course = helpers.create_course(test_client)
        deleted_course = m.Course.query.get(helpers.get_id(deleted_course))
        deleted_course.state = m.CourseState.deleted
        session.commit()

        user = m.User.resolve(
            helpers.create_user_with_role(
                session, 'Student',
                [course1, course2, archived_course1, deleted_course]))
        user.courses[archived_course2.id] = m.CourseRole.query.filter_by(
            name='Teacher', course=archived_course2).one()

    with describe('Returns no courses when not logged in'):
        assert not m.Course.query.filter(
            a.CoursePermissions.ensure_may_see_filter()).all()

    with describe('Returns all archived course as teacher when logged in'):
        with a.as_current_user(admin_user):
            query = m.Course.query.filter(
                a.CoursePermissions.ensure_may_see_filter()).with_entities(
                    m.Course.id)
            course_ids = sorted(c_id for c_id, in query)
            assert course_ids == sorted([
                helpers.get_id(c) for c in [
                    course1, course2, course3, archived_course1,
                    archived_course2
                ]
            ])

    with describe('Returns all courses when logged in'):
        with a.as_current_user(user):
            query = m.Course.query.filter(
                a.CoursePermissions.ensure_may_see_filter()).with_entities(
                    m.Course.id)
            course_ids = sorted(c_id for c_id, in query)
            assert course_ids == sorted([
                # We can see archived_course2, but not archived_course1
                helpers.get_id(c)
                for c in [course1, course2, archived_course2]
            ])

    with describe('Returns single courses when logged in for a single course'):
        with a.as_current_user(
                user, jwt_claims={'for_course': helpers.get_id(course2)}):
            course_id = m.Course.query.filter(
                a.CoursePermissions.ensure_may_see_filter()).with_entities(
                    m.Course.id).all()
            assert len(course_id) == 1
            assert course_id[0] == (helpers.get_id(course2), )

    with describe(
            'Returns no courses when logged in for course that the user is not'
            ' enrolled in'):
        with a.as_current_user(
                user, jwt_claims={'for_course': helpers.get_id(course3)}):
            assert not m.Course.query.filter(
                a.CoursePermissions.ensure_may_see_filter()).with_entities(
                    m.Course.id).all()