示例#1
0
        def test(*args):
            if not isinstance(args[0], tuple):
                args = [(student, work_id, *args)]

            test_client.req('get',
                            url,
                            200,
                            result={
                                'id': w_id,
                                'assignment_id': assignment['id'],
                                'student_submissions': {
                                    str(get_id(arg[0])): [{
                                        'id':
                                        get_id(arg[1]),
                                        'created_at':
                                        str,
                                        'grade':
                                        arg[2],
                                        'assignee_id':
                                        arg[3] and get_id(arg[3]),
                                    }]
                                    for arg in args
                                },
                                'data_sources': [str, str],
                            })
示例#2
0
def test_delete_submission(assignment_real_works, session, monkeypatch,
                           canvas_lti1p1_provider, stub_function_class,
                           describe):
    assignment, submission = assignment_real_works
    passback = stub_function_class(lambda: True)
    monkeypatch.setattr(psef.lti.v1_1.LTI, '_passback_grade', passback)

    def do_delete(was_latest, new_latest=None):
        psef.signals.WORK_DELETED.send(
            psef.signals.WorkDeletedData(
                deleted_work=m.Work.query.get(helpers.get_id(submission)),
                was_latest=was_latest,
                new_latest=new_latest,
            ))

    with describe('deleting submission without lti should work'):
        do_delete(True)
        assert not passback.called

        canvas_lti1p1_provider._delete_submission(
            (helpers.get_id(submission), assignment.id))
        assert not passback.called

    user_id = submission['user']['id']
    assignment.assignment_results[user_id] = m.AssignmentResult(
        sourcedid='wow', user_id=user_id)
    m.CourseLTIProvider.create_and_add(
        lti_context_id=str(uuid.uuid4()),
        course=assignment.course,
        lti_provider=canvas_lti1p1_provider,
        deployment_id='',
    )
    assignment.lti_grade_service_data = 'http://aaa'
    assignment.is_lti = True
    session.commit()

    with describe('deleting newest submission'):
        do_delete(was_latest=True)

        assert len(passback.all_args) == 1
        assert passback.all_args[0]['grade'] is None
        passback.reset()

    with describe('deleting non newest should not delete grade'):
        sub_new = m.Work(user_id=user_id, assignment=assignment)
        session.add(sub_new)
        session.commit()

        do_delete(was_latest=False, new_latest=None)
        assert not passback.called

    with describe('deleting in non existing assignment'):
        canvas_lti1p1_provider._delete_submission(
            (helpers.get_id(submission), None))
        assert not passback.called

    with describe('deleting in non existing submissions'):
        canvas_lti1p1_provider._delete_submission((-1, assignment.id))
        assert not passback.called
示例#3
0
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)
示例#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)
示例#5
0
 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}
     )
示例#6
0
    def assert_mailed(user, amount=1):
        if amount > 0:
            assert any_mails() and len(stubmailer.args) > 0, (
                'Nobody was mailed')

        user_id = helpers.get_id(user)
        user = m.User.query.get(user_id)
        assert user is not None, f'Given user {user_id} was not found'
        msgs = []

        for arg, in stubmailer.args:
            message = arg._message()
            recipients = message['To'].split(', ')
            assert recipients, 'A mail was send to nobody'
            for recipient in recipients:
                print(recipient)
                if '<{}>'.format(user.email) in recipient:
                    msgs.append(
                        dotdict(
                            orig=message,
                            msg=arg.html,
                            subject=message['Subject'],
                            message_id=message['Message-ID'],
                            in_reply_to=message['In-Reply-To'],
                            references=(message['References']
                                        and message['References'].split(' ')),
                        ))
                    amount -= 1

        assert amount == 0, 'The given user was not mailed or mailed to much'
        return msgs
示例#7
0
def get_all_connections(assignment, amount):
    assig_id = helpers.get_id(assignment)
    assignment = m.Assignment.query.get(assig_id)
    assert assignment is not None, 'Could not find assignment'
    pf_settings = assignment.peer_feedback_settings
    if amount == 0:
        assert pf_settings is None
        return
    assert pf_settings is not None, 'Not a PF assig'
    connections = sorted((conn.peer_user_id, conn.user_id)
                         for conn in pf_settings.connections)
    seen_amount = {}
    res = {}
    for a, b in connections:
        assert a != b
        if a not in res:
            res[a] = []
        if b not in seen_amount:
            seen_amount[b] = 0
        seen_amount[b] += 1
        assert b not in res[a]
        res[a].append(b)

    for a, conns in res.items():
        assert len(conns) == amount
        assert seen_amount[a] == amount

    return res
