Пример #1
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()
Пример #2
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()
Пример #3
0
def get_all_course_assignments(
        course_id: int) -> JSONResponse[t.Sequence[models.Assignment]]:
    """Get all :class:`.models.Assignment` objects of the given
    :class:`.models.Course`.

    .. :quickref: Course; Get all assignments for single course.

    The returned assignments are sorted by deadline.

    :param int course_id: The id of the course
    :returns: A response containing the JSON serialized assignments sorted by
        deadline of the assignment. See
        :py:func:`.models.Assignment.__to_json__` for the way assignments are
        given.

    :raises APIException: If there is no course with the given id.
                          (OBJECT_ID_NOT_FOUND)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not see assignments in the
                                 given course. (INCORRECT_PERMISSION)
    """
    auth.ensure_permission(CPerm.can_see_assignments, course_id)

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

    return jsonify(course.get_all_visible_assignments())
Пример #4
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()
Пример #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_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()
Пример #7
0
def get_assignment_rubric(assignment_id: int
                          ) -> JSONResponse[t.Sequence[models.RubricRow]]:
    """Return the rubric corresponding to the given `assignment_id`.

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

    :param int assignment_id: The id of the assignment
    :returns: A list of JSON of :class:`.models.RubricRows` items

    :raises APIException: If no assignment with given id exists.
        (OBJECT_ID_NOT_FOUND)
    :raises APIException: If the assignment has no rubric.
        (OBJECT_ID_NOT_FOUND)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user is not allowed to see this is
                                 assignment. (INCORRECT_PERMISSION)
    """
    assig = helpers.get_or_404(models.Assignment, assignment_id)

    auth.ensure_permission('can_see_assignments', 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
        )

    return jsonify(assig.rubric_rows)
Пример #8
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()
Пример #9
0
def create_new_assignment(course_id: int) -> JSONResponse[models.Assignment]:
    """Create a new course for the given assignment.

    .. :quickref: Course; Create a new assignment in a course.

    :param int course_id: The course to create an assignment in.

    :<json str name: The name of the new assignment.

    :returns: The newly created assignment.

    :raises PermissionException: If the current user does not have the
        ``can_create_assignment`` permission (INCORRECT_PERMISSION).
    """
    auth.ensure_permission('can_create_assignment', course_id)

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

    course = helpers.get_or_404(models.Course, course_id)

    if course.lti_course_id is not None:
        raise APIException('You cannot add assignments to a LTI course',
                           f'The course "{course_id}" is a LTI course',
                           APICodes.INVALID_STATE, 400)

    assig = models.Assignment(name=name,
                              course=course,
                              deadline=datetime.datetime.utcnow())
    db.session.add(assig)
    db.session.commit()

    return jsonify(assig)
Пример #10
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()
Пример #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 create_or_edit_registration_link(
        course_id: int) -> JSONResponse[models.CourseRegistrationLink]:
    """Create or edit a registration link.

    .. :quickref: Course; Create or edit a registration link for a course.

    :param course_id: The id of the course in which this link should enroll
        users.
    :>json id: The id of the link to edit, omit to create a new link.
    :>json role_id: The id of the role that users should get when registering
        with this link.
    :>json expiration_date: The date this link should stop working, this date
        should be in ISO8061 format without any timezone information, as it
        will be interpret as a UTC date.
    :returns: The created or edited link.
    """
    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)

    with get_from_map_transaction(
            get_json_dict_from_request()) as [get, opt_get]:
        expiration_date = get('expiration_date', str)
        role_id = get('role_id', int)
        link_id = opt_get('id', str, default=None)

    if link_id is None:
        link = models.CourseRegistrationLink(course=course)
        db.session.add(link)
    else:
        link = helpers.filter_single_or_404(
            models.CourseRegistrationLink,
            models.CourseRegistrationLink.id == uuid.UUID(link_id),
            also_error=lambda l: l.course_id != course.id)

    link.course_role = helpers.get_or_404(
        models.CourseRole,
        role_id,
        also_error=lambda r: r.course_id != course.id)
    link.expiration_date = parsers.parse_datetime(expiration_date)
    if link.expiration_date < helpers.get_request_start_time():
        helpers.add_warning('The link has already expired.',
                            APIWarnings.ALREADY_EXPIRED)

    if link.course_role.has_permission(CPerm.can_edit_course_roles):
        helpers.add_warning(
            ('Users that register with this link will have the permission'
             ' to give themselves more permissions.'),
            APIWarnings.DANGEROUS_ROLE)

    db.session.commit()
    return jsonify(link)
