示例#1
0
def assignment(course_name, state_is_hidden, session, request, with_works):
    course = m.Course.query.filter_by(name=course_name).one()
    state = (
        m.AssignmentStateEnum.hidden
        if state_is_hidden else m.AssignmentStateEnum.open
    )
    assig = m.Assignment(
        name='TEST COURSE',
        state=state,
        course=course,
        deadline=DatetimeWithTimezone.utcnow() +
        datetime.timedelta(days=1 if request.param == 'new' else -1),
        is_lti=False,
    )
    session.add(assig)
    session.commit()

    if with_works:
        names = ['Student1', 'Student2', 'Student3', 'Œlµo']
        if with_works != 'single':
            names += names
        for uname in names:
            user = m.User.query.filter_by(name=uname).one()
            work = m.Work(assignment=assig, user=user)
            session.add(work)
        session.commit()

    yield assig
示例#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_data(db=None):
    db = db or psef.models.db

    if not app.config['DEBUG']:
        print('You can not add test data in production mode', file=sys.stderr)
        return 1

    seed()
    db.session.commit()
    with open(
        f'{os.path.dirname(os.path.abspath(__file__))}/test_data/courses.json',
        'r'
    ) as c:
        cs = json.load(c)
        for c in cs:
            if m.Course.query.filter_by(name=c['name']).first() is None:
                db.session.add(m.Course(name=c['name']))
    db.session.commit()
    with open(
        f'{os.path.dirname(os.path.abspath(__file__))}/test_data/assignments.json',
        'r'
    ) as c:
        cs = json.load(c)
        for c in cs:
            assig = m.Assignment.query.filter_by(name=c['name']).first()
            if assig is None:
                db.session.add(
                    m.Assignment(
                        name=c['name'],
                        deadline=datetime.datetime.utcnow() +
                        datetime.timedelta(days=c['deadline']),
                        state=c['state'],
                        description=c['description'],
                        course=m.Course.query.filter_by(name=c['course']
                                                        ).first()
                    )
                )
            else:
                assig.description = c['description']
                assig.state = c['state']
                assig.course = m.Course.query.filter_by(name=c['course']
                                                        ).first()
    db.session.commit()
    with open(
        f'{os.path.dirname(os.path.abspath(__file__))}/test_data/users.json',
        'r'
    ) as c:
        cs = json.load(c)
        for c in cs:
            u = m.User.query.filter_by(name=c['name']).first()
            courses = {
                m.Course.query.filter_by(name=name).first(): role
                for name, role in c['courses'].items()
            }
            perms = {
                course.id:
                m.CourseRole.query.filter_by(name=name,
                                             course_id=course.id).first()
                for course, name in courses.items()
            }
            username = c['name'].split(' ')[0].lower()
            if u is not None:
                u.name = c['name']
                u.courses = perms
                u.email = c['name'].replace(' ', '_').lower() + '@example.com'
                u.password = c['name']
                u.username = username
                u.role = m.Role.query.filter_by(name=c['role']).first()
            else:
                u = m.User(
                    name=c['name'],
                    courses=perms,
                    email=c['name'].replace(' ', '_').lower() + '@example.com',
                    password=c['name'],
                    username=username,
                    role=m.Role.query.filter_by(name=c['role']).first()
                )
                db.session.add(u)
                for course, role in courses.items():
                    if role == 'Student':
                        for assig in course.assignments:
                            work = m.Work(assignment=assig, user=u)
                            db.session.add(
                                m.File(
                                    work=work,
                                    name='Top stub dir',
                                    is_directory=True
                                )
                            )
                            db.session.add(work)
    db.session.commit()
    with open(
        f'{os.path.dirname(os.path.abspath(__file__))}/test_data/rubrics.json',
        'r'
    ) as c:
        cs = json.load(c)
        for c in cs:
            for row in c['rows']:
                assignment = m.Assignment.query.filter_by(
                    name=c['assignment']
                ).first()
                if assignment is not None:
                    rubric_row = m.RubricRow.query.filter_by(
                        header=row['header'],
                        description=row['description'],
                        assignment_id=assignment.id
                    ).first()
                    if rubric_row is None:
                        rubric_row = m.RubricRow(
                            header=row['header'],
                            description=row['description'],
                            assignment=assignment
                        )
                        db.session.add(rubric_row)
                    for item in row['items']:
                        if not db.session.query(
                            m.RubricItem.query.filter_by(
                                rubricrow_id=rubric_row.id,
                                **item,
                            ).exists()
                        ).scalar():
                            rubric_item = m.RubricItem(
                                description=item['description'] * 5,
                                header=item['header'],
                                points=item['points'],
                                rubricrow=rubric_row
                            )
                            db.session.add(rubric_item)
    db.session.commit()
