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

    return jsonify(course.get_all_visible_assignments())
Пример #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)
    """
    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)

    db.session.delete(snip)
    db.session.commit()
    return make_empty_response()
Пример #3
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)
Пример #4
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)
Пример #5
0
def delete_role(course_id: int, role_id: int) -> EmptyResponse:
    """Remove a CourseRole from the given Course.

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

    :param int course_id: The id of the course
    :param int role_id: The id of the role you want to delete
    :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)
    """
    course = helpers.get_or_404(
        models.Course,
        course_id,
        also_error=lambda c: c.virtual,
    )
    auth.CoursePermissions(course).ensure_may_edit_roles()
    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()
Пример #6
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()
Пример #7
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)
Пример #8
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)
Пример #9
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()
Пример #10
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.CoursePermissions(course).ensure_may_edit_users()
    return jsonify(course.registration_links)
Пример #11
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)
Пример #12
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()
Пример #13
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`
    """
    course = helpers.get_or_404(models.Course, course_id)
    auth.CoursePermissions(course).ensure_may_see_users()

    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))
Пример #14
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.CoursePermissions(course).ensure_may_edit_users()
    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()
Пример #15
0
def get_course_snippets(
        course_id: int) -> JSONResponse[t.Sequence[models.CourseSnippet]]:
    """Get all snippets (:class:`.models.CourseSnippet`) of the given
    :class:`.models.Course`.

    .. :quickref: Course; Get all snippets for the given course.

    :param int course_id: The id of the course from which you want to get the
        snippets.

    :returns: An array containing all snippets for the given course.

    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not use snippets.
        (INCORRECT_PERMISSION)
    :raises PermissionException: If the user can not manage snippets for this
        course. (INCORRECT_PERMISSION)
    """
    course = helpers.get_or_404(models.Course, course_id)
    auth.CoursePermissions(course).ensure_may_see_snippets()
    return jsonify(course.snippets)
Пример #16
0
def create_or_edit_registration_link(
        course_id: int) -> JSONResponse[models.CourseRegistrationLink]:
    """Create or edit an enroll 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.
    :returns: The created or edited link.
    """
    data = rqa.FixedMapping(
        rqa.OptionalArgument(
            'id',
            rqa.RichValue.UUID,
            'The id of the link to edit, omit to create a new link.',
        ),
        rqa.RequiredArgument(
            'role_id',
            rqa.SimpleValue.int,
            """
            The id of the role that users should get when enrolling with this
            link.
            """,
        ),
        rqa.RequiredArgument(
            'expiration_date',
            rqa.RichValue.DateTime,
            'The date this link should stop working.',
        ),
        rqa.OptionalArgument(
            'allow_register',
            rqa.SimpleValue.bool,
            """
            Should students be allowed to register a new account using this
            link. For registration to actually work this feature should be
            enabled.
            """,
        ),
    ).from_flask()
    course = helpers.get_or_404(models.Course,
                                course_id,
                                also_error=lambda c: c.virtual)
    auth.CoursePermissions(course).ensure_may_edit_users()
    if course.is_lti:
        raise APIException(
            'You cannot create course enroll links in LTI courses',
            f'The course {course.id} is an LTI course', APICodes.INVALID_PARAM,
            400)

    if data.id.is_nothing:
        link = models.CourseRegistrationLink(course=course)
        db.session.add(link)
    else:
        link = helpers.filter_single_or_404(
            models.CourseRegistrationLink,
            models.CourseRegistrationLink.id == data.id.value,
            also_error=lambda l: l.course_id != course.id)

    link.course_role = helpers.get_or_404(
        models.CourseRole,
        data.role_id,
        also_error=lambda r: r.course_id != course.id)
    if data.allow_register.is_just:
        link.allow_register = data.allow_register.value

    link.expiration_date = data.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)
Пример #17
0
def get_user_submissions(
    course_id: int, user_id: int
) -> MultipleExtendedJSONResponse[t.Mapping[int, t.Sequence[models.Work]],
                                  models.Work]:
    """Get all :class:`.models.Work`s by the given :class:`.models.User` in the
    given :class:`.models.Course`.

    .. :quickref: Course; Get submissions by user in a course.

    :qparam boolean latest_only: Only get the latest submission of a
        user. Please use this option if at all possible, as students have a
        tendency to submit many attempts and that can make this route quite
        slow.

    :param int course_id: The id of the course
    :param int user_id: The id of the user
    :returns: A response containing the JSON serialized submissions.

    :raises NotFoundException: If the course does not exist.
        (OBJECT_ID_NOT_FOUND)
    :raises NotFoundException: If the user does not exist.
        (OBJECT_ID_NOT_FOUND)
    :raises APIException: If the given user is not member of the course.
        (INVALID_PARAM)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the given user is not the logged in user
        and the logged in user does not have the permission to see others work.
        (INCORRECT_PERMISSION)
    """
    course = helpers.get_or_404(models.Course, course_id)
    auth.CoursePermissions(course).ensure_may_see()
    assignments = course.get_all_visible_assignments()

    user = helpers.get_or_404(models.User, user_id)
    if any(not u.is_enrolled(course_id) for u in user.get_contained_users()):
        raise APIException(
            'User is not enrolled in this course',
            f'User {user_id} not enrolled in course {course_id}',
            APICodes.INVALID_PARAM, 400)
    elif not user.contains_user(current_user):
        auth.ensure_permission(CPerm.can_see_others_work, course.id)

    latest_only = helpers.request_arg_true('latest_only')

    def get_subs(query: models.MyQuery[models.Work]) -> t.List[models.Work]:
        return models.Work.update_query_for_extended_jsonify(
            query.filter(models.Work.user_submissions_filter(user), )).all()

    if latest_only:
        subs = {}
        for assignment in assignments:
            subs[assignment.id] = get_subs(
                assignment.get_all_latest_submissions(), )
    else:
        query = models.Work.query.filter(
            models.Work.assignment_id.in_([a.id for a in assignments]),
            # Use _deleted because we already know the assignment exists.
            ~models.Work._deleted,  # pylint: disable=protected-access
        ).order_by(models.Work.created_at.asc())
        subs = {assig.id: [] for assig in assignments}
        for sub in get_subs(query):
            subs[sub.assignment_id].append(sub)

    return MultipleExtendedJSONResponse.make(
        subs,
        use_extended=models.Work,
    )
Пример #18
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
Пример #19
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.CoursePermissions(course).ensure_may_see()
    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)