示例#8
0
 def do_delete(was_latest, new_latest=None):
     psef.signals.WORK_DELETED.send(
         psef.signals.WorkDeletedData(
             deleted_work=m.Work.query.get(helpers.get_id(submission)),
             was_latest=was_latest,
             new_latest=new_latest,
         ))
示例#9
0
def do_oidc_login(test_client,
                  provider,
                  with_id=False,
                  redirect_to=None,
                  override_data={}):
    provider = m.LTI1p3Provider.query.get(helpers.get_id(provider))

    redirect_uri = str(uuid.uuid4())
    login_hint = str(uuid.uuid4())
    url = '/api/v1/lti1.3/login'
    if with_id:
        url += '/' + str(provider.id)

    redirect = test_client.post(url,
                                data={
                                    'target_link_uri': redirect_uri,
                                    'iss': provider.iss,
                                    'client_id': provider.client_id,
                                    'login_hint': login_hint,
                                    **override_data,
                                })
    assert redirect.status_code == 303
    if redirect_to is None:
        assert redirect.headers['Location'].startswith(
            provider._auth_login_url)
    else:
        assert redirect.headers['Location'].startswith(redirect_to)
    return furl.furl(redirect.headers['Location'])
示例#10
0
def slow_dfs(node, visited_nodes, max_depth):
    node_id = get_id(node)
    list_of_search_moves.append(f'{node.clicked_index} {node_id}')

    if node_id in visited_nodes:
        return False
    visited_nodes.append(node_id)
    if is_all_zeros(node.game_board):
        while node.parent is not None:
            list_of_solution_moves.append(f'{node.clicked_index} {get_id(node)}')
            node = node.parent
        return True
    if node.depth == max_depth:
        #loop until we hit parent (in case where all children were visited)
        while node is not None:
            node = node.parent
            #cant find non-visited child anywhere, exist
            if node is None:
                return False
            #get next non-visited child of parent    
            next_node = get_next_nonvisited_child(node, visited_nodes)
            if next_node is not None:
                return slow_dfs(next_node, visited_nodes, max_depth)
          
        return False

    slow_build_boards(node)
    next_node = get_next_nonvisited_child(node, visited_nodes)
    if next_node is None:
        return False
    if slow_dfs(next_node, visited_nodes, max_depth):
        return True
    return False
示例#11
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=[])
示例#12
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)
示例#13
0
        def add_reply(
            txt,
            line=0,
            include_response=False,
            include_base=False,
            in_reply_to=None,
            expect_error=False,
        ):
            in_reply_to_id = in_reply_to and get_id(in_reply_to)
            base = test_client.req('put',
                                   '/api/v1/comments/',
                                   200,
                                   data={
                                       'file_id': code_id,
                                       'line': line,
                                   },
                                   result={
                                       'id': int,
                                       '__allow_extra__': True,
                                   })

            res, rv = test_client.req(
                'post',
                f'/api/v1/comments/{get_id(base)}/replies/',
                expect_error or 200,
                data={
                    'comment': txt,
                    'reply_type': 'markdown',
                    'in_reply_to': in_reply_to_id,
                },
                result=error_template if expect_error else {
                    'id': int,
                    'reply_type': 'markdown',
                    'comment': txt,
                    'in_reply_to_id': in_reply_to_id,
                    'last_edit': None,
                    '__allow_extra__': True,
                },
                include_response=True,
            )
            reply = Reply(res)
            result = (reply, rv) if include_response else reply

            if include_base:
                base['replies'].append({**res})
                base['replies'][-1].pop('author')
                base['replies'][-1].pop('comment_base_id')
                result = (result, base)

            return result
示例#14
0
 def test(items):
     test_client.req(
         'get',
         url,
         200,
         result={
             'name': 'rubric_data',
             'data': {
                 str(work_id): [{
                     'item_id': get_id(item),
                     'multiplier': 1.0,
                 } for item in items]
             },
         },
     )
