Example #1
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)
Example #2
0
def get_course_or_global_permissions(
) -> JSONResponse[t.Union[GlobalPermMap, t.Mapping[int, CoursePermMap]]]:
    """Get all the global :class:`.psef.models.Permission` or the value of a
    permission in all courses of the currently logged in
    :class:`.psef.models.User`

    .. :quickref: Permission; Get global permissions or all the course
        permissions for the current user.

    :qparam str type: The type of permissions to get. This can be ``global`` or
        ``course``.
    :qparam str permission: The permissions to get when getting course
        permissions. You can pass this parameter multiple times to get multiple
        permissions. DEPRECATED: This option is deprecated, as it is preferred
        that you simply get all permissions for a course.

    :returns: The returning object depends on the given ``type``. If it was
        ``global`` a mapping between permissions name and a boolean indicating
        if the currently logged in user has this permissions is returned.

        If it was ``course`` such a mapping is returned for every course the
        user is enrolled in. So it is a mapping between course ids and
        permission mapping. The permissions given as ``permission`` query
        parameter are the only ones that are present in the permission
        map. When no ``permission`` query is given all course permissions are
        returned.
    """
    ensure_keys_in_dict(request.args, [('type', str)])
    permission_type = t.cast(str, request.args['type']).lower()

    if permission_type == 'global':
        return jsonify(GPerm.create_map(current_user.get_all_permissions()))
    elif permission_type == 'course' and 'permission' in request.args:
        add_deprecate_warning(
            'Requesting a subset of course permissions is deprecated')
        # Make sure at least one permission is present
        ensure_keys_in_dict(request.args, [('permission', str)])
        perm_names = t.cast(t.List[str], request.args.getlist('permission'))
        return jsonify({
            course_id: CPerm.create_map(v)
            for course_id, v in current_user.get_permissions_in_courses(
                [CPerm.get_by_name(p) for p in perm_names]).items()
        })
    elif permission_type == 'course':
        return jsonify({
            course_id: CPerm.create_map(v)
            for course_id, v in
            current_user.get_all_permissions_in_courses().items()
        })
    else:
        raise APIException(
            'Invalid permission type given',
            f'The given type "{permission_type}" is not "global" or "course"',
            APICodes.INVALID_PARAM,
            400,
        )
Example #3
0
def get_code(file_id: int) -> t.Union[werkzeug.wrappers.Response, JSONResponse[
    t.Union[t.Mapping[str, str], models.File, _FeedbackMapping]
]]:
    """Get data from the :class:`.models.File` with the given id.

    .. :quickref: Code; Get code or its metadata.

    The are several options to change the data that is returned. Based on the
    argument type in the request different functions are called.

    - If ``type == 'metadata'`` the JSON serialized :class:`.models.File` is
      returned.
    - If ``type == 'file-url'`` or ``type == 'pdf'`` (deprecated) an object
      with a single key, `name`, with as value the return values of
      :py:func:`.get_file_url`.
    - If ``type == 'feedback'`` or ``type == 'linter-feedback'`` see
      :py:func:`.code.get_feedback`
    - Otherwise the content of the file is returned as plain text.

    :param int file_id: The id of the file
    :returns: A response containing a plain text file unless specified
        otherwise.

    :raises APIException: If there is not file with the given id.
                          (OBJECT_ID_NOT_FOUND)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the file does not belong to user and the
                                 user can not view files in the attached
                                 course. (INCORRECT_PERMISSION)
    """
    file = helpers.filter_single_or_404(models.File, models.File.id == file_id)

    auth.ensure_can_view_files(file.work, file.fileowner == FileOwner.teacher)
    get_type = request.args.get('type', None)

    if get_type == 'metadata':
        return jsonify(file)
    elif get_type == 'feedback':
        return jsonify(get_feedback(file, linter=False))
    elif get_type == 'pdf' or get_type == 'file-url':
        return jsonify({'name': get_file_url(file)})
    elif get_type == 'linter-feedback':
        return jsonify(get_feedback(file, linter=True))
    else:
        contents = psef.files.get_file_contents(file)
        res: 'werkzeug.wrappers.Response' = make_response(contents)
        res.headers['Content-Type'] = 'application/octet-stream'
        return res
Example #4
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)
Example #5
0
def get_courses() -> JSONResponse[t.Sequence[t.Mapping[str, t.Any]]]:
    """Return all :class:`.models.Course` objects the current user is a member
    of.

    .. :quickref: Course; Get all courses the current user is enrolled in.

    :returns: A response containing the JSON serialized courses

    :param str extended: If set to `true` all the assignments for each course
        are also included under the key `assignments`.

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

    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    """
    def _get_rest(course: models.Course) -> t.Mapping[str, t.Any]:
        if request.args.get('extended') == 'true':
            return {
                'assignments': course.get_all_visible_assignments(),
                **course.__to_json__(),
            }
        return course.__to_json__()

    return jsonify([{
        'role': c.name,
        **_get_rest(c.course),
    } for c in current_user.courses.values()])