示例#4
0
def test_data(db=None):
    db = psef.models.db if db is None else db

    if not app.config['DEBUG']:
        print('You can not add test data in production mode', file=sys.stderr)
        return 1
    if not os.path.isdir(app.config['UPLOAD_DIR']):
        os.mkdir(app.config['UPLOAD_DIR'])

    seed()
    db.session.commit()
    with open(
            f'{os.path.dirname(os.path.abspath(__file__))}/test_data/courses.json',
            'r') as c:
        cs = json.load(c)
        for c in cs:
            if m.Course.query.filter_by(name=c['name']).first() is None:
                db.session.add(m.Course.create_and_add(name=c['name']))
    db.session.commit()
    with open(
            f'{os.path.dirname(os.path.abspath(__file__))}/test_data/assignments.json',
            'r') as c:
        cs = json.load(c)
        for c in cs:
            assig = m.Assignment.query.filter_by(name=c['name']).first()
            if assig is None:
                db.session.add(
                    m.Assignment(
                        name=c['name'],
                        deadline=cg_dt_utils.now() +
                        datetime.timedelta(days=c['deadline']),
                        state=c['state'],
                        description=c['description'],
                        course=m.Course.query.filter_by(
                            name=c['course']).first(),
                        is_lti=False,
                    ))
            else:
                assig.description = c['description']
                assig.state = c['state']
                assig.course = m.Course.query.filter_by(
                    name=c['course']).first()
    db.session.commit()
    with open(
            f'{os.path.dirname(os.path.abspath(__file__))}/test_data/users.json',
            'r') as c:
        cs = json.load(c)
        for c in cs:
            u = m.User.query.filter_by(name=c['name']).first()
            courses = {
                m.Course.query.filter_by(name=name).first(): role
                for name, role in c['courses'].items()
            }
            perms = {
                course.id:
                m.CourseRole.query.filter_by(name=name,
                                             course_id=course.id).first()
                for course, name in courses.items()
            }
            username = c['name'].split(' ')[0].lower()
            if u is not None:
                u.name = c['name']
                u.courses = perms
                u.email = c['name'].replace(' ', '_').lower() + '@example.com'
                u.password = c['name']
                u.username = username
                u.role = m.Role.query.filter_by(name=c['role']).first()
            else:
                u = m.User(name=c['name'],
                           courses=perms,
                           email=c['name'].replace(' ', '_').lower() +
                           '@example.com',
                           password=c['name'],
                           username=username,
                           role=m.Role.query.filter_by(name=c['role']).first())
                db.session.add(u)
                for course, role in courses.items():
                    if role == 'Student':
                        for assig in course.assignments:
                            work = m.Work(assignment=assig, user=u)
                            f = m.File(work=work,
                                       name='Top stub dir ({})'.format(u.name),
                                       is_directory=True)
                            filename = str(uuid.uuid4())
                            shutil.copyfile(
                                __file__,
                                os.path.join(app.config['UPLOAD_DIR'],
                                             filename))
                            f.children = [
                                m.File(work=work,
                                       name='manage.py',
                                       is_directory=False,
                                       filename=filename)
                            ]
                            db.session.add(f)
                            db.session.add(work)
    db.session.commit()
    with open(
            f'{os.path.dirname(os.path.abspath(__file__))}/test_data/rubrics.json',
            'r') as c:
        cs = json.load(c)
        for c in cs:
            for row in c['rows']:
                assignment = m.Assignment.query.filter_by(
                    name=c['assignment']).first()
                if assignment is not None:
                    rubric_row = m.RubricRow.query.filter_by(
                        header=row['header'],
                        description=row['description'],
                        assignment_id=assignment.id,
                    ).first()
                    if rubric_row is None:
                        rubric_row = m.RubricRow(
                            header=row['header'],
                            description=row['description'],
                            assignment=assignment,
                            rubric_row_type='normal',
                        )
                        db.session.add(rubric_row)
                    for item in row['items']:
                        if not db.session.query(
                                m.RubricItem.query.filter_by(
                                    rubricrow_id=rubric_row.id,
                                    **item,
                                ).exists()).scalar():
                            rubric_item = m.RubricItem(
                                description=item['description'] * 5,
                                header=item['header'],
                                points=item['points'],
                                rubricrow=rubric_row)
                            db.session.add(rubric_item)
    db.session.commit()
