Пример #1
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()
Пример #2
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()
Пример #3
0
def create_course_snippet(
        course_id: int) -> JSONResponse[models.CourseSnippet]:
    """Add or modify a :class:`.models.CourseSnippet` by key.

    .. :quickref: Course; 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)
    """
    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()

    snippet = models.CourseSnippet.query.filter_by(
        course=course,
        key=key,
    ).one_or_none()

    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)
Пример #4
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)
    """
    course = helpers.get_or_404(
        models.Course,
        course_id,
        also_error=lambda c: c.virtual,
    )
    auth.CoursePermissions(course).ensure_may_edit_roles()

    with helpers.get_from_request_transaction() as [get, _]:
        name = get('name', str)

    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()
Пример #5
0
def add_course() -> ExtendedJSONResponse[models.Course]:
    """Add a new :class:`.models.Course`.

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

    :returns: A response containing the JSON serialization of the new course

    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not create courses.
                                 (INCORRECT_PERMISSION)
    :raises APIException: If the parameter "name" is not in the request.
        (MISSING_REQUIRED_PARAM)
    """
    with helpers.get_from_request_transaction() as [get, _]:
        name = get('name', str)

    new_course = models.Course.create_and_add(name)
    db.session.flush()

    role = models.CourseRole.get_initial_course_role(new_course)
    current_user.courses[new_course.id] = role
    db.session.commit()

    return ExtendedJSONResponse.make(new_course, use_extended=models.Course)
Пример #6
0
def send_students_an_email(course_id: int) -> JSONResponse[models.TaskResult]:
    """Sent the authors in this course an email.

    .. :quickref: Course; Send users in this course an email.

    :>json subject: The subject of the email to send, should not be empty.
    :>json body: The body of the email to send, should not be empty.
    :>json email_all_users: If true all users are emailed, except for those in
        ``usernames``. If ``false`` no users are emailed except for those in
        ``usernames``.
    :>jsonarr usernames: The usernames of the users to which you want to send
        an email (or not if ``email_all_users`` is ``true``).
    :returns: A task result that will send these emails.
    """
    course = helpers.filter_single_or_404(
        models.Course,
        models.Course.id == course_id,
        also_error=lambda c: c.virtual,
    )
    auth.ensure_permission(CPerm.can_email_students, course.id)

    with helpers.get_from_request_transaction() as [get, _]:
        subject = get('subject', str)
        body = get('body', str)
        email_all_users = get('email_all_users', bool)
        usernames: t.List[str] = get('usernames', list)

    if helpers.contains_duplicate(usernames):
        raise APIException('The given exceptions list contains duplicates',
                           'Each exception can only be mentioned once',
                           APICodes.INVALID_PARAM, 400)

    exceptions = helpers.get_in_or_error(
        models.User,
        models.User.username,
        usernames,
        same_order_as_given=True,
    )

    if any(course_id not in u.courses for u in exceptions):
        raise APIException(
            'Not all given users are enrolled in this course',
            f'Some given users are not enrolled in course {course_id}',
            APICodes.INVALID_PARAM, 400)

    if not (subject and body):
        raise APIException(
            'Both a subject and body should be given',
            (f'One or both of the given subject ({subject}) or body'
             f' ({body}) is empty'), APICodes.INVALID_PARAM, 400)

    if email_all_users:
        recipients = course.get_all_users_in_course(
            include_test_students=False).filter(
                models.User.id.notin_([e.id for e in exceptions
                                       ])).with_entities(models.User).all()
    else:
        recipients = exceptions
        # The test student cannot be a member of a group, so we do not need to
        # run this on the expanded group members, and we also do not want to
        # run it when `email_all_users` is true because in that case we let the
        # DB handle it for us.
        if any(r.is_test_student for r in recipients):
            raise APIException('Cannot send an email to the test student',
                               'Test student was selected',
                               APICodes.INVALID_PARAM, 400)

    recipients = helpers.flatten(r.get_contained_users() for r in recipients)

    if not recipients:
        raise APIException(
            'At least one recipient should be given as recipient',
            'No recipients were selected', APICodes.INVALID_PARAM, 400)

    task_result = models.TaskResult(current_user)
    db.session.add(task_result)
    db.session.commit()

    psef.tasks.send_email_as_user(
        receiver_ids=[u.id for u in recipients],
        subject=subject,
        body=body,
        task_result_hex_id=task_result.id.hex,
        sender_id=current_user.id,
    )

    return JSONResponse.make(task_result)
Пример #7
0
def create_group_set(course_id: int) -> JSONResponse[models.GroupSet]:
    """Create or update a :class:`.models.GroupSet` in the given course id.

    .. :quickref: Course; Create a new group set in the course.

    :>json int minimum_size: The minimum size attribute that the group set
        should have.
    :>json int maximum_size: The maximum size attribute that the group set
        should have.
    :>json int id: The id of the group to update.
    :param course_id: The id of the course in which the group set should be
        created or updated. The course id of a group set cannot change.
    :returns: The created or updated group.
    """
    course = helpers.get_or_404(models.Course, course_id)

    with helpers.get_from_request_transaction() as [get, opt_get]:
        min_size = get('minimum_size', int)
        max_size = get('maximum_size', int)
        old_id = opt_get('id', int)

    if min_size <= 0:
        raise APIException('Minimum size should be larger than 0',
                           f'Minimum size "{min_size}" is <= than 0',
                           APICodes.INVALID_PARAM, 400)
    elif max_size < min_size:
        raise APIException('Maximum size is smaller than minimum size',
                           (f'Maximum size "{max_size}" is smaller '
                            f'than minimum size "{min_size}"'),
                           APICodes.INVALID_PARAM, 400)

    if old_id is helpers.MISSING:
        group_set = models.GroupSet(course_id=course.id, course=course)
        models.db.session.add(group_set)
        auth.GroupSetPermissions(group_set).ensure_may_add()
    else:
        group_set = helpers.get_or_404(models.GroupSet, old_id)
        auth.GroupSetPermissions(group_set).ensure_may_edit()

        if group_set.course_id != course.id:
            raise APIException(
                'You cannot change the course id of a group set',
                (f'The group set {group_set.id} is '
                 f'not connected to course {course.id}'),
                APICodes.INVALID_PARAM, 400)

    if group_set.largest_group_size > max_size:
        raise APIException('There are groups larger than the new maximum size',
                           f'Some groups have more than {max_size} members',
                           APICodes.INVALID_PARAM, 400)
    elif group_set.smallest_group_size < min_size:
        raise APIException(
            'There are groups smaller than the new minimum size',
            f'Some groups have less than {min_size} members',
            APICodes.INVALID_PARAM, 400)
    group_set.minimum_size = min_size
    group_set.maximum_size = max_size

    models.db.session.commit()

    return jsonify(group_set)
Пример #8
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.
    """
    course = helpers.get_or_404(models.Course, course_id)
    auth.CoursePermissions(course).ensure_may_edit_users()
    with helpers.get_from_request_transaction() as [get, opt_get]:
        role_id = get('role_id', int)
        user_id = opt_get('user_id', int, None)
        username = opt_get('username', str, None)

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

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

    if user_id is not None:
        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 is not None:
        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 "username" were found',
            ('The given content ({})'
             ' does  not contain "user_id" or "username"').format(
                 helpers.get_json_dict_from_request()),
            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