Beispiel #1
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()
Beispiel #2
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()
Beispiel #3
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()
Beispiel #4
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)
Beispiel #5
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()
Beispiel #6
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()
Beispiel #7
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()
Beispiel #8
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()
Beispiel #9
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)
Beispiel #10
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()
Beispiel #11
0
def get_all_course_roles(
    course_id: int
) -> t.Union[JSONResponse[t.List[models.CourseRole]],
             JSONResponse[t.List[models.CourseRole.AsJSONWithPerms]], ]:
    """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)
    """
    course = helpers.get_or_404(models.Course, course_id)
    auth.CoursePermissions(course).ensure_may_see_roles()

    course_roles = models.CourseRole.query.filter(
        models.CourseRole.course == course,
        ~models.CourseRole.hidden).order_by(models.CourseRole.name).all()

    if request.args.get('with_roles') == 'true':
        res = [r.__to_json_with_perms__() for r in course_roles]
        return jsonify(res)
    return jsonify(course_roles)
Beispiel #12
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.
    """
    with get_from_map_transaction(get_json_dict_from_request()) as [get, _]:
        name = get('name', str)

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

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

    return jsonify(assig)
Beispiel #13
0
def get_course_by_id(course_id: int) -> ExtendedJSONResponse[models.Course]:
    """Return course data for a given :class:`.models.Course`.

    .. :quickref: Course; Get data for a given course.

    :param int course_id: The id of the course

    :returns: A response containing the JSON serialized course

    :>json str role: The name of the role the current user has in this
        course.
    :>json ``**rest``: JSON serialization of :py:class:`psef.models.Course`.

    :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)
    """
    course = helpers.get_or_404(models.Course, course_id)
    auth.CoursePermissions(course).ensure_may_see()

    if not helpers.request_arg_true('no_role_name'):
        helpers.add_deprecate_warning(
            'Getting the role of the current user in the requested course is'
            ' deprecated and will be removed in the next major version of'
            ' CodeGrade')
        helpers.jsonify_options.get_options().add_role_to_course = True

    return ExtendedJSONResponse.make(course, use_extended=models.Course)
Beispiel #14
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)
Beispiel #15
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())
Beispiel #16
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()
Beispiel #17
0
def user_patch_handle_reset_password() -> JSONResponse[t.Mapping[str, str]]:
    """Handle the ``reset_password`` type for the PATCH login route.

    :returns: A response with a jsonified mapping between ``access_token`` and
        a token which can be used to login. This is only key available.
    """
    data = ensure_json_dict(request.get_json())
    ensure_keys_in_dict(data, [('new_password', str), ('token', str),
                               ('user_id', int)])

    password = t.cast(str, data['new_password'])
    user_id = t.cast(int, data['user_id'])
    token = t.cast(str, data['token'])

    if password == '':
        raise APIException('Password should at least be 1 char',
                           f'The password is {len(password)} chars long',
                           APICodes.INVALID_PARAM, 400)
    user = helpers.get_or_404(models.User, user_id)
    user.reset_password(token, password)
    db.session.commit()
    return jsonify({
        'access_token':
        flask_jwt.create_access_token(
            identity=user.id,
            fresh=True,
        )
    })
Beispiel #18
0
def user_patch_handle_reset_password() -> JSONResponse[t.Mapping[str, str]]:
    """Handle the ``reset_password`` type for the PATCH login route.

    :returns: A response with a jsonified mapping between ``access_token`` and
        a token which can be used to login. This is only key available.
    """
    data = ensure_json_dict(
        request.get_json(),
        replace_log=lambda k, v: '<PASSWORD>' if 'password' in k else v
    )
    ensure_keys_in_dict(
        data, [('new_password', str), ('token', str), ('user_id', int)]
    )
    password = t.cast(str, data['new_password'])
    user_id = t.cast(int, data['user_id'])
    token = t.cast(str, data['token'])

    user = helpers.get_or_404(models.User, user_id)
    validate.ensure_valid_password(password, user=user)

    user.reset_password(token, password)
    db.session.commit()
    return jsonify(
        {
            'access_token':
                flask_jwt.create_access_token(
                    identity=user.id,
                    fresh=True,
                )
        }
    )
Beispiel #19
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()
Beispiel #20
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()
Beispiel #21
0
def update_course(course_id: int) -> ExtendedJSONResponse[models.Course]:
    """Update the given :class:`.models.Course` with new values.

    .. :quickref: Course; Update course data.

    :param int course_id: The id of the course you want to update.

    :returns: The updated course, in extended format.
    """
    data = rqa.FixedMapping(
        rqa.OptionalArgument(
            'name',
            rqa.SimpleValue.str,
            'The new name of the course',
        ),
        rqa.OptionalArgument(
            'state',
            rqa.EnumValue(models.CourseState),
            """
            The new state of the course, currently you cannot set the state of
            a course to 'deleted'
            """,
        )).from_flask()
    course = helpers.get_or_404(models.Course, course_id)
    checker = auth.CoursePermissions(course)
    checker.ensure_may_see()

    if data.name.is_just:
        if course.is_lti:
            raise APIException(
                'You cannot rename LTI courses',
                ('LTI courses get their name from the LMS, so renaming is'
                 ' not possible'), APICodes.INVALID_PARAM, 400)
        if not data.name.value:
            raise APIException(
                'The name of a course should contain at least one character',
                'A course name cannot be empty', APICodes.INVALID_PARAM, 400)
        checker.ensure_may_edit_info()
        course.name = data.name.value

    if data.state.is_just:
        if data.state.value.is_deleted:
            raise APIException(
                'It is not yet possible to delete a course',
                'Deleting courses in the API is not yet possible',
                APICodes.INVALID_PARAM, 400)
        checker.ensure_may_edit_state()
        course.state = data.state.value

    db.session.commit()

    return ExtendedJSONResponse.make(course, use_extended=models.Course)
Beispiel #22
0
def get_group_sets(
        course_id: int) -> JSONResponse[t.Sequence[models.GroupSet]]:
    """Get the all the :class:`.models.GroupSet` objects in the given course.

    .. :quickref: Course; Get all group sets in the course.

    :param int course_id: The id of the course of which the group sets should
        be retrieved.
    :returns: A list of group sets.
    """
    course = helpers.get_or_404(models.Course, course_id)
    auth.ensure_enrolled(course.id)
    return jsonify(course.group_sets)
Beispiel #23
0
def get_group_sets(
        course_id: int) -> JSONResponse[t.Sequence[models.GroupSet]]:
    """Get the all the group sets of a given course.

    .. :quickref: Course; Get all group sets in the course.

    :param int course_id: The id of the course of which the group sets should
        be retrieved.
    :returns: A list of group sets.
    """
    course = helpers.get_or_404(models.Course, course_id)
    auth.CoursePermissions(course).ensure_may_see()
    return jsonify(course.group_sets)
Beispiel #24
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()
Beispiel #25
0
def get_permissions_for_course(
    course_id: int, ) -> JSONResponse[t.Mapping[str, bool]]:
    """Get all the course :class:`.models.Permission` of the currently logged
    in :class:`.models.User`

    .. :quickref: Course; Get all the course permissions for the current user.

    :param int course_id: The id of the course of which the permissions should
        be retrieved.
    :returns: A mapping between the permission name and a boolean indicating if
        the currently logged in user has this permission.
    """
    course = helpers.get_or_404(models.Course, course_id)
    return jsonify(current_user.get_all_permissions(course))
Beispiel #26
0
def _get_non_expired_link(course_id: int,
                          link_id: uuid.UUID) -> models.CourseRegistrationLink:
    link = helpers.get_or_404(
        models.CourseRegistrationLink,
        link_id,
        also_error=lambda l: l.course_id != course_id or l.course.is_lti,
    )

    if link.expiration_date < helpers.get_request_start_time():
        raise APIException('This registration link has expired.',
                           f'The registration link {link.id} has expired',
                           APICodes.OBJECT_EXPIRED, 409)

    return link
Beispiel #27
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()
Beispiel #28
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()
Beispiel #29
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())
Beispiel #30
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)