Example #6
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)
Example #7
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,
                )
        }
    )
Example #8
0
def add_snippet() -> JSONResponse[models.Snippet]:
    """Add or modify a :class:`.models.Snippet` by key.

    .. :quickref: Snippet; Add or modify a 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" and/or "value" 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 user snippets
                                 (INCORRECT_PERMISSION)
    """
    content = ensure_json_dict(request.get_json())
    ensure_keys_in_dict(content, [('value', str), ('key', str)])
    value = t.cast(str, content['value'])

    snippet: t.Optional[models.Snippet] = models.Snippet.query.filter_by(
        user_id=current_user.id, key=content['key']).first()
    if snippet is None:
        snippet = models.Snippet(key=content['key'],
                                 value=content['value'],
                                 user=current_user)
        db.session.add(snippet)
    else:
        snippet.value = value

    db.session.commit()

    return jsonify(snippet, status_code=201)
Example #9
0
def register_user_in_course(
        course_id: int,
        link_id: uuid.UUID) -> JSONResponse[t.Mapping[str, str]]:
    """Register as a new user, and directly enroll in a course.

    .. :quickref: Course; Register as a new user, and enroll in a 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.
    :>json access_token: The access token that the created user can use to
        login.
    """
    link = _get_non_expired_link(course_id, link_id)

    if not link.allow_register:
        raise PermissionException(
            'You are not allowed to register using this link',
            'This link does not support registration',
            APICodes.INCORRECT_PERMISSION, 403)

    with get_from_map_transaction(get_json_dict_from_request()) as [get, _]:
        username = get('username', str)
        password = get('password', str)
        email = get('email', str)
        name = get('name', str)

    user = models.User.register_new_user(username=username,
                                         password=password,
                                         email=email,
                                         name=name)
    user.courses[link.course_id] = link.course_role
    db.session.commit()

    return jsonify({'access_token': user.make_access_token()})
Example #10
0
def get_courses() -> JSONResponse[t.Sequence[t.Mapping[str, t.Any]]]:
    """Return all :class:`.models.Course` objects the current user is a member
    of.

    .. :quickref: Course; Get all courses the current user is enrolled in.

    :returns: A response containing the JSON serialized courses

    :param str extended: If set to ``true``, ``1`` or the empty string all the
        assignments and group sets for each course are also included under the
        key ``assignments`` and ``group_sets`` respectively.

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

    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    """
    def _get_rest(course: models.Course) -> t.Mapping[str, t.Any]:
        if helpers.extended_requested():
            snippets: t.Sequence[models.CourseSnippet] = []
            if (current_user.has_permission(GPerm.can_use_snippets)
                    and current_user.has_permission(
                        CPerm.can_view_course_snippets, course_id=course.id)):
                snippets = course.snippets

            return {
                'assignments': course.get_all_visible_assignments(),
                'group_sets': course.group_sets,
                'snippets': snippets,
                **course.__to_json__(),
            }
        return course.__to_json__()

    extra_loads: t.Optional[t.List[t.Any]] = None
    if helpers.extended_requested():
        extra_loads = [
            selectinload(models.Course.assignments),
            selectinload(models.Course.snippets),
            selectinload(models.Course.group_sets)
        ]

    # We don't use `helpers.get_or_404` here as preloading doesn't seem to work
    # when we do.
    user = models.User.query.filter_by(id=current_user.id).options([
        selectinload(models.User.courses, ).selectinload(
            models.CourseRole._permissions,  # pylint: disable=protected-access
        ),
    ]).first()
    assert user is not None

    return jsonify([{
        'role': user.courses[c.id].name,
        **_get_rest(c),
    } for c in helpers.get_in_or_error(
        models.Course,
        t.cast(models.DbColumn[int], models.Course.id),
        [cr.course_id for cr in user.courses.values()],
        extra_loads,
    )])
Example #11
0
def search_users() -> JSONResponse[t.Sequence[models.User]]:
    """Search for a user by name and username.

    .. :quickref: User; Fuzzy search for a user by name and username.

    :param str q: The string to search for, all SQL wildcard are escaped and
        spaces are replaced by wildcards.

    :returns: A list of :py:class:`.models.User` objects that match the given
        query string.

    :raises APIException: If the query string less than 3 characters
        long. (INVALID_PARAM)
    :raises PermissionException: If the currently logged in user does not have
        the permission ``can_search_users``. (INCORRECT_PERMISSION)
    :raises RateLimitExceeded: If you hit this end point more than once per
        second. (RATE_LIMIT_EXCEEDED)
    """
    ensure_keys_in_dict(request.args, [('q', str)])
    query = t.cast(str, request.args.get('q'))

    if len(query) < 3:
        raise APIException(
            'The search string should be at least 3 chars',
            f'The search string "{query}" is not 3 chars or longer.',
            APICodes.INVALID_PARAM, 400)

    likes = [
        t.cast(t.Any, col).ilike('%{}%'.format(
            escape_like(query).replace(' ', '%'), ))
        for col in [models.User.name, models.User.username]
    ]
    return jsonify(models.User.query.filter(or_(*likes)).all())
