예제 #1
0
def delete_snippets(snippet_id: int) -> EmptyResponse:
    """Delete the :class:`.models.Snippet` with the given id.

    .. :quickref: Snippet; Delete a snippet.

    :param int snippet_id: The id of the snippet
    :returns: An empty response with return code 204

    :raises APIException: If the snippet with the given id does not exist.
                          (OBJECT_ID_NOT_FOUND)
    :raises APIException: If the snippet does not belong the current user.
                          (INCORRECT_PERMISSION)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not use snippets.
                                 (INCORRECT_PERMISSION)
    """
    snip: t.Optional[models.Snippet]
    snip = helpers.get_or_404(models.Snippet, snippet_id)
    snip = models.Snippet.query.get(snippet_id)
    assert snip is not None

    if snip.user_id != current_user.id:
        raise APIException(
            'The given snippet is not your snippet',
            'The snippet "{}" does not belong to user "{}"'.format(
                snip.id, current_user.id), APICodes.INCORRECT_PERMISSION, 403)
    else:
        db.session.delete(snip)
        db.session.commit()
        return make_empty_response()
예제 #2
0
def unselect_rubric_item(submission_id: int,
                         rubric_item_id: int) -> EmptyResponse:
    """Unselect the given rubric item for the given submission.

    .. :quickref: Submission; Unselect the given rubric item.

    :param submission_id: The submission to unselect the item for.
    :param rubric_item_id: The rubric items id to unselect.
    :returns: Nothing.
    """
    submission = helpers.get_or_404(models.Work, submission_id)

    auth.ensure_permission('can_grade_work', submission.assignment.course_id)

    new_items = [
        item for item in submission.selected_items if item.id != rubric_item_id
    ]
    if len(new_items) == len(submission.selected_items):
        raise APIException(
            'Selected rubric item was not selected for this submission',
            f'The item {rubric_item_id} is not selected for {submission_id}',
            APICodes.INVALID_PARAM, 400)

    submission.selected_items = new_items
    db.session.commit()

    return make_empty_response()
예제 #3
0
def delete_submission(submission_id: int) -> EmptyResponse:
    """Delete a submission and all its files.

    .. :quickref: Submission; Delete a submission and all its files.

    .. warning::

        This is irreversible, so make sure the user really wants this!

    :param submission_id: The submission to delete.
    :returns: Nothing
    """
    submission = helpers.get_or_404(models.Work, submission_id)

    auth.ensure_permission('can_delete_submission',
                           submission.assignment.course_id)

    for sub_file in db.session.query(models.File).filter_by(
            work_id=submission_id, is_directory=False).all():
        try:
            sub_file.delete_from_disk()
        except FileNotFoundError:  # pragma: no cover
            pass

    db.session.delete(submission)
    db.session.commit()

    return make_empty_response()
예제 #4
0
def delete_role(course_id: int, role_id: int) -> EmptyResponse:
    """Remove a :class:`.models.CourseRole` from the given
    :class:`.models.Course`.

    .. :quickref: Course; Delete a course role from a course.

    :param int course_id: The id of the course
    :returns: An empty response with return code 204

    :raises APIException: If the role with the given ids does not exist.
        (OBJECT_NOT_FOUND)
    :raises APIException: If there are still users with this role.
        (INVALID_PARAM)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not manage the course with the
        given id. (INCORRECT_PERMISSION)
    """
    auth.ensure_permission(CPerm.can_edit_course_roles, course_id)

    course = helpers.get_or_404(
        models.Course,
        course_id,
        also_error=lambda c: c.virtual,
    )
    role = helpers.filter_single_or_404(
        models.CourseRole,
        models.CourseRole.course_id == course_id,
        models.CourseRole.id == role_id,
        also_error=lambda r: r.hidden,
    )

    if course.lti_provider is not None:
        if LTICourseRole.codegrade_role_name_used(role.name):
            lms = course.lti_provider.lms_name
            raise APIException(
                f'You cannot delete default {lms} roles',
                ('The course "{}" is an LTI course so it is impossible to '
                 'delete role {}').format(course.id, role.id),
                APICodes.INCORRECT_PERMISSION, 403)

    users_with_role = db.session.query(models.user_course).filter(
        models.user_course.c.course_id == role_id).exists()
    if db.session.query(users_with_role).scalar():
        raise APIException(
            'There are still users with this role',
            'There are still users with role {}'.format(role_id),
            APICodes.INVALID_PARAM, 400)
    links_with_role = db.session.query(
        models.CourseRegistrationLink).filter_by(
            course_role_id=role_id).exists()
    if db.session.query(links_with_role).scalar():
        raise APIException(
            'There are still registration links with this role',
            f'The role "{role_id}" cannot be deleted as it is still in use',
            APICodes.INVALID_PARAM, 400)

    db.session.delete(role)
    db.session.commit()

    return make_empty_response()