示例#15
0
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]
示例#16
0
def make_launch_data(base, provider, override_replace={}):
    provider = m.LTI1p3Provider.query.get(helpers.get_id(provider))

    return replace_values(
        base,
        {
            'cg_username': '******',
            'cg_deadline': '',
            'cg_available_at': '',
            'cg_resource_id': '',
            'cg_lock_date': '',
            'Assignment.name': 'Test Assignment 1',
            'Course.label': 'Test Course 1',
            'Course.id': str(uuid.uuid4()),
            'User.id': str(uuid.uuid4()),
            'User.username': '******',
            'Assignment.id': str(uuid.uuid4()),
            'iss': provider.iss,
            'client_id': provider.client_id,
            **override_replace,
        },
    )
示例#17
0
def test_maybe_open_assignment(describe, session, test_client, logged_in,
                               admin_user, tomorrow, stub_function):
    with describe('setup'), logged_in(admin_user):
        assig = helpers.create_assignment(test_client)
        stub_apply = stub_function(psef.tasks._maybe_open_assignment_at_1,
                                   'apply_async')
        assig_id = helpers.get_id(assig)

    with describe('Can call without available_at set'):
        psef.tasks._maybe_open_assignment_at_1(assig_id)
        assert m.Assignment.query.get(assig_id).state.is_hidden
        assert not stub_apply.called

    with describe('Can call with non existant id'):
        psef.tasks._maybe_open_assignment_at_1(1000000000)
        assert not stub_apply.called

    with describe('Can call with non existant id'):
        m.Assignment.query.filter_by(id=assig_id).update(
            {'_available_at': tomorrow.isoformat()})
        psef.tasks._maybe_open_assignment_at_1(assig_id)
        assert stub_apply.called_amount == 1
        assert stub_apply.kwargs[0]['eta'] == tomorrow
示例#18
0
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']
示例#19
0
    def inner(work_id):
        code_id = session.query(m.File.id).filter(
            m.File.work_id == get_id(work_id),
            m.File.parent_id.isnot(None),
            m.File.name != '__init__',
        ).first()[0]

        def add_reply(txt,
                      line=0,
                      include_response=False,
                      include_base=False,
                      in_reply_to=None,
                      expect_error=False,
                      expect_peer_feedback=False,
                      base_id=None):
            in_reply_to_id = in_reply_to and get_id(in_reply_to)
            base = base_id or test_client.req('put',
                                              '/api/v1/comments/',
                                              200,
                                              data={
                                                  'file_id': code_id,
                                                  'line': line,
                                              },
                                              result={
                                                  'id': int,
                                                  '__allow_extra__': True,
                                              })

            res, rv = test_client.req(
                'post',
                f'/api/v1/comments/{get_id(base)}/replies/',
                expect_error or 200,
                data={
                    'comment': txt,
                    'reply_type': 'markdown',
                    'in_reply_to': in_reply_to_id,
                },
                result=error_template if expect_error else {
                    'id':
                    int,
                    'reply_type':
                    'markdown',
                    'comment':
                    txt,
                    'in_reply_to_id':
                    in_reply_to_id,
                    'last_edit':
                    None,
                    'comment_base_id':
                    get_id(base),
                    'comment_type':
                    ('peer_feedback' if expect_peer_feedback else 'normal'),
                    '__allow_extra__':
                    True,
                },
                include_response=True,
            )
            if expect_error:
                return (res, rv) if include_response else res
            reply = Reply(res)
            result = (reply, rv) if include_response else reply

            if include_base:
                base['replies'].append({**res})
                base['replies'][-1].pop('author')
                result = (result, base)

            return result

        return add_reply