示例#5
0
def upload_work(assignment_id: int) -> JSONResponse[models.Work]:
    """Upload one or more files as :class:`.models.Work` to the given
    :class:`.models.Assignment`.

    .. :quickref: Assignment; Create work by uploading a file.

    :query ignored_files: How to handle ignored files. The options are:
        ``ignore``: this the default, sipmly do nothing about ignored files,
        ``delete``: delete the ignored files, ``error``: raise an
        :py:class:`.APIException` when there are ignored files in the archive.
    :query author: The username of the user that should be the author of this
        new submission. Simply don't give this if you want to be the author.

    :param int assignment_id: The id of the assignment
    :returns: A JSON serialized work and with the status code 201.

    :raises APIException: If the request is bigger than the maximum upload
        size. (REQUEST_TOO_LARGE)
    :raises APIException: If there was no file in the request.
        (MISSING_REQUIRED_PARAM)
    :raises APIException: If some file was under the wrong key or some filename
        is empty. (INVALID_PARAM)
    """
    files = get_submission_files_from_request(check_size=True)
    assig = helpers.get_or_404(models.Assignment, assignment_id)
    given_author = request.args.get('author', None)

    if given_author is None:
        author = current_user
    else:
        author = helpers.filter_single_or_404(
            models.User,
            models.User.username == given_author,
        )

    auth.ensure_can_submit_work(assig, author)

    work = models.Work(assignment=assig, user_id=author.id)
    work.divide_new_work()
    db.session.add(work)

    try:
        raise_or_delete = psef.files.IgnoreHandling[request.args.get(
            'ignored_files',
            'keep',
        )]
    except KeyError:  # The enum value does not exist
        raise APIException(
            'The given value for "ignored_files" is invalid',
            (
                f'The value "{request.args.get("ignored_files")}" is'
                ' not in the `IgnoreHandling` enum'
            ),
            APICodes.INVALID_PARAM,
            400,
        )

    tree = psef.files.process_files(
        files,
        force_txt=False,
        ignore_filter=IgnoreFilterManager(assig.cgignore),
        handle_ignore=raise_or_delete,
    )
    work.add_file_tree(db.session, tree)
    db.session.flush()

    if assig.is_lti:
        work.passback_grade(initial=True)
    db.session.commit()

    work.run_linter()

    return jsonify(work, status_code=201)
示例#6
0
def post_submissions(assignment_id: int) -> EmptyResponse:
    """Add submissions to the  given:class:`.models.Assignment` from a
    blackboard zip file as :class:`.models.Work` objects.

    .. :quickref: Assignment; Create works from a blackboard zip.

    You should upload a file as multiform post request. The key should start
    with 'file'. Multiple blackboard zips are not supported and result in one
    zip being chosen at (psuedo) random.

    :param int assignment_id: The id of the assignment
    :returns: An empty response with return code 204

    :raises APIException: If no assignment with given id exists.
        (OBJECT_ID_NOT_FOUND)
    :raises APIException: If there was no file in the request.
        (MISSING_REQUIRED_PARAM)
    :raises APIException: If the file parameter name is incorrect or if the
        given file does not contain any valid submissions. (INVALID_PARAM)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user is not allowed to manage the
        course attached to the assignment. (INCORRECT_PERMISSION)
    """
    assignment = helpers.get_or_404(models.Assignment, assignment_id)
    auth.ensure_permission('can_upload_bb_zip', assignment.course_id)
    files = get_submission_files_from_request(check_size=False)

    try:
        submissions = psef.files.process_blackboard_zip(files[0])
    except Exception:  # pylint: disable=broad-except
        # TODO: Narrow this exception down.
        submissions = []

    if not submissions:
        raise APIException(
            "The blackboard zip could not imported or it was empty.",
            'The blackboard zip could not'
            ' be parsed or it did not contain any valid submissions.',
            APICodes.INVALID_PARAM, 400
        )

    missing, recalc_missing = assignment.get_divided_amount_missing()
    sub_lookup = {}
    for sub in assignment.get_all_latest_submissions():
        sub_lookup[sub.user_id] = sub

    student_course_role = models.CourseRole.query.filter_by(
        name='Student', course_id=assignment.course_id
    ).first()
    global_role = models.Role.query.filter_by(name='Student').first()

    subs = []
    hists = []

    found_users = {
        u.username: u
        for u in models.User.query.filter(
            t.cast(
                models.DbColumn[str],
                models.User.username,
            ).in_([si.student_id for si, _ in submissions])
        ).options(joinedload(models.User.courses))
    }

    newly_assigned: t.Set[t.Optional[int]] = set()

    for submission_info, submission_tree in submissions:
        user = found_users.get(submission_info.student_id, None)

        if user is None:
            # TODO: Check if this role still exists
            user = models.User(
                name=submission_info.student_name,
                username=submission_info.student_id,
                courses={assignment.course_id: student_course_role},
                email='',
                password=None,
                role=global_role,
            )
            found_users[user.username] = user
            # We don't need to track the users to insert as we are already
            # tracking the submissions of them and they are coupled.
        else:
            user.courses[assignment.course_id] = student_course_role

        work = models.Work(
            assignment=assignment,
            user=user,
            created_at=submission_info.created_at,
        )
        subs.append(work)

        if user.id is not None and user.id in sub_lookup:
            work.assigned_to = sub_lookup[user.id].assigned_to

        if work.assigned_to is None:
            if missing:
                work.assigned_to = max(
                    missing.keys(), key=lambda k: missing[k]
                )
                missing = recalc_missing(work.assigned_to)
                sub_lookup[user.id] = work

        hists.append(
            work.set_grade(
                submission_info.grade, current_user, add_to_session=False
            )
        )
        work.add_file_tree(db.session, submission_tree)
        if work.assigned_to is not None:
            newly_assigned.add(work.assigned_to)

    assignment.set_graders_to_not_done(
        list(newly_assigned),
        send_mail=True,
        ignore_errors=True,
    )

    db.session.bulk_save_objects(subs)
    db.session.flush()

    # TODO: This loop should be eliminated.
    for hist in hists:
        hist.work_id = hist.work.id
        hist.user_id = hist.user.id

    db.session.bulk_save_objects(hists)
    db.session.commit()

    return make_empty_response()