예제 #5
0
def delete_rubric(assignment_id: int) -> EmptyResponse:
    """Delete the rubric for the given assignment.

    .. :quickref: Assignment; Delete the rubric of an assignment.

    :param assignment_id: The id of the :class:`.models.Assignment` whose
        rubric should be deleted.
    :returns: Nothing.

    :raises PermissionException: If the user does not have the
        ``manage_rubrics`` permission (INCORRECT_PERMISSION).
    :raises APIException: If the assignment has no rubric.
        (OBJECT_ID_NOT_FOUND)
    """
    assig = helpers.get_or_404(models.Assignment, assignment_id)
    auth.ensure_permission('manage_rubrics', assig.course_id)

    if not assig.rubric_rows:
        raise APIException(
            'Assignment has no rubric',
            'The assignment with id "{}" has no rubric'.format(assignment_id),
            APICodes.OBJECT_ID_NOT_FOUND, 404
        )

    assig.rubric_rows = []

    db.session.commit()

    return make_empty_response()
예제 #6
0
def delete_course_snippets(course_id: int, snippet_id: int) -> EmptyResponse:
    """Delete the :class:`.models.CourseSnippet` with the given id.

    .. :quickref: CourseSnippet; Delete a course snippet.

    :param int snippet_id: The id of the snippet
    :returns: An empty response with return code 204

    :raises APIException: If the snippet with the given id does not exist.
        (OBJECT_ID_NOT_FOUND)
    :raises APIException: If the snippet does not belong the current user.
        (INCORRECT_PERMISSION)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not use snippets.
        (INCORRECT_PERMISSION)
    """
    auth.ensure_permission(CPerm.can_manage_course_snippets, course_id)

    course = helpers.get_or_404(models.Course, course_id)
    snip = helpers.get_or_404(
        models.CourseSnippet,
        snippet_id,
        also_error=lambda snip: snip.course_id != course.id)

    db.session.delete(snip)
    db.session.commit()
    return make_empty_response()
예제 #7
0
def remove_comment(code_id: int, line: int) -> EmptyResponse:
    """Removes the given :class:`.models.Comment` in the given
    :class:`.models.File`

    .. :quickref: Code; Remove a comment.

    :param int code_id: The id of the code file
    :param int line: The line number of the comment
    :returns: An empty response with return code 204

    :raises APIException: If there is no comment at the given line number.
                          (OBJECT_NOT_FOUND)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not can grade work in the
                                 attached course. (INCORRECT_PERMISSION)
    """
    comment = helpers.filter_single_or_404(
        models.Comment, models.Comment.file_id == code_id,
        models.Comment.line == line
    )

    auth.ensure_permission(
        'can_grade_work', comment.file.work.assignment.course_id
    )
    db.session.delete(comment)
    db.session.commit()

    return make_empty_response()
예제 #8
0
def select_rubric_item(submission_id: int,
                       rubricitem_id: int) -> EmptyResponse:
    """Select a rubric item of the given submission (:class:`.models.Work`).

    .. :quickref: Submission; Select a rubric item.

    :param int submission_id: The id of the submission
    :param int rubricitem_id: The id of the rubric item
    :returns: Nothing.

    :raises APIException: If either the submission or rubric item with the
                          given ids does not exist. (OBJECT_ID_NOT_FOUND)
    :raises APIException: If the assignment of the rubric is not the assignment
                          of the submission. (INVALID_PARAM)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not grade the given submission
                                 (INCORRECT_PERMISSION)
    """
    work = helpers.get_or_404(models.Work, submission_id)
    rubric_item = helpers.get_or_404(models.RubricItem, rubricitem_id)

    auth.ensure_permission('can_grade_work', work.assignment.course_id)
    if rubric_item.rubricrow.assignment_id != work.assignment_id:
        raise APIException(
            'Rubric item selected does not match assignment',
            'The rubric item with id {} does not match the assignment'.format(
                rubricitem_id), APICodes.INVALID_PARAM, 400)

    work.remove_selected_rubric_item(rubric_item.rubricrow_id)
    work.select_rubric_items([rubric_item], current_user, False)
    db.session.commit()

    return make_empty_response()
예제 #9
0
def user_patch_handle_change_user_data() -> EmptyResponse:
    """Handle the PATCH login route when no ``type`` is given.

    :returns: An empty response.
    """
    data = ensure_json_dict(
        request.get_json(),
        replace_log=lambda k, v: f'<PASSWORD "{k}">' if 'password' in k else v
    )

    ensure_keys_in_dict(
        data, [
            ('email', str), ('old_password', str), ('name', str),
            ('new_password', str)
        ]
    )
    email = t.cast(str, data['email'])
    old_password = t.cast(str, data['old_password'])
    new_password = t.cast(str, data['new_password'])
    name = t.cast(str, data['name'])

    def _ensure_password(
        changed: str,
        msg: str = 'To change your {} you need a correct old password.'
    ) -> None:
        if current_user.password != old_password:
            raise APIException(
                msg.format(changed), 'The given old password was not correct',
                APICodes.INVALID_CREDENTIALS, 403
            )

    if old_password != '':
        _ensure_password('', 'The given old password is wrong')

    if current_user.email != email:
        auth.ensure_permission(GPerm.can_edit_own_info)
        _ensure_password('email')
        validate.ensure_valid_email(email)
        current_user.email = email

    if new_password != '':
        auth.ensure_permission(GPerm.can_edit_own_password)
        _ensure_password('password')
        validate.ensure_valid_password(new_password, user=current_user)
        current_user.password = new_password

    if current_user.name != name:
        auth.ensure_permission(GPerm.can_edit_own_info)
        if name == '':
            raise APIException(
                'Your new name cannot be empty',
                'The given new name was empty', APICodes.INVALID_PARAM, 400
            )
        current_user.name = name

    db.session.commit()
    return make_empty_response()