Пример #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 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()
Пример #15
0
def get_all_works_for_assignment(
    assignment_id: int
) -> t.Union[JSONResponse[WorkList], ExtendedJSONResponse[WorkList]]:
    """Return all :class:`.models.Work` objects for the given
    :class:`.models.Assignment`.

    .. :quickref: Assignment; Get all works for an assignment.

    :qparam boolean extended: Whether to get extended or normal
        :class:`.models.Work` objects. The default value is ``false``, you can
        enable extended by passing ``true``, ``1`` or an empty string.

    :param int assignment_id: The id of the assignment
    :returns: A response containing the JSON serialized submissions.

    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the assignment is hidden and the user is
                                 not allowed to view it. (INCORRECT_PERMISSION)
    """
    assignment = helpers.get_or_404(models.Assignment, assignment_id)

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

    if assignment.is_hidden:
        auth.ensure_permission(
            'can_see_hidden_assignments', assignment.course_id
        )

    obj = models.Work.query.filter_by(
        assignment_id=assignment_id,
    ).options(joinedload(
        models.Work.selected_items,
    )).order_by(t.cast(t.Any, models.Work.created_at).desc())

    if not current_user.has_permission(
        'can_see_others_work', course_id=assignment.course_id
    ):
        obj = obj.filter_by(user_id=current_user.id)

    extended = request.args.get('extended', 'false').lower()

    if extended in {'true', '1', ''}:
        obj = obj.options(undefer(models.Work.comment))
        return extended_jsonify(
            obj.all(),
            use_extended=lambda obj: isinstance(obj, models.Work),
        )
    else:
        return jsonify(obj.all())
Пример #16
0
def get_registration_links(
        course_id: int
) -> JSONResponse[t.Sequence[models.CourseRegistrationLink]]:
    """Get the registration links for the given course.

    .. :quickref: Course; Get the registration links for this course.

    :param course_id: The course id for which to get the registration links.
    :returns: An array of registration links.
    """
    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)
    return jsonify(course.registration_links)
Пример #17
0
def patch_submission(submission_id: int) -> JSONResponse[models.Work]:
    """Update the given submission (:class:`.models.Work`) if it already
    exists.

    .. :quickref: Submission; Update a submissions grade and feedback.

    :param int submission_id: The id of the submission
    :returns: Empty response with return code 204

    :>json float grade: The new grade, this can be null or float where null
        resets the grade or clears it. This field is optional
    :>json str feedback: The feedback for the student. This field is optional.

    :raise APIException: If the submission with the given id does not exist
        (OBJECT_ID_NOT_FOUND)
    :raise APIException: If the value of the "grade" parameter is not a float
        (INVALID_PARAM)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If user can not grade the submission with the
        given id (INCORRECT_PERMISSION)
    """
    work = helpers.get_or_404(models.Work, submission_id)
    content = ensure_json_dict(request.get_json())

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

    if 'feedback' in content:
        ensure_keys_in_dict(content, [('feedback', str)])
        feedback = t.cast(str, content['feedback'])

        work.comment = feedback

    if 'grade' in content:
        ensure_keys_in_dict(content, [('grade', (numbers.Real, type(None)))])
        grade = t.cast(t.Optional[float], content['grade'])

        if not (grade is None or (0 <= float(grade) <= 10)):
            raise APIException(
                'Grade submitted not between 0 and 10',
                f'Grade for work with id {submission_id} '
                f'is {content["grade"]} which is not between 0 and 10',
                APICodes.INVALID_PARAM, 400)

        work.set_grade(grade, current_user)

    db.session.commit()
    return jsonify(work)
Пример #18
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()
Пример #19
0
def get_submission(
    submission_id: int
) -> ExtendedJSONResponse[t.Union[models.Work, t.Mapping[str, str]]]:
    """Get the given submission (:class:`.models.Work`).

    .. :quickref: Submission; Get a single submission.

    This API has some options based on the 'type' argument in the request

    - If ``type == 'zip'`` see :py:func:`.get_zip`
    - If ``type == 'feedback'`` see :py:func:`.submissions.get_feedback`

    :param int submission_id: The id of the submission
    :returns: A response with the JSON serialized submission as content unless
              specified otherwise
    :rtype: flask.Response

    :query str owner: The type of files to list, if set to `teacher` only
        teacher files will be listed, otherwise only student files will be
        listed.

    :raises APIException: If the submission with 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 submission does not belong to the
                                 current user and the user can not see others
                                 work in the attached course.
                                 (INCORRECT_PERMISSION)
    """
    work = helpers.get_or_404(models.Work, submission_id)

    if work.user_id != current_user.id:
        auth.ensure_permission('can_see_others_work',
                               work.assignment.course_id)

    if request.args.get('type') == 'zip':
        exclude_owner = models.File.get_exclude_owner(
            request.args.get('owner'),
            work.assignment.course_id,
        )
        return extended_jsonify(get_zip(work, exclude_owner))
    elif request.args.get('type') == 'feedback':
        auth.ensure_can_see_grade(work)
        return extended_jsonify(get_feedback(work))
    return extended_jsonify(work)