示例#20
0
def test_getting_analytics_workspace(logged_in, test_client, session,
                                     admin_user, describe, tomorrow):
    with describe('setup'), logged_in(admin_user):
        assignment = helpers.create_assignment(test_client,
                                               state='open',
                                               deadline=tomorrow)
        course = assignment['course']
        teacher = admin_user
        student = helpers.create_user_with_role(session, 'Student', course)

        work_id = helpers.get_id(
            helpers.create_submission(test_client,
                                      assignment,
                                      for_user=student))

        base_url = (
            f'/api/v1/analytics/{assignment["analytics_workspace_ids"][0]}')
        url = base_url + '/data_sources/rubric_data'

        def test(items):
            test_client.req(
                'get',
                url,
                200,
                result={
                    'name': 'rubric_data',
                    'data': {
                        str(work_id): [{
                            'item_id': get_id(item),
                            'multiplier': 1.0,
                        } for item in items]
                    },
                },
            )

    with describe('by default there should be not rubric data source'
                  ), logged_in(teacher):
        test_client.req('get',
                        base_url,
                        200,
                        result={
                            'id': assignment['analytics_workspace_ids'][0],
                            'assignment_id': assignment['id'],
                            'student_submissions': {
                                str(get_id(student)): [{
                                    'id': work_id,
                                    'created_at': str,
                                    'grade': None,
                                    'assignee_id': None,
                                }],
                            },
                            'data_sources': lambda x: 'rubric_data' not in x,
                        })
        test_client.req('get', url, 404)

    with describe('After adding a rubric it should have the data source'
                  ), logged_in(teacher):
        rubric = test_client.req(
            'put',
            f'/api/v1/assignments/{get_id(assignment)}/rubrics/',
            200,
            data=RUBRIC)
        test_client.req('get',
                        base_url,
                        200,
                        result={
                            'id': assignment['analytics_workspace_ids'][0],
                            'assignment_id': assignment['id'],
                            'student_submissions': {
                                str(get_id(student)): [{
                                    'id': work_id,
                                    'created_at': str,
                                    'grade': None,
                                    'assignee_id': None,
                                }],
                            },
                            'data_sources': lambda x: 'rubric_data' in x,
                        })
        test([])

    with describe('After selecting there are returned'), logged_in(teacher):
        to_select = [
            get_rubric_item(rubric, 'My header', '10points'),
            get_rubric_item(rubric, 'My header2', '2points'),
        ]

        for idx, item in enumerate(to_select):
            test_client.req(
                'patch',
                f'/api/v1/submissions/{work_id}/rubricitems/{item["id"]}',
                204,
            )

            test(to_select[:idx + 1])

    with describe('Changing item in header works'), logged_in(teacher):
        item = get_rubric_item(rubric, 'My header', '5points')
        to_select[0] = item
        test_client.req(
            'patch',
            f'/api/v1/submissions/{work_id}/rubricitems/{item["id"]}',
            204,
        )

        test(to_select)

    with describe('students cannot access it'), logged_in(student):
        test_client.req('get', url, 403)
示例#21
0
def test_enabling_login_links(
    describe, test_client, logged_in, admin_user, yesterday, session,
    stub_function, app, monkeypatch, tomorrow, error_template
):
    with describe('setup'), logged_in(admin_user):
        send_mail_stub = stub_function(psef.mail, 'send_login_link_mail')
        assig = helpers.create_assignment(test_client)
        assig_id = helpers.get_id(assig)
        course = helpers.get_id(assig['course'])
        url = f'/api/v1/assignments/{assig_id}?no_course_in_assignment=t'
        teacher = helpers.create_user_with_perms(
            session, [
                CPerm.can_see_assignments,
                CPerm.can_see_hidden_assignments,
                CPerm.can_view_analytics,
                CPerm.can_edit_assignment_info,
            ], course
        )
        no_perm = helpers.create_user_with_perms(
            session, [
                CPerm.can_see_assignments,
                CPerm.can_see_hidden_assignments,
                CPerm.can_view_analytics,
            ], course
        )
        # Make sure there are users to email
        for _ in range(10):
            helpers.create_user_with_role(session, 'Student', course)

    with describe('cannot enable login links for wrong kind'
                  ), logged_in(teacher):
        test_client.req('patch', url, 409, data={'send_login_links': True})
        _, rv = test_client.req(
            'patch',
            url,
            200,
            data={
                'deadline': yesterday.isoformat(),
                'available_at': (yesterday - timedelta(minutes=1)).isoformat(),
                'kind': 'exam',
                'send_login_links': True,
            },
            result={
                '__allow_extra__': True,
                'send_login_links': True,
            },
            include_response=True,
        )
        warning = rv.headers['warning']
        assert re.match(r'.*deadline.*expired.*not send', warning)
        assert not send_mail_stub.called

    with describe('cannot enable login links for wrong kind'
                  ), logged_in(teacher):
        test_client.req('patch', url, 409, data={'kind': 'normal'})
        assert not send_mail_stub.called

    with describe('cannot change login links with incorrect perms'
                  ), logged_in(no_perm):
        test_client.req('patch', url, 403, data={'send_login_links': False})
        assert not send_mail_stub.called

    with describe('Setting again does nothing'), logged_in(teacher):
        old_token = m.Assignment.query.get(assig_id).send_login_links_token
        test_client.req(
            'patch',
            url,
            200,
            data={
                'send_login_links': True,
                'available_at': (yesterday - timedelta(minutes=1)).isoformat(),
            }
        )
        new_token = m.Assignment.query.get(assig_id).send_login_links_token
        assert new_token == old_token
        assert not send_mail_stub.called

    with describe('Changing available at reschedules links'
                  ), logged_in(teacher):
        old_token = m.Assignment.query.get(assig_id).send_login_links_token
        test_client.req(
            'patch',
            url,
            200,
            data={
                'send_login_links': True,
                'available_at': (yesterday - timedelta(minutes=2)).isoformat(),
            }
        )
        new_token = m.Assignment.query.get(assig_id).send_login_links_token
        assert new_token != old_token
        assert new_token is not None
        assert not send_mail_stub.called

    with describe('Disabling clears token'), logged_in(teacher):
        test_client.req('patch', url, 200, data={'send_login_links': False})
        new_token = m.Assignment.query.get(assig_id).send_login_links_token
        assert new_token is None
        assert not send_mail_stub.called

    with describe('Cannot enable login links with large availability window'
                  ), logged_in(teacher):
        psef.site_settings.Opt.EXAM_LOGIN_MAX_LENGTH.set_and_commit_value(
            'P1D'
        )
        test_client.req(
            'patch',
            url,
            409,
            data={
                'send_login_links': True,
                'deadline': tomorrow.isoformat(),
                'available_at': yesterday.isoformat(),
            },
            result={
                **error_template, 'message':
                    re.compile(
                        'Login links can only be enabled if the deadline is at'
                        ' most 24 hours after.*'
                    )
            }
        )