예제 #10
0
def user_patch_handle_reset_on_lti() -> EmptyResponse:
    """Handle the ``reset_on_lti`` type for the PATCH login route.

    :returns: An empty response.
    """
    auth.ensure_logged_in()
    current_user.reset_email_on_lti = True
    db.session.commit()

    return make_empty_response()
예제 #11
0
def update_role(course_id: int, role_id: int) -> EmptyResponse:
    """Update the :class:`.models.Permission` of a given
    :class:`.models.CourseRole` in the given :class:`.models.Course`.

    .. :quickref: Course; Update a permission for a certain role.

    :param int course_id: The id of the course.
    :param int role_id: The id of the course role.
    :returns: An empty response with return code 204.

    :<json str permission: The name of the permission to change.
    :<json bool value: The value to set the permission to (``True`` means the
        specified role has the specified permission).

    :raises APIException: If the value or permission parameter are not in the
                          request. (MISSING_REQUIRED_PARAM)
    :raises APIException: If the role with the given id does not exist or the
                          permission with the given name does not exist.
                          (OBJECT_NOT_FOUND)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not manage the course with the
                                 given id. (INCORRECT_PERMISSION)
    """
    content = ensure_json_dict(request.get_json())

    auth.ensure_permission('can_edit_course_roles', course_id)

    ensure_keys_in_dict(content, [('value', bool), ('permission', str)])
    value = t.cast(bool, content['value'])
    permission = t.cast(str, content['permission'])

    role = helpers.filter_single_or_404(
        models.CourseRole,
        models.CourseRole.course_id == course_id,
        models.CourseRole.id == role_id,
    )
    perm = helpers.filter_single_or_404(
        models.Permission,
        models.Permission.name == permission,
        models.Permission.course_permission == True,  # pylint: disable=singleton-comparison
    )

    if (current_user.courses[course_id].id == role.id
            and perm.name == 'can_edit_course_roles'):
        raise APIException(
            'You cannot remove this permission from your own role',
            ('The current user is in role {} which'
             ' cannot remove "can_edit_course_roles"').format(role.id),
            APICodes.INCORRECT_PERMISSION, 403)

    role.set_permission(perm, value)

    db.session.commit()

    return make_empty_response()
예제 #12
0
def patch_course_snippet(course_id: int, snippet_id: int) -> EmptyResponse:
    """Modify the :class:`.models.CourseSnippet` with the given id.

    .. :quickref: CourseSnippet; Change a snippets key and value.

    :param int snippet_id: The id of the snippet to change.
    :returns: An empty response with return code 204.

    :>json str key: The new key of the snippet.
    :>json str value: The new value of the snippet.

    :raises APIException: If the parameters "key" and/or "value" were not in
        the request. (MISSING_REQUIRED_PARAM)
    :raises APIException: If the snippet does not belong to the current user.
        (INCORRECT_PERMISSION)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not use snippets.
        (INCORRECT_PERMISSION)
    :raises APIException: If another snippet with the same key already exists.
        (OBJECT_ALREADY_EXISTS)
    """
    with helpers.get_from_request_transaction() as [get, _]:
        value = get('value', str)
        key = get('key', str)

    course = helpers.get_or_404(models.Course,
                                course_id,
                                with_for_update=True,
                                with_for_update_of=models.Course)
    auth.CoursePermissions(course).ensure_may_edit_snippets()

    snip = helpers.get_or_404(
        models.CourseSnippet,
        snippet_id,
        also_error=lambda snip: snip.course_id != course.id)

    other = models.CourseSnippet.query.filter_by(
        course=course,
        key=key,
    ).one_or_none()
    if other is not None and other.id != snippet_id:
        raise APIException(
            'A snippet with the same key already exists.',
            'A snippet with key "{}" already exists for course "{}"'.format(
                key, course_id),
            APICodes.OBJECT_ALREADY_EXISTS,
            400,
        )

    snip.key = key
    snip.value = value
    db.session.commit()

    return make_empty_response()
예제 #13
0
def patch_course_snippet(course_id: int, snippet_id: int) -> EmptyResponse:
    """Modify the :class:`.models.CourseSnippet` with the given id.

    .. :quickref: CourseSnippet; Change a snippets key and value.

    :param int snippet_id: The id of the snippet to change.
    :returns: An empty response with return code 204.

    :<json str key: The new key of the snippet.
    :<json str value: The new value of the snippet.

    :raises APIException: If the parameters "key" and/or "value" were not in
        the request. (MISSING_REQUIRED_PARAM)
    :raises APIException: If the snippet does not belong to the current user.
        (INCORRECT_PERMISSION)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not use snippets.
        (INCORRECT_PERMISSION)
    :raises APIException: If another snippet with the same key already exists.
        (OBJECT_ALREADY_EXISTS)
    """
    auth.ensure_permission(CPerm.can_manage_course_snippets, course_id)
    content = get_json_dict_from_request()

    ensure_keys_in_dict(content, [('key', str), ('value', str)])
    key = t.cast(str, content['key'])
    value = t.cast(str, content['value'])

    course = helpers.get_or_404(models.Course, course_id)
    snip = helpers.get_or_404(
        models.CourseSnippet,
        snippet_id,
        also_error=lambda snip: snip.course_id != course.id)

    other = models.CourseSnippet.query.filter_by(
        course=course,
        key=key,
    ).first()
    if other is not None and other.id != snippet_id:
        raise APIException(
            'A snippet with the same key already exists.',
            'A snippet with key "{}" already exists for course "{}"'.format(
                key, course_id),
            APICodes.OBJECT_ALREADY_EXISTS,
            400,
        )

    snip.key = key
    snip.value = value
    db.session.commit()

    return make_empty_response()