Пример #20
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()
Пример #21
0
def get_all_graders(
    assignment_id: int
) -> JSONResponse[t.Sequence[t.Mapping[str, t.Union[float, str, bool]]]]:
    """Gets a list of all :class:`.models.User` objects who can grade the given
    :class:`.models.Assignment`.

    .. :quickref: Assignment; Get all graders for an assignment.

    :param int assignment_id: The id of the assignment
    :returns: A response containing the JSON serialized graders.

    :>jsonarr string name: The name of the grader.
    :>jsonarr int id: The user id of this grader.
    :>jsonarr bool divided: Is this user assigned to any submission for this
        assignment.
    :>jsonarr bool done: Is this user done grading?

    :raises APIException: If no assignment with given id exists.
                          (OBJECT_ID_NOT_FOUND)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user is not allowed to view graders of
                                 this assignment. (INCORRECT_PERMISSION)
    """
    assignment = helpers.get_or_404(models.Assignment, assignment_id)
    auth.ensure_permission('can_see_assignee', assignment.course_id)

    result = assignment.get_all_graders(sort=True)

    divided: t.MutableMapping[int, float] = defaultdict(int)
    for assigned_grader in models.AssignmentAssignedGrader.query.filter_by(
        assignment_id=assignment_id
    ):
        divided[assigned_grader.user_id] = assigned_grader.weight

    return jsonify(
        [
            {
                'id': res[1],
                'name': res[0],
                'weight': float(divided[res[1]]),
                'done': res[2],
            } for res in result
        ]
    )
Пример #22
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()
Пример #23
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()
Пример #24
0
def get_all_course_roles(
    course_id: int
) -> JSONResponse[t.Union[t.Sequence[models.CourseRole],
                          t.Sequence[t.MutableMapping[str, t.Union[t.Mapping[
                              str, bool], bool]]]]]:
    """Get a list of all :class:`.models.CourseRole` objects of a given
    :class:`.models.Course`.

    .. :quickref: Course; Get all course roles for a single course.

    :param int course_id: The id of the course to get the roles for.
    :returns: An array of all course roles for the given course.

    :>jsonarr perms: All permissions this role has as returned
        by :py:meth:`.models.CourseRole.get_all_permissions`.
    :>jsonarrtype perms: :py:class:`t.Mapping[str, bool]`
    :>jsonarr bool own: True if the current course role is the current users
        course role.
    :>jsonarr ``**rest``: The course role as returned by
        :py:meth:`.models.CourseRole.__to_json__`

    :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_roles: t.Sequence[models.CourseRole]
    course_roles = models.CourseRole.query.filter_by(
        course_id=course_id,
        hidden=False).order_by(models.CourseRole.name).all()

    if request.args.get('with_roles') == 'true':
        res = []
        for course_role in course_roles:
            json_course = course_role.__to_json__()
            json_course['perms'] = CPerm.create_map(
                course_role.get_all_permissions())
            json_course['own'] = current_user.courses[
                course_role.course_id] == course_role
            res.append(json_course)
        return jsonify(res)
    return jsonify(course_roles)
Пример #25
0
def get_linter_state(linter_id: str) -> JSONResponse[models.AssignmentLinter]:
    """Get the state of the :class:`.models.AssignmentLinter` with the given
    id.

    .. :quickref: Linter; Get the state of a given linter.

    :param str linter_id: The id of the linter
    :returns: A response containing the JSON serialized linter

    :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)

    return jsonify(linter)
Пример #26
0
def get_rubric(submission_id: int) -> JSONResponse[t.Mapping[str, t.Any]]:
    """Return full rubric of the :class:`.models.Assignment` of the given
    submission (:class:`.models.Work`).

    .. :quickref: Submission; Get a rubric and its selected items.

    :param int submission_id: The id of the submission
    :returns: A response containing the JSON serialized rubric as described in
        :py:meth:`.Work.__rubric_to_json__`.

    :raises APIException: If the submission 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 see the assignment of the
                                 given submission. (INCORRECT_PERMISSION)
    """
    work = helpers.get_or_404(models.Work, submission_id)
    auth.ensure_permission('can_see_assignments', work.assignment.course_id)
    return jsonify(work.__rubric_to_json__())