示例#22
0
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()
示例#23
0
def test_delete_feedback(logged_in, test_client, session, admin_user,
                         mail_functions, describe, tomorrow, make_add_reply):
    with describe('setup'), logged_in(admin_user):
        assignment = helpers.create_assignment(test_client,
                                               state='open',
                                               deadline=tomorrow)
        course = assignment['course']
        teacher = admin_user
        student = helpers.create_user_with_role(session, 'Student', course)
        ta = helpers.create_user_with_role(session, 'TA', course)

        work_id = helpers.get_id(
            helpers.create_submission(test_client,
                                      assignment,
                                      for_user=student))

        add_reply = make_add_reply(work_id)
        feedback_url = f'/api/v1/submissions/{work_id}/feedbacks/?with_replies'

    with describe('Users can delete own replies'):
        for user in [teacher, student, ta]:
            with logged_in(user):
                reply = add_reply('a reply')
                test_client.req(
                    'delete', (f'/api/v1/comments/{reply["comment_base_id"]}/'
                               f'replies/{reply["id"]}'), 204)
                with logged_in(teacher):
                    test_client.req(
                        'get',
                        feedback_url,
                        200,
                        result={
                            'general': '',
                            'linter': {},
                            'authors': [],
                            'user': []
                        },
                    )

    with describe('Only teachers can delete replies of others'):
        with logged_in(student):
            reply, base = add_reply('a reply', include_base=True)
        with logged_in(ta):
            test_client.req('delete',
                            (f'/api/v1/comments/{reply["comment_base_id"]}/'
                             f'replies/{reply["id"]}'), 403)
        with logged_in(teacher):
            test_client.req(
                'get',
                feedback_url,
                200,
                result={
                    'general': '',
                    'linter': {},
                    'authors': [student],
                    'user': [base],
                },
            )

        with logged_in(teacher):
            test_client.req('delete',
                            (f'/api/v1/comments/{reply["comment_base_id"]}/'
                             f'replies/{reply["id"]}'), 204)
            test_client.req(
                'get',
                feedback_url,
                200,
                result={
                    'general': '',
                    'linter': {},
                    'authors': [],
                    'user': []
                },
            )

    with describe('Cannot delete a reply twice'), logged_in(student):
        reply = add_reply('To delete')
        test_client.req('delete',
                        (f'/api/v1/comments/{reply["comment_base_id"]}/'
                         f'replies/{reply["id"]}'), 204)
        test_client.req('delete',
                        (f'/api/v1/comments/{reply["comment_base_id"]}/'
                         f'replies/{reply["id"]}'), 404)
# miscellanous functions
import helpers as hp