예제 #14
0
def update_role(course_id: int, role_id: int) -> EmptyResponse:
    """Update the :class:`.models.Permission` of a given
    :class:`.models.CourseRole` in the given :class:`.models.Course`.

    .. :quickref: Course; Update a permission for a certain role.

    :param int course_id: The id of the course.
    :param int role_id: The id of the course role.
    :returns: An empty response with return code 204.

    :<json str permission: The name of the permission to change.
    :<json bool value: The value to set the permission to (``True`` means the
        specified role has the specified permission).

    :raises APIException: If the value or permission parameter are not in the
                          request. (MISSING_REQUIRED_PARAM)
    :raises APIException: If the role with the given id does not exist or the
                          permission with the given name does not exist.
                          (OBJECT_NOT_FOUND)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not manage the course with the
                                 given id. (INCORRECT_PERMISSION)
    """
    course = helpers.get_or_404(models.Course, course_id)
    auth.CoursePermissions(course).ensure_may_edit_roles()

    with helpers.get_from_request_transaction() as [get, _]:
        value = get('value', bool)
        permission = get('permission', CPerm)

    role = helpers.filter_single_or_404(
        models.CourseRole,
        models.CourseRole.course == course,
        models.CourseRole.id == role_id,
        also_error=lambda r: r.hidden,
    )

    if (current_user.courses[course_id].id == role.id
            and permission == CPerm.can_edit_course_roles):
        raise APIException(
            'You cannot remove this permission from your own role',
            ('The current user is in role {} which'
             ' cannot remove "can_edit_course_roles"').format(role.id),
            APICodes.INCORRECT_PERMISSION, 403)

    role.set_permission(permission, value)

    db.session.commit()

    return make_empty_response()
예제 #15
0
def put_comment(code_id: int, line: int) -> EmptyResponse:
    """Create or change a single :class:`.models.Comment` of a code
    :class:`.models.File`.

    .. :quickref: Code; Add or change a comment.

    :param int code_id: The id of the code file
    :param int line: The line number of the comment
    :returns: An empty response with return code 204

    :<json str comment: The comment to add to the given file on the given line.

    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not can grade work in the
                                 attached course. (INCORRECT_PERMISSION)
    """
    comment = db.session.query(models.Comment).filter(
        models.Comment.file_id == code_id, models.Comment.line == line
    ).one_or_none()

    def __get_comment() -> str:
        content = ensure_json_dict(request.get_json())
        ensure_keys_in_dict(content, [('comment', str)])
        return t.cast(str, content['comment'])

    if comment:
        auth.ensure_permission(
            'can_grade_work', comment.file.work.assignment.course_id
        )

        comment.comment = __get_comment()
    else:
        file = helpers.get_or_404(models.File, code_id)
        auth.ensure_permission(
            'can_grade_work',
            file.work.assignment.course_id,
        )

        db.session.add(
            models.Comment(
                file_id=code_id,
                user_id=current_user.id,
                line=line,
                comment=__get_comment(),
            )
        )

    db.session.commit()

    return make_empty_response()
예제 #16
0
def user_patch_handle_send_reset_email() -> EmptyResponse:
    """Handle the ``reset_email`` type for the PATCH login route.

    :returns: An empty response.
    """
    data = ensure_json_dict(request.get_json())
    ensure_keys_in_dict(data, [('username', str)])

    mail.send_reset_password_email(
        helpers.filter_single_or_404(models.User,
                                     models.User.username == data['username']))
    db.session.commit()

    return make_empty_response()
예제 #17
0
def add_role(course_id: int) -> EmptyResponse:
    """Add a new :class:`.models.CourseRole` to the given
    :class:`.models.Course`.

    .. :quickref: Course; Add a new course role to a course.

    :param int course_id: The id of the course
    :returns: An empty response with return code 204.

    :<json str name: The name of the new course role.

    :raises APIException: If the name parameter was not in the request.
                          (MISSING_REQUIRED_PARAM)
    :raises APIException: If the course with the given id was not found.
                          (OBJECT_NOT_FOUND)
    :raises APIException: If the course already has a role with the submitted
                          name. (INVALID_PARAM)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not manage the course with the
                                 given id. (INCORRECT_PERMISSION)
    """
    auth.ensure_permission(CPerm.can_edit_course_roles, course_id)

    content = get_json_dict_from_request()

    ensure_keys_in_dict(content, [('name', str)])
    name = t.cast(str, content['name'])

    course = helpers.get_or_404(
        models.Course,
        course_id,
        also_error=lambda c: c.virtual,
    )

    if models.CourseRole.query.filter_by(
            name=name, course_id=course_id).first() is not None:
        raise APIException(
            'This course already has a role with this name',
            'The course "{}" already has a role named "{}"'.format(
                course_id, name), APICodes.INVALID_PARAM, 400)

    role = models.CourseRole(name=name, course=course, hidden=False)
    db.session.add(role)
    db.session.commit()

    return make_empty_response()