Example #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)
Example #13
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())
Example #14
0
def add_course() -> JSONResponse[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)
    """
    content = get_json_dict_from_request()
    ensure_keys_in_dict(content, [('name', str)])
    name = t.cast(str, content['name'])

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

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

    return jsonify(new_course)
Example #15
0
def get_all_roles() -> JSONResponse[t.Sequence[t.Mapping[str, t.Any]]]:
    """Get all global roles with their permissions

    .. :quickref: Role; Get all global roles with their permissions.

    :returns: A object as described in :py:meth:`.models.Role.__to_json__` with
        the following keys added:

    - ``perms``: All permissions of this role, as described in
      :py:meth:`.models.Role.get_all_permissions`.
    - ``own``: Is the given role the role of the current user.

    :raises PermissionException: If the current user does not have the
        ``can_manage_site_users`` permission. (INCORRECT_PERMISSION)
    """
    roles: t.Sequence[models.Role]
    roles = models.Role.query.order_by(models.Role.name).all()  # type: ignore

    res = []
    for role in roles:
        json_role = role.__to_json__()
        json_role['perms'] = role.get_all_permissions()
        json_role['own'] = current_user.role_id == role.id
        res.append(json_role)

    return jsonify(res)
Example #16
0
def get_course_data(course_id: int) -> JSONResponse[t.Mapping[str, t.Any]]:
    """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)
    """
    # TODO: Optimize this loop to a single query
    for course_role in current_user.courses.values():
        if course_role.course_id == course_id:
            return jsonify({
                'role': course_role.name,
                **course_role.course.__to_json__(),
            })

    raise APIException('Course not found',
                       'The course with id {} was not found'.format(course_id),
                       APICodes.OBJECT_ID_NOT_FOUND, 404)
Example #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,
        )
    })
Example #18
0
def post_file() -> JSONResponse[str]:
    """Temporarily store some data on the server.

    .. :quickref: File; Safe a file temporarily on the server.

    .. note::
        The posted data will be removed after 60 seconds.

    :returns: A response with the JSON serialized name of the file as content
              and return code 201.

    :raises APIException: If the request is bigger than the maximum upload
                          size. (REQUEST_TOO_LARGE)
    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    """
    if (request.content_length
            and request.content_length > app.config['MAX_FILE_SIZE']):
        raise APIException(
            'Uploaded file is too big.',
            'Request is bigger than maximum upload size of {}.'.format(
                app.config['MAX_FILE_SIZE']), APICodes.REQUEST_TOO_LARGE, 400)

    path, name = psef.files.random_file_path(True)

    FileStorage(request.stream).save(path)

    return jsonify(name, status_code=201)
Example #19
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)
Example #20
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))
Example #21
0
def get_course_permissions(
) -> JSONResponse[t.Union[_PermMap, t.Mapping[int, _PermMap]]]:
    """Get all the global :class:`.psef.models.Permission` or the value of a
    permission in all courses of the currently logged in
    :class:`.psef.models.User`

    .. :quickref: Permission; Get global permissions or all the course
        permissions for the current user.

    :qparam str type: The type of permissions to get. This can be ``global`` or
        ``course``.
    :qparam str permission: The permissions to get when getting course
        permissions. You can pass this parameter multiple times to get multiple
        permissions.

    :returns: The returning object depends on the given ``type``. If it was
        ``global`` a mapping between permissions name and a boolean indicating
        if the currently logged in user has this permissions is returned.

        If it was ``course`` such a mapping is returned for every course the
        user is enrolled in. So it is a mapping between course ids and
        permission mapping. The permissions given as ``permission`` query
        parameter are the only ones that are present in the permission map.
    """
    ensure_keys_in_dict(request.args, [('type', str)])
    permission_type = t.cast(str, request.args['type']).lower()

    if permission_type == 'global':
        return jsonify(current_user.get_all_permissions())
    elif permission_type == 'course':
        # Make sure at least one permission is present
        ensure_keys_in_dict(request.args, [('permission', str)])
        perms = t.cast(t.List[str], request.args.getlist('permission'))
        return jsonify(current_user.get_permissions_in_courses(perms))
    else:
        raise APIException(
            'Invalid permission type given',
            f'The given type "{permission_type}" is not "global" or "course"',
            APICodes.INVALID_PARAM,
            400,
        )
Example #22
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)
Example #23
0
def self_information(
) -> t.Union[JSONResponse[t.Union[models.User, t.MutableMapping[str, t.Any], t.
                                  Mapping[int, str]]],
             ExtendedJSONResponse[t.Union[models.User, t.MutableMapping[str, t.
                                                                        Any]]],
             ]:
    """Get the info of the currently logged in :class:`.models.User`.

    .. :quickref: User; Get information about the currently logged in user.

    :query type: If this is ``roles`` a mapping between course_id and role name
        will be returned, if this is ``extended`` the result of
        :py:meth:`.models.User.__extended_to_json__()` will be returned. If
        this is something else or not present the result of
        :py:meth:`.models.User.__to_json__()` will be returned.
    :query with_permissions: Setting this to true will add the key
        ``permissions`` to the user. The value will be a mapping indicating
        which global permissions this user has.
    :returns: A response containing the JSON serialized user

    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    """
    args = request.args
    if args.get('type') == 'roles':
        return jsonify(
            {
                role.course_id: role.name
                for role in current_user.courses.values()
            }
        )

    elif helpers.extended_requested() or args.get('type') == 'extended':
        obj = current_user.__extended_to_json__()
        if request_arg_true('with_permissions'):
            obj['permissions'] = GPerm.create_map(
                current_user.get_all_permissions()
            )
        return extended_jsonify(obj)
    return jsonify(current_user)
Example #24
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)
Example #25
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)
Example #26
0
def second_phase_lti_launch() -> helpers.JSONResponse[t.Mapping[str, t.Union[
    str, models.Assignment, bool]]]:
    """Do the second part of an LTI launch.

    .. :quickref: LTI; Do the callback of a LTI launch.

    :query string Jwt: The JWT token that is the current LTI state. This token
        can only be acquired using the ``/lti/launch/1`` route.

    :>json assignment: The assignment that the LTI launch was for.
    :>json bool new_role_created: Was a new role created in the LTI launch.
    :>json access_token: A fresh access token for the current user. This value
        is not always available, this depends on internal state so you should
        simply check.
    :>json updated_email: The new email of the current user. This is value is
        also not always available, check!
    :raises APIException: If the given Jwt token is not valid. (INVALID_PARAM)
    """
    try:
        launch_params = jwt.decode(flask.request.headers.get('Jwt', None),
                                   app.config['LTI_SECRET_KEY'],
                                   algorithm='HS512')['params']
    except jwt.DecodeError:
        traceback.print_exc()
        raise errors.APIException(
            ('Decoding given JWT token failed, LTI is probably '
             'not configured right. Please contact your site admin.'),
            f'The decoding of "{flask.request.headers.get("Jwt")}" failed.',
            errors.APICodes.INVALID_PARAM,
            400,
        )
    lti = CanvasLTI(launch_params)

    user, new_token, updated_email = lti.ensure_lti_user()
    course = lti.get_course()
    assig = lti.get_assignment(user)
    lti.set_user_role(user)
    new_role_created = lti.set_user_course_role(user, course)
    db.session.commit()

    result: t.Mapping[str, t.Union[str, models.Assignment, bool]]
    result = {
        'assignment': assig,
        'new_role_created': new_role_created,
    }
    if new_token is not None:
        result['access_token'] = new_token
    if updated_email:
        result['updated_email'] = updated_email

    return helpers.jsonify(result)
Example #27
0
def get_snippets() -> JSONResponse[t.Sequence[models.Snippet]]:
    """Get all snippets (:class:`.models.Snippet`) of the current
    :class:`.models.User`.

    .. :quickref: Snippet; Get all snippets for the currently logged in user.

    :returns: An array containing all snippets for the currently logged in
        user.

    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    :raises PermissionException: If the user can not use snippets.
        (INCORRECT_PERMISSION)
    """
    return jsonify(models.Snippet.get_all_snippets(current_user))
Example #28
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))
Example #29
0
def self_information() -> t.Union[JSONResponse[t.Union[models.User, t.Mapping[
    int, str]]], ExtendedJSONResponse[models.User], ]:
    """Get the info of the currently logged in :class:`.models.User`.

    .. :quickref: User; Get information about the currently logged in user.

    :query type: If this is ``roles`` a mapping between course_id and role name
        will be returned, if this is ``extended`` the result of
        :py:meth:`.models.User.__extended_to_json__()` will be returned. If
        this is something else or not present the result of
        :py:meth:`.models.User.__to_json__()` will be returned.
    :returns: A response containing the JSON serialized user

    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    """
    if request.args.get('type') == 'roles':
        return jsonify({
            role.course_id: role.name
            for role in current_user.courses.values()
        })
    elif request.args.get('type') == 'extended':
        return extended_jsonify(current_user)
    return jsonify(current_user)
Example #30
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())