# hash of hash.
# the first indice is the time difference between two
# coordinated objects. the second one is referring to a
# session. if a session refered by i has a value of 5, it
# means that it has 5 correlated objects with a predecessor at
# exactly i times steps behind.
session_sequences = {}
# keeps track of the number of events taken into account for one
# session in order to normalize
session_norm = {}
for file_path in hp.get_csv():
# iterating over the files (sessions)
    session_id = hp.get_id(file_path)
    # The Id in an integer
    session_id = int(session_id)
    session_norm[session_id] = 0
    # the name of the session is used as a table name in the
    # database
    session_name = hp.get_name(file_path)
    # array of arrays, stores for each coordinated object the
    # list of timespans between student switches
    event_time_differences = []

    con = lite.connect('mysteries.db')
    with con:
        cur = con.cursor()
        # Gets the list of all distinct objects
        cur.execute("\
示例#25
0
def test_get_entire_workspace(logged_in, test_client, session, admin_user,
                              describe, tomorrow):
    with describe('setup'), logged_in(admin_user):
        assignment = helpers.create_assignment(test_client,
                                               state='open',
                                               deadline=tomorrow)
        course = assignment['course']
        teacher = admin_user
        student = helpers.create_user_with_role(session, 'Student', course)
        w_id, = assignment["analytics_workspace_ids"]

        work_id = helpers.get_id(
            helpers.create_submission(test_client,
                                      assignment,
                                      for_user=student))

        url = f'/api/v1/analytics/{w_id}'

        rubric = test_client.req(
            'put',
            f'/api/v1/assignments/{get_id(assignment)}/rubrics/',
            200,
            data=RUBRIC)

        def test(*args):
            if not isinstance(args[0], tuple):
                args = [(student, work_id, *args)]

            test_client.req('get',
                            url,
                            200,
                            result={
                                'id': w_id,
                                'assignment_id': assignment['id'],
                                'student_submissions': {
                                    str(get_id(arg[0])): [{
                                        'id':
                                        get_id(arg[1]),
                                        'created_at':
                                        str,
                                        'grade':
                                        arg[2],
                                        'assignee_id':
                                        arg[3] and get_id(arg[3]),
                                    }]
                                    for arg in args
                                },
                                'data_sources': [str, str],
                            })

    with describe('should send the current grade'), logged_in(teacher):
        test(None, None)

        for item in [
                get_rubric_item(rubric, 'My header', '10points'),
                get_rubric_item(rubric, 'My header2', '2points'),
        ]:
            test_client.req(
                'patch',
                f'/api/v1/submissions/{work_id}/rubricitems/{item["id"]}',
                204,
            )

        test(10, None)

        test_client.req('patch',
                        f'/api/v1/submissions/{work_id}',
                        200,
                        data={'grade': 0.5})
        test(0.5, None)

        test_client.req('patch',
                        f'/api/v1/submissions/{work_id}',
                        200,
                        data={'grade': None})
        test(10, None)

    with describe('should show the assignee'), logged_in(teacher):
        test_client.req('patch',
                        f'/api/v1/submissions/{work_id}/grader',
                        204,
                        data={'user_id': get_id(teacher)})

        test(10, teacher)

    with describe('should not show test students'), logged_in(teacher):
        helpers.create_submission(test_client,
                                  assignment,
                                  is_test_submission=True)

        test(10, teacher)

    with describe('Should show multiple students'), logged_in(teacher):
        student2 = helpers.create_user_with_role(session, 'Student', course)
        work2 = helpers.create_submission(test_client,
                                          assignment,
                                          for_user=student2)
        test((student, work_id, 10, teacher), (student2, work2, None, None))

    with describe('students cannot access it'), logged_in(student):
        test_client.req('get', url, 403)
示例#26
0
def test_send_login_links(describe, session, test_client, logged_in,
                          admin_user, tomorrow, stub_function, yesterday):
    with describe('setup'), logged_in(admin_user):
        stub_apply = stub_function(psef.tasks._send_login_links_to_users_1,
                                   'apply_async')
        stub_mail = stub_function(psef.mail, 'send_login_link_mail')

        assig = helpers.create_assignment(test_client)
        assig_id = helpers.get_id(assig)
        login_links_token = uuid.uuid4()
        m.Assignment.query.filter_by(id=assig_id).update({
            '_available_at':
            tomorrow.isoformat(),
            '_deadline': (tomorrow + timedelta(minutes=1)).isoformat(),
            '_send_login_links_token':
            login_links_token,
            'kind':
            'exam',
        })

        task_result = m.TaskResult(user=None)
        session.add(task_result)
        session.commit()

        student = helpers.create_user_with_role(session, 'Student',
                                                assig['course'])

    with describe('does not crash if task does not exist'):
        psef.tasks._send_login_links_to_users_1(assig_id,
                                                uuid.uuid4().hex,
                                                tomorrow.isoformat(),
                                                login_links_token, 1)
        assert task_result.state.is_not_started

    with describe('reschedules when called too early'):
        psef.tasks._send_login_links_to_users_1(assig_id, task_result.id.hex,
                                                tomorrow.isoformat(),
                                                login_links_token.hex, 1)

        assert stub_apply.called_amount == 1
        assert task_result.state.is_not_started

    with describe('Does nothing when token is different'):
        psef.tasks._send_login_links_to_users_1(assig_id, task_result.id.hex,
                                                yesterday.isoformat(),
                                                uuid.uuid4().hex, 1)
        assert not stub_mail.called
        assert task_result.state.is_skipped
        task_result.state = m.TaskResultState.not_started
        session.commit()

    with describe('Does nothing when deadline expired'):
        del flask.g.request_start_time
        with freeze_time(tomorrow + timedelta(days=1)):
            psef.tasks._send_login_links_to_users_1(assig_id,
                                                    task_result.id.hex,
                                                    yesterday.isoformat(),
                                                    login_links_token.hex, 1)

        assert not stub_mail.called
        assert task_result.state.is_skipped
        task_result.state = m.TaskResultState.not_started
        session.commit()

    with describe('Does nothing when task was already finished'):
        task_result.state = m.TaskResultState.finished
        session.commit()

        psef.tasks._send_login_links_to_users_1(assig_id, task_result.id.hex,
                                                yesterday.isoformat(),
                                                login_links_token.hex, 1)
        assert not stub_mail.called
        assert task_result.state.is_finished
示例#27
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
示例#28
0
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
示例#29
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)
示例#30
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',
                },
            )