예제 #18
0
def delete_submission_grader(submission_id: int) -> EmptyResponse:
    """Change the assigned grader of the given submission.

    .. :quickref: Submission; Delete grader for the submission.

    :returns: Empty response and a 204 status.

    :raises PermissionException: If the logged in user cannot manage the
        course of the submission. (INCORRECT_PERMISSION)
    """
    work = helpers.get_or_404(models.Work, submission_id)

    auth.ensure_permission('can_assign_graders', work.assignment.course_id)

    work.assignee = None
    db.session.commit()

    return make_empty_response()
예제 #19
0
def delete_role(course_id: int, role_id: int) -> EmptyResponse:
    """Remove a :class:`.models.CourseRole` from the given
    :class:`.models.Course`.

    .. :quickref: Course; Delete a course role from a course.

    :param int course_id: The id of the course
    :returns: An empty response with return code 204

    :raises APIException: If the role with the given ids does not exist.
        (OBJECT_NOT_FOUND)
    :raises APIException: If there are still users with this role.
        (INVALID_PARAM)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not manage the course with the
        given id. (INCORRECT_PERMISSION)
    """
    auth.ensure_permission('can_edit_course_roles', course_id)

    course = helpers.get_or_404(models.Course, course_id)
    role = helpers.filter_single_or_404(
        models.CourseRole, models.CourseRole.course_id == course_id,
        models.CourseRole.id == role_id)

    if course.lti_provider is not None:
        if any(r['role'] == role.name for r in LTI_ROLE_LOOKUPS.values()):
            raise APIException(
                'You cannot delete default LTI roles for a LTI course',
                ('The course "{}" is an LTI course '
                 'so it is impossible to delete role {}').format(
                     course.id, role.id), APICodes.INCORRECT_PERMISSION, 403)

    sql = db.session.query(models.user_course).filter(
        models.user_course.c.course_id == role_id).exists()
    if db.session.query(sql).scalar():
        raise APIException(
            'There are still users with this role',
            'There are still users with role {}'.format(role_id),
            APICodes.INVALID_PARAM, 400)

    db.session.delete(role)
    db.session.commit()

    return make_empty_response()
예제 #20
0
def set_grader_to_not_done(
    assignment_id: int, grader_id: int
) -> EmptyResponse:
    """Indicate that the given grader is not yet done grading the given
    `:class:.models.Assignment`.

    .. :quickref: Assignment; Set the grader status to 'not done'.

    :param assignment_id: The id of the assignment the grader is not yet done
        grading.
    :param grader_id: The id of the `:class:.models.User` that is not yet done
        grading.
    :returns: An empty response with return code 204

    :raises APIException: If the given grader was not indicated as done before
        calling this endpoint. (INVALID_STATE)
    :raises PermissionException: If the current user wants to change a status
        of somebody else but the user does not have the
        `can_update_grader_status` permission. (INCORRECT_PERMISSION)
    :raises PermissionException: If the current user wants to change its own
        status but does not have the `can_update_grader_status` or the
        `can_grade_work` permission. (INCORRECT_PERMISSION)
    """
    assig = helpers.get_or_404(models.Assignment, assignment_id)

    if current_user.id == grader_id:
        auth.ensure_permission('can_grade_work', assig.course_id)
    else:
        auth.ensure_permission('can_update_grader_status', assig.course_id)

    try:
        send_mail = grader_id != current_user.id
        assig.set_graders_to_not_done([grader_id], send_mail=send_mail)
        db.session.commit()
    except ValueError:
        raise APIException(
            'The grader is not finished!',
            f'The grader {grader_id} is not done.',
            APICodes.INVALID_STATE,
            400,
        )
    else:
        return make_empty_response()
예제 #21
0
def set_role_permission(role_id: int) -> EmptyResponse:
    """Update the :class:`.models.Permission` of a given
    :class:`.models.Role`.

    .. :quickref: Role; Update a permission for a certain role.

    :param int role_id: The id of the (non course) role.
    :returns: An empty response with return code 204.

    :<json str permission: The name of the permission to change.
    :<json bool value: The value to set the permission to (``True`` means the
        specified role has the specified permission).

    :raises PermissionException: If the current user does not have the
        ``can_manage_site_users`` permission. (INCORRECT_PERMISSION)
    """
    content = ensure_json_dict(request.get_json())

    ensure_keys_in_dict(content, [('permission', str), ('value', bool)])

    perm_name = t.cast(str, content['permission'])
    value = t.cast(bool, content['value'])

    if (current_user.role_id == role_id
            and perm_name == 'can_manage_site_users'):
        raise APIException(
            'You cannot remove this permission from your own role',
            ('The current user is in role {} which'
             ' cannot remove "can_manage_site_users"').format(role_id),
            APICodes.INCORRECT_PERMISSION, 403)

    perm = helpers.filter_single_or_404(models.Permission,
                                        models.Permission.name == perm_name,
                                        ~models.Permission.course_permission)

    role = helpers.get_or_404(models.Role, role_id)

    role.set_permission(perm, value)

    db.session.commit()

    return make_empty_response()