Пример #27
0
def get_assignments_feedback(
    assignment_id: int
) -> JSONResponse[t.Mapping[str, t.Mapping[str, t.Union[t.Sequence[str], str]]]
                  ]:
    """Get all feedbacks for all latest submissions for a given assignment.

    .. :quickref: Assignment; Get feedback for all submissions in a assignment.

    :param int assignment_id: The assignment to query for.
    :returns: A mapping between the id of the submission and a object contain
        three keys: ``general`` for general feedback as a string, ``user`` for
        user feedback as a list of strings and ``linter`` for linter feedback
        as a list of strings. If a user cannot see others work only submissions
        by the current users are returned.
    """
    assignment = helpers.get_or_404(models.Assignment, assignment_id)

    auth.ensure_enrolled(assignment.course_id)

    latest_subs = assignment.get_all_latest_submissions()
    try:
        auth.ensure_permission('can_see_others_work', assignment.course_id)
    except auth.PermissionException:
        latest_subs = latest_subs.filter_by(user_id=current_user.id)

    res = {}
    for sub in latest_subs:
        try:
            # This call should be cached in auth.py
            auth.ensure_can_see_grade(sub)

            user_feedback, linter_feedback = sub.get_all_feedback()
            item = {
                'general': sub.comment or '',
                'user': list(user_feedback),
                'linter': list(linter_feedback),
            }
        except auth.PermissionException:
            item = {'user': [], 'linter': [], 'general': ''}

        res[str(sub.id)] = item

    return jsonify(res)
Пример #28
0
def get_all_course_users(
    course_id: int
) -> JSONResponse[t.Union[t.List[_UserCourse], t.List[models.User]]]:
    """Return a list of all :class:`.models.User` objects and their
    :class:`.models.CourseRole` in the given :class:`.models.Course`.

    .. :quickref: Course; Get all users for a single course.

    :param int course_id: The id of the course

    :query string q: Search for users matching this query string. This will
        change the output to a list of users.

    :returns: A response containing the JSON serialized users and course roles

    :>jsonarr User:  A member of the given course.
    :>jsonarrtype User: :py:class:`~.models.User`
    :>jsonarr CourseRole: The role that this user has.
    :>jsonarrtype CourseRole: :py:class:`~.models.CourseRole`
    """
    auth.ensure_permission(CPerm.can_list_course_users, course_id)
    course = helpers.get_or_404(models.Course, course_id)

    if 'q' in request.args:

        @limiter.limit('1 per second', key_func=lambda: str(current_user.id))
        def get_users_in_course() -> t.List[models.User]:
            query: str = request.args.get('q', '')
            base = course.get_all_users_in_course(
                include_test_students=False).from_self(models.User)
            return helpers.filter_users_by_name(query, base).all()

        return jsonify(get_users_in_course())

    users = course.get_all_users_in_course(include_test_students=False)

    user_course: t.List[_UserCourse]
    user_course = [{
        'User': user,
        'CourseRole': crole
    } for user, crole in users]
    return jsonify(sorted(user_course, key=lambda item: item['User'].name))
Пример #29
0
def create_course_snippet(
        course_id: int) -> JSONResponse[models.CourseSnippet]:
    """Add or modify a :class:`.models.CourseSnippet` by key.

    .. :quickref: CourseSnippet; Add or modify a course snippet.

    :returns: A response containing the JSON serialized snippet and return
              code 201.
    :<json str value: The new value of the snippet.
    :<json str key: The key of the new or existing snippet.

    :raises APIException: If the parameters "key", "value", and/or "course_id"
        were not in the request. (MISSING_REQUIRED_PARAM)
    :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)
    content = get_json_dict_from_request()
    ensure_keys_in_dict(content, [('value', str), ('key', str)])
    key = t.cast(str, content['key'])
    value = t.cast(str, content['value'])

    course = helpers.get_or_404(models.Course, course_id)
    snippet = models.CourseSnippet.query.filter_by(
        course=course,
        key=key,
    ).first()

    if snippet is None:
        snippet = models.CourseSnippet(
            course=course,
            key=key,
            value=value,
        )
        db.session.add(snippet)
    else:
        snippet.value = value

    db.session.commit()

    return jsonify(snippet, status_code=201)
Пример #30
0
def create_new_assignment(course_id: int) -> JSONResponse[models.Assignment]:
    """Create a new course for the given assignment.

    .. :quickref: Course; Create a new assignment in a course.

    :param int course_id: The course to create an assignment in.

    :<json str name: The name of the new assignment.

    :returns: The newly created assignment.

    :raises PermissionException: If the current user does not have the
        ``can_create_assignment`` permission (INCORRECT_PERMISSION).
    """
    auth.ensure_permission(CPerm.can_create_assignment, 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 course.lti_provider is not None:
        lms = course.lti_provider.lms_name
        raise APIException(f'You cannot add assignments to a {lms} course',
                           f'The course "{course_id}" is a LTI course',
                           APICodes.INVALID_STATE, 400)

    assig = models.Assignment(
        name=name,
        course=course,
        is_lti=False,
    )
    db.session.add(assig)
    db.session.commit()

    return jsonify(assig)