示例#31
0
def test_getting_inline_feedback_analytics(logged_in, test_client, session,
                                           admin_user, describe, tomorrow,
                                           make_add_reply):
    with describe('setup'), logged_in(admin_user):
        assignment = helpers.create_assignment(test_client,
                                               state='open',
                                               deadline=tomorrow)
        course = assignment['course']
        teacher = admin_user
        student = helpers.create_user_with_role(session, 'Student', course)

        work_id = helpers.get_id(
            helpers.create_submission(test_client,
                                      assignment,
                                      for_user=student))

        add_reply = make_add_reply(work_id)
        base_url = (
            f'/api/v1/analytics/{assignment["analytics_workspace_ids"][0]}')
        url = base_url + '/data_sources/inline_feedback'

        def test(amount):
            test_client.req(
                'get',
                url,
                200,
                result={
                    'name': 'inline_feedback',
                    'data': {
                        str(work_id): {
                            'total_amount': amount,
                        }
                    },
                },
            )

    with describe('should be includes as data source'), logged_in(teacher):
        test_client.req('get',
                        base_url,
                        200,
                        result={
                            'id': assignment['analytics_workspace_ids'][0],
                            'assignment_id': assignment['id'],
                            'student_submissions': {
                                str(get_id(student)): [{
                                    'id': work_id,
                                    'created_at': str,
                                    'grade': None,
                                    'assignee_id': None,
                                }],
                            },
                            'data_sources': lambda x: 'inline_feedback' in x,
                        })

    with describe('standard no comments'), logged_in(teacher):
        test(0)

    with describe('after adding reply it should return 1'), logged_in(teacher):
        r1 = add_reply('A reply', line=1)
        test(1)

    with describe('after two replies on 1 line should return 1'), logged_in(
            teacher):
        r2 = add_reply('A reply on the reply', line=1)
        test(1)

    with describe('a reply on another line should return more'), logged_in(
            teacher):
        r3 = add_reply('A new line reply', line=2)
        r4 = add_reply('WOW MUCH REPLIES', line=1000)
        test(3)

    with describe('Updating should not change a thing'), logged_in(teacher):
        test(3)
        r4 = r4.update('wow new text')
        test(3)

    with describe('after deleting replies it should be back at 0'), logged_in(
            teacher):
        # Should begin with 3
        test(3)

        # There is another comment on this line so it should stay the same
        r1.delete()
        test(3)
        r3.delete()
        test(2)
        r4.delete()
        test(1)
        r2.delete()
        test(0)

    with describe('students cannot access it'), logged_in(student):
        test_client.req('get', url, 403)