예제 #22
0
def select_rubric_items(submission_id: int, ) -> EmptyResponse:
    """Select the given rubric items for the given submission.

    .. :quickref: Submission; Select multiple rubric items.

    :param submission_id: The submission to unselect the item for.

    :>json array items: The ids of the rubric items you want to select.

    :returns: Nothing.

    :raises APIException: If the assignment of a given item does not belong to
        the assignment of the given submission. of the submission
        (INVALID_PARAM).
    :raises PermissionException: If the current user cannot grace work
        (INCORRECT_PERMISSION).
    """
    submission = helpers.get_or_404(models.Work, submission_id)

    auth.ensure_permission('can_grade_work', submission.assignment.course_id)

    content = ensure_json_dict(request.get_json())
    ensure_keys_in_dict(content, [('items', list)])
    item_ids = t.cast(list, content['items'])

    items = []
    for item_id in item_ids:
        items.append(helpers.get_or_404(models.RubricItem, item_id))

    if any(item.rubricrow.assignment_id != submission.assignment_id
           for item in items):
        raise APIException(
            'Selected rubric item is not coupled to the given submission',
            f'A given item of "{", ".join(str(i) for i in item_ids)}"'
            f' does not belong to assignment "{submission.assignment_id}"',
            APICodes.INVALID_PARAM, 400)

    submission.select_rubric_items(items, current_user, True)
    db.session.commit()

    return make_empty_response()
예제 #23
0
def delete_registration_link(course_id: int,
                             link_id: uuid.UUID) -> EmptyResponse:
    """Delete the given registration link.

    .. :quickref: Course; The delete a registration link of the given course.

    :param course_id: The id of the course to which the registration link is
        connected.
    :param link_id: The id of the registration link.
    :returns: Nothing.
    """
    course = helpers.get_or_404(models.Course,
                                course_id,
                                also_error=lambda c: c.virtual)
    auth.ensure_permission(CPerm.can_edit_course_users, course_id)
    link = helpers.get_or_404(models.CourseRegistrationLink,
                              link_id,
                              also_error=lambda l: l.course_id != course.id)
    db.session.delete(link)
    db.session.commit()
    return make_empty_response()
예제 #24
0
def patch_snippet(snippet_id: int) -> EmptyResponse:
    """Modify the :class:`.models.Snippet` with the given id.

    .. :quickref: Snippet; Change a snippets key and value.

    :param int snippet_id: The id of the snippet to change.
    :returns: An empty response with return code 204.

    :<json str key: The new key of the snippet.
    :<json str value: The new value of the snippet.

    :raises APIException: If the parameters "key" and/or "value" were not in
                          the request. (MISSING_REQUIRED_PARAM)
    :raises APIException: If the snippet does not belong to the current user.
                          (INCORRECT_PERMISSION)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not use snippets.
                                 (INCORRECT_PERMISSION)
    """
    content = ensure_json_dict(request.get_json())

    ensure_keys_in_dict(content, [('key', str), ('value', str)])
    key = t.cast(str, content['key'])
    value = t.cast(str, content['value'])

    snip = helpers.get_or_404(models.Snippet, snippet_id)

    if snip.user_id != current_user.id:
        raise APIException(
            'The given snippet is not your snippet',
            'The snippet "{}" does not belong to user "{}"'.format(
                snip.id, current_user.id), APICodes.INCORRECT_PERMISSION, 403)

    snip.key = key
    snip.value = value
    db.session.commit()

    return make_empty_response()
예제 #25
0
def update_submission_grader(submission_id: int) -> EmptyResponse:
    """Change the assigned grader of the given submission.

    .. :quickref: Submission; Update grader for the submission.

    :returns: Empty response and a 204 status.

    :>json int user_id: Id of the new grader. This is a required parameter.

    :raises PermissionException: If the logged in user cannot manage the
        course of the submission. (INCORRECT_PERMISSION)
    :raises APIException: If the new grader does not have the correct
        permission to grade this submission. (INCORRECT_PERMISSION)
    """
    work = helpers.get_or_404(models.Work, submission_id)
    content = ensure_json_dict(request.get_json())
    ensure_keys_in_dict(content, [('user_id', int)])
    user_id = t.cast(int, content['user_id'])

    auth.ensure_permission('can_assign_graders', work.assignment.course_id)

    grader = helpers.get_or_404(models.User, user_id)
    if not grader.has_permission('can_grade_work', work.assignment.course_id):
        raise APIException(
            f'User "{grader.name}" doesn\'t have the required permission',
            f'User "{grader.name}" doesn\'t have permission "can_grade_work"',
            APICodes.INCORRECT_PERMISSION, 400)

    work.assignee = grader
    work.assignment.set_graders_to_not_done(
        [grader.id],
        send_mail=grader.id != current_user.id,
        ignore_errors=True,
    )
    db.session.commit()

    return make_empty_response()
예제 #26
0
def delete_linter_output(linter_id: str) -> EmptyResponse:
    """Delete the all the output created by the
    :class:`.models.AssignmentLinter` with the given id.

    .. :quickref: Linter; Delete all linter input for a given linter.

    :param int linter_id: The id of the linter
    :returns: An empty response with return code 204

    :raises APIException: If the linter with the given id does not exist.
                          (OBJECT_ID_NOT_FOUND)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not use the linters in the
                                 course attached to the linter with the given
                                 id. (INCORRECT_PERMISSION)
    """
    linter = helpers.get_or_404(models.AssignmentLinter, linter_id)

    auth.ensure_permission('can_use_linter', linter.assignment.course_id)

    db.session.delete(linter)
    db.session.commit()

    return make_empty_response()
예제 #27
0
def delete_code(file_id: int) -> EmptyResponse:
    """Delete the given file.

    .. :quickref: Code; Delete the given file.

    If a student does this request before the deadline, the file will be
    completely deleted. If the request is done after the deadline the user
    doing the delete will be removed from the ownership of the file and if
    there are no owners left the file is deleted.

    If the file owner of the given file is the same as that of the user doing
    the request (so the file will be completely deleted) the given file should
    not have any comments (Linter or normal) associated with it. If it still
    has comments the request will fail with error code 400.

    :returns: Nothing.

    :raises APIException: If the request will result in wrong
        state. (INVALID_STATE)
    :raises APIException: If there is not file with the given id.
        (OBJECT_ID_NOT_FOUND)
    :raises APIException: If you do not have permission to delete the given
        file. (INCORRECT_PERMISSION)
    """
    code: models.File = helpers.get_or_404(models.File, file_id)

    auth.ensure_can_edit_work(code.work)

    def _raise_invalid() -> None:
        raise APIException(
            'You cannot delete this file as you don\'t own it',
            f'File {file_id} is not owned by {current_user.id}',
            APICodes.INCORRECT_PERMISSION, 403
        )

    if code.work.user_id == current_user.id:
        current, other = models.FileOwner.student, models.FileOwner.teacher
    else:
        current, other = models.FileOwner.teacher, models.FileOwner.student

    if not all(child.fileowner == other for child in code.children.all()):
        raise APIException(
            'You cannot delete this directory as it has children',
            f'The file "{file_id}" has children with fileowner "{current}"',
            APICodes.INVALID_STATE, 400
        )

    if code.fileowner == other:
        _raise_invalid()
    elif code.fileowner == current:
        if db.session.query(
            sql.or_(
                models.Comment.query.filter_by(file_id=code.id).exists(),
                models.LinterComment.query.filter_by(file_id=code.id).exists(),
            )
        ).scalar():
            raise APIException(
                'You cannot delete this file as it has comments',
                f'The file "{file_id}" has comments associated with it.',
                APICodes.INVALID_STATE,
                400,
            )
        code.delete_from_disk()
        db.session.delete(code)
    elif code.fileowner == models.FileOwner.both:
        code.fileowner = other

    db.session.commit()

    return make_empty_response()
예제 #28
0
def set_grader_to_done(assignment_id: int, grader_id: int) -> EmptyResponse:
    """Indicate that the given grader is done grading the given
    `:class:.models.Assignment`.

    .. :quickref: Assignment; Set the grader status to 'done'.

    :param assignment_id: The id of the assignment the grader is done grading.
    :param grader_id: The id of the `:class:.models.User` that is done grading.
    :returns: An empty response with return code 204

    :raises APIException: If the given grader was indicated as done before
        calling this endpoint. (INVALID_STATE)
    :raises PermissionException: If the current user wants to change a status
        of somebody else but the user does not have the
        `can_update_grader_status` permission. (INCORRECT_PERMISSION)
    :raises PermissionException: If the current user wants to change its own
        status but does not have the `can_update_grader_status` or the
        `can_grade_work` permission. (INCORRECT_PERMISSION)
    """
    assig = helpers.get_or_404(
        models.Assignment,
        assignment_id,
        options=[joinedload(models.Assignment.finished_graders)],
    )

    if current_user.id == grader_id:
        auth.ensure_permission('can_grade_work', assig.course_id)
    else:
        auth.ensure_permission('can_update_grader_status', assig.course_id)

    grader = helpers.get_or_404(models.User, grader_id)
    if not grader.has_permission('can_grade_work', assig.course_id):
        raise APIException(
            'The given user is not a grader in this course',
            (
                f'The user with id "{grader_id}" is not a grader '
                f'in the course "{assig.course_id}"'
            ),
            APICodes.INVALID_PARAM,
            400,
        )

    if any(g.user_id == grader_id for g in assig.finished_graders):
        raise APIException(
            'The grader is already finished!',
            f'The grader {grader_id} is already done.',
            APICodes.INVALID_STATE,
            400,
        )
    done_before = assig.graders_are_done()

    grader_done = models.AssignmentGraderDone(
        user_id=grader_id,
        assignment=assig,
    )
    db.session.add(grader_done)
    db.session.commit()

    if not done_before and assig.graders_are_done():
        psef.tasks.send_done_mail(assig.id)

    if assig.has_non_graded_submissions(grader_id):
        return make_empty_response(
            make_warning(
                'You have non graded work!',
                APIWarnings.GRADER_NOT_DONE,
            )
        )

    return make_empty_response()
예제 #29
0
def divide_assignments(assignment_id: int) -> EmptyResponse:
    """Assign graders to all the latest :class:`.models.Work` objects of
    the given :class:`.models.Assignment`.

    .. :quickref: Assignment; Divide a submission among given TA's.

    The redivide tries to minimize shuffles. This means that calling it twice
    with the same data is effectively a noop. If the relative weight (so the
    percentage of work) of a user doesn't change it will not lose or gain any
    submissions.

    .. warning::

        If a user was marked as done grading and gets assigned new submissions
        this user is marked as not done and gets a notification email!

    :<json dict graders: A mapping that maps user ids (strings) and the new
        weight they should get (numbers).
    :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 or the
                          assignment has no submissions. (OBJECT_ID_NOT_FOUND)
    :raises APIException: If there was no grader in the request.
                          (MISSING_REQUIRED_PARAM)
    :raises APIException: If some grader id is invalid or some grader does not
                          have the permission to grade the assignment.
                          (INVALID_PARAM)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user is not allowed to divide
                                 submissions for this assignment.
                                 (INCORRECT_PERMISSION)
    """
    assignment = helpers.get_or_404(models.Assignment, assignment_id)

    auth.ensure_permission('can_assign_graders', assignment.course_id)

    content = ensure_json_dict(request.get_json())
    ensure_keys_in_dict(content, [('graders', dict)])
    graders = {}

    for user_id, weight in t.cast(dict, content['graders']).items():
        if not (isinstance(user_id, str) and isinstance(weight, (float, int))):
            raise APIException(
                'Given graders weight or id is invalid',
                'Both key and value in graders object should be integers',
                APICodes.INVALID_PARAM, 400
            )
        graders[int(user_id)] = weight

    if graders:
        users = helpers.filter_all_or_404(
            models.User,
            models.User.id.in_(graders.keys())  # type: ignore
        )
    else:
        models.Work.query.filter_by(assignment_id=assignment.id).update(
            {
                'assigned_to': None
            }
        )
        assignment.assigned_graders = {}
        db.session.commit()
        return make_empty_response()

    if len(users) != len(graders):
        raise APIException(
            'Invalid grader id given', 'Invalid grader (=user) id given',
            APICodes.INVALID_PARAM, 400
        )

    can_grade_work = helpers.filter_single_or_404(
        models.Permission, models.Permission.name == 'can_grade_work'
    )
    for user in users:
        if not user.has_permission(can_grade_work, assignment.course_id):
            raise APIException(
                'Selected grader has no permission to grade',
                f'The grader {user.id} has no permission to grade',
                APICodes.INVALID_PARAM, 400
            )

    assignment.divide_submissions([(user, graders[user.id]) for user in users])
    db.session.commit()

    return make_empty_response()
예제 #30
0
def set_course_permission_user(
        course_id: int) -> t.Union[EmptyResponse, JSONResponse[_UserCourse]]:
    """Set the :class:`.models.CourseRole` of a :class:`.models.User` in the
    given :class:`.models.Course`.

    .. :quickref: Course; Change the course role for a user.

    :param int course_id: The id of the course
    :returns: If the user_id parameter is set in the request the response will
              be empty with return code 204. Otherwise the response will
              contain the JSON serialized user and course role with return code
              201

    :raises APIException: If the parameter role_id or not at least one of
                          user_id and user_email are in the request.
                          (MISSING_REQUIRED_PARAM)
    :raises APIException: If no role with the given role_id or no user
                          with the supplied parameters exists.
                          (OBJECT_ID_NOT_FOUND)
    :raises APIException: If the user was selected by email and the user is
                          already in the course. (INVALID_PARAM)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not manage the course with the
                                 given id. (INCORRECT_PERMISSION)

    .. todo::
        This function should probability be splitted.
    """
    auth.ensure_permission(CPerm.can_edit_course_users, course_id)

    content = get_json_dict_from_request()
    ensure_keys_in_dict(content, [('role_id', int)])
    role_id = t.cast(int, content['role_id'])

    role = helpers.filter_single_or_404(
        models.CourseRole, models.CourseRole.id == role_id,
        models.CourseRole.course_id == course_id)

    res: t.Union[EmptyResponse, JSONResponse[_UserCourse]]

    if 'user_id' in content:
        with get_from_map_transaction(content) as [get, _]:
            user_id = get('user_id', int)

        user = helpers.get_or_404(models.User, user_id)

        if user.id == current_user.id:
            raise APIException(
                'You cannot change your own role',
                'The user requested and the current user are the same',
                APICodes.INCORRECT_PERMISSION, 403)

        res = make_empty_response()
    elif 'username' in content:
        with get_from_map_transaction(content) as [get, _]:
            username = get('username', str)

        user = helpers.filter_single_or_404(models.User,
                                            models.User.username == username)

        if course_id in user.courses:
            raise APIException(
                'The specified user is already in this course',
                'The user {} is in course {}'.format(user.id, course_id),
                APICodes.INVALID_PARAM, 400)

        res = jsonify({
            'User': user,
            'CourseRole': role,
        }, status_code=201)
    else:
        raise APIException(
            'None of the keys "user_id" or "role_id" were found',
            ('The given content ({})'
             ' does  not contain "user_id" or "user_email"').format(content),
            APICodes.MISSING_REQUIRED_PARAM, 400)

    if user.is_test_student:
        raise APIException('You cannot change the role of a test student',
                           f'The user {user.id} is a test student',
                           APICodes.INVALID_PARAM, 400)

    user.courses[role.course_id] = role
    db.session.commit()
    return res