Example #1
0
class LibraryBlocksView(APIView):
    """
    Views to work with XBlocks in a specific content library.
    """
    @apidocs.schema(
        parameters=[
            *LibraryApiPagination.apidoc_params,
            apidocs.query_parameter(
                'text_search',
                str,
                description="The string used to filter libraries by searching in title, id, org, or description",
            ),
        ],
    )
    @convert_exceptions
    def get(self, request, lib_key_str):
        """
        Get the list of all top-level blocks in this content library
        """
        key = LibraryLocatorV2.from_string(lib_key_str)
        text_search = request.query_params.get('text_search', None)

        api.require_permission_for_library_key(key, request.user, permissions.CAN_VIEW_THIS_CONTENT_LIBRARY)
        result = api.get_library_blocks(key, text_search=text_search)

        # Verify `pagination` param to maintain compatibility with older
        # non pagination-aware clients
        if request.GET.get('pagination', 'false').lower() == 'true':
            paginator = LibraryApiPagination()
            result = paginator.paginate_queryset(result, request)
            serializer = LibraryXBlockMetadataSerializer(result, many=True)
            return paginator.get_paginated_response(serializer.data)

        return Response(LibraryXBlockMetadataSerializer(result, many=True).data)

    @convert_exceptions
    def post(self, request, lib_key_str):
        """
        Add a new XBlock to this content library
        """
        library_key = LibraryLocatorV2.from_string(lib_key_str)
        api.require_permission_for_library_key(library_key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY)
        serializer = LibraryXBlockCreationSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        parent_block_usage_str = serializer.validated_data.pop("parent_block", None)
        if parent_block_usage_str:
            # Add this as a child of an existing block:
            parent_block_usage = LibraryUsageLocatorV2.from_string(parent_block_usage_str)
            if parent_block_usage.context_key != library_key:
                raise ValidationError(detail={"parent_block": "Usage ID doesn't match library ID in the URL."})
            result = api.create_library_block_child(parent_block_usage, **serializer.validated_data)
        else:
            # Create a new regular top-level block:
            try:
                result = api.create_library_block(library_key, **serializer.validated_data)
            except api.IncompatibleTypesError as err:
                raise ValidationError(
                    detail={'block_type': str(err)},
                )
        return Response(LibraryXBlockMetadataSerializer(result).data)
Example #2
0
class LibraryApiPagination(PageNumberPagination):
    """
    Paginates over ContentLibraryMetadata objects.
    """
    page_size = 50
    page_size_query_param = 'page_size'

    apidoc_params = [
        apidocs.query_parameter(
            'pagination',
            bool,
            description="Enables paginated schema",
        ),
        apidocs.query_parameter(
            'page',
            int,
            description="Page number of result. Defaults to 1",
        ),
        apidocs.query_parameter(
            'page_size',
            int,
            description="Page size of the result. Defaults to 50",
        ),
    ]
Example #3
0
class UserProgramCourseEnrollmentView(
        DeveloperErrorViewMixin,
        UserProgramSpecificViewMixin,
        PaginatedAPIView,
):
    """
    A view for getting data associated with a user's course enrollments
    as part of a program enrollment.

    For full documentation, see the `program_enrollments` section of
    http://$LMS_BASE_URL/api-docs/.
    """
    authentication_classes = (
        JwtAuthentication,
        BearerAuthenticationAllowInactiveUser,
        SessionAuthenticationAllowInactiveUser,
    )
    permission_classes = (IsAuthenticated, )
    serializer_class = CourseRunOverviewSerializer
    pagination_class = UserProgramCourseEnrollmentPagination

    @schema(
        parameters=[
            path_parameter(
                'username',
                str,
                description=
                ('The username of the user for which enrollment overviews will be fetched. '
                 'For now, this must be the requesting user; otherwise, 403 will be returned. '
                 'In the future, global staff users may be able to supply other usernames.'
                 )),
            path_parameter(
                'program_uuid',
                str,
                description=
                ('UUID of a program. '
                 'Enrollments will be returned for course runs in this program.'
                 )),
            query_parameter(
                'page_size',
                int,
                description=('Number of results to return per page. '
                             'Defaults to 10. Maximum is 25.')),
        ],
        responses={
            200:
            cursor_paginate_serializer(CourseRunOverviewSerializer),
            401:
            'The requester is not authenticated.',
            403:
            ('The requester cannot access the specified program and/or '
             'the requester may not retrieve this data for the specified user.'
             ),
            404:
            'The requested program does not exist.'
        },
    )
    @verify_program_exists
    @verify_user_enrolled_in_program
    def get(self, request, username, program_uuid):  # lint-amnesty, pylint: disable=unused-argument
        """
        Get an overview of each of a user's course enrollments associated with a program.

        This endpoint exists to get an overview of each course-run enrollment
        that a user has for course-runs within a given program.
        Fields included are the title, upcoming due dates, etc.
        This API endpoint is intended for use with the
        [Program Learner Portal MFE](https://github.com/edx/frontend-app-learner-portal-programs).

        It is important to note that the set of enrollments that this endpoint returns
        is different than a user's set of *program-course-run enrollments*.
        Specifically, this endpoint may include course runs that are *within*
        the specified program but were not *enrolled in* via the specified program.

        **Example Response:**
        ```json
        {
            "next": null,
            "previous": null,
            "results": [
                {
                    "course_run_id": "edX+AnimalsX+Aardvarks",
                    "display_name": "Astonishing Aardvarks",
                    "course_run_url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Aardvarks/course/",
                    "start_date": "2017-02-05T05:00:00Z",
                    "end_date": "2018-02-05T05:00:00Z",
                    "course_run_status": "completed"
                    "emails_enabled": true,
                    "due_dates": [
                        {
                            "name": "Introduction: What even is an aardvark?",
                            "url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Aardvarks/jump_to/
                                  block-v1:edX+AnimalsX+Aardvarks+type@chapter+block@1414ffd5143b4b508f739b563ab468b7",
                            "date": "2017-05-01T05:00:00Z"
                        },
                        {
                            "name": "Quiz: Aardvark or Anteater?",
                            "url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Aardvarks/jump_to/
                                    block-v1:edX+AnimalsX+Aardvarks+type@sequential+block@edx_introduction",
                            "date": "2017-03-05T00:00:00Z"
                        }
                    ],
                    "micromasters_title": "Animals",
                    "certificate_download_url": "https://courses.edx.org/certificates/123"
                },
                {
                    "course_run_id": "edX+AnimalsX+Baboons",
                    "display_name": "Breathtaking Baboons",
                    "course_run_url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Baboons/course/",
                    "start_date": "2018-02-05T05:00:00Z",
                    "end_date": null,
                    "course_run_status": "in_progress"
                    "emails_enabled": false,
                    "due_dates": [],
                    "micromasters_title": "Animals",
                    "certificate_download_url": "https://courses.edx.org/certificates/123",
                    "resume_course_run_url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Baboons/jump_to/
                                               block-v1:edX+AnimalsX+Baboons+type@sequential+block@edx_introduction"
                }
            ]
        }
        ```
        """
        if request.user.username != username:
            # TODO: Should this be case-insensitive?
            raise PermissionDenied()
        enrollments = get_enrollments_for_courses_in_program(
            self.request.user, self.program)
        paginated_enrollments = self.paginate_queryset(enrollments)
        paginated_enrollment_overviews = get_enrollment_overviews(
            user=self.request.user,
            program=self.program,
            enrollments=paginated_enrollments,
            request=self.request,
        )
        serializer = CourseRunOverviewSerializer(
            paginated_enrollment_overviews, many=True)
        return self.get_paginated_response(serializer.data)
Example #4
0
        return Response(status=status.HTTP_200_OK, data=course_list)


@schema_for(
    "list",
    """
    Get the list of all enrollments linked to this classroom.
    """,
)
@schema_for(
    "retrieve",
    """
    Fetch details for a single classroom enrollment by id.
    """,
    parameters=[query_parameter("id", int, "Enrollment id")],
)
# TODO Improve Parameter display
@schema_for(
    "create",
    """
    Create a classroom enrollment.

    **POST Parameters**

        A POST request must include an emails.

        * user_id: ID of the user enrolled in the Classroom
    """,
)
class ClassroomEnrollmentViewSet(viewsets.ModelViewSet):
Example #5
0
class LibraryRootView(APIView):
    """
    Views to list, search for, and create content libraries.
    """
    @apidocs.schema(
        parameters=[
            apidocs.query_parameter(
                'pagination',
                bool,
                description="Enables paginated schema",
            ),
            apidocs.query_parameter(
                'page',
                int,
                description="Page number of result. Defaults to 1",
            ),
            apidocs.query_parameter(
                'page_size',
                int,
                description="Page size of the result. Defaults to 50",
            ),
        ], )
    def get(self, request):
        """
        Return a list of all content libraries that the user has permission to view.
        """
        paginator = LibraryRootPagination()
        queryset = api.get_libraries_for_user(request.user)
        paginated_qs = paginator.paginate_queryset(queryset, request)
        result = api.get_metadata_from_index(paginated_qs)
        serializer = ContentLibraryMetadataSerializer(result, many=True)
        # Verify `pagination` param to maintain compatibility with older
        # non pagination-aware clients
        if request.GET.get('pagination', 'false').lower() == 'true':
            return paginator.get_paginated_response(serializer.data)
        return Response(serializer.data)

    def post(self, request):
        """
        Create a new content library.
        """
        if not request.user.has_perm(permissions.CAN_CREATE_CONTENT_LIBRARY):
            raise PermissionDenied
        serializer = ContentLibraryMetadataSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        data = serializer.validated_data
        # Get the organization short_name out of the "key.org" pseudo-field that the serializer added:
        org_name = data["key"]["org"]
        # Move "slug" out of the "key.slug" pseudo-field that the serializer added:
        data["slug"] = data.pop("key")["slug"]
        try:
            org = Organization.objects.get(short_name=org_name)
        except Organization.DoesNotExist:
            raise ValidationError(
                detail={
                    "org": "No such organization '{}' found.".format(org_name)
                })
        try:
            result = api.create_library(org=org, **data)
        except api.LibraryAlreadyExists:
            raise ValidationError(
                detail={"slug": "A library with that ID already exists."})
        # Grant the current user admin permissions on the library:
        api.set_library_user_permissions(result.key, request.user,
                                         api.AccessLevel.ADMIN_LEVEL)
        return Response(ContentLibraryMetadataSerializer(result).data)
Example #6
0
    403: 'Operation not permitted.',
    **HEDGEHOG_404_RESPONSE
}


@schema_for(
    'list',
    """
    Fetch the list of edX hedgehogs.

    Hedgehogs can be filtered by minimum weight (grams or ounces),
    their favorite food, whether they graduated college,
    or any combination of those criterion.
    """,
    parameters=[
        query_parameter('min-grams', int,
                        "Filter on whether minimum weight (grams)."),
        query_parameter('min-ounces', float,
                        "Filter hogs by minimum weight (ounces)."),
        query_parameter('fav-food', str, "Filter hogs by favorite food."),
        query_parameter('graduated', bool,
                        "Filter hogs by whether they graudated."),
    ],
    responses=HEDGEHOG_404_RESPONSE,
)
@schema_for(
    'retrieve',
    """
    Fetch details for a _single_ hedgehog by key.
    """,
    parameters=[HEDGEHOG_KEY_PARAMETER],
    responses=HEDGEHOG_404_RESPONSE,
Example #7
0
    """
    Fetch a list of tags.

    The list can be narrowed using the available filters.
    - Some filters are incompatible with each other,\
    namely `course_id`, `username` and `target_type`. The reason being that `course_id` and `username`\
    have an implicit `target_type` of `courseoverview` and `user`.
    - DateTime filters must have the following format `YY-MM-DD HH:MM:SS`. Time is optional, date is not.\
    Time must be UTC.
    - Parameters not defined bellow will be ignored. If you apply a filter with a typo you'll get the \
    whole list of tags.
    """,
    parameters=[
        query_parameter(
            "key",
            str,
            "The unique identifier. Same as `GET /eox-tagging/api/v1/tags/{key}`",
        ),
        query_parameter(
            "status", str, "Filter active or inactive tags. Default: active"
        ),
        query_parameter(
            "include_inactive", bool, "If true include the inactive tags on the list. Default false"
        ),
        query_parameter(
            "tag_type",
            str,
            "The type of the tag, set on the configuration of the site (i.e. Subscription level)",
        ),
        query_parameter("tag_value", str, "The value of the tag (i.e. Premium)"),
        query_parameter(
Example #8
0
class ProgramEnrollmentView(EnrollmentMixin, JobInvokerMixin, APIView):
    """
    A view for enrolling students in a program, or retrieving/modifying program enrollment data.

    Path: /api/[version]/programs/{program_key}/enrollments

    Accepts: [GET, POST, PATCH]

    ------------------------------------------------------------------------------------
    GET
    ------------------------------------------------------------------------------------

    See @schema decorator on `get` method.

    ------------------------------------------------------------------------------------
    POST / PATCH
    ------------------------------------------------------------------------------------

    Create or modify program enrollments. Checks user permissions and forwards request
    to the LMS program_enrollments endpoint.  Accepts up to 25 enrollments

    Returns:
     * 200: Returns a map of students and their enrollment status.
     * 207: Not all students enrolled. Returns resulting enrollment status.
     * 401: User is not authenticated
     * 403: User lacks enrollment write access to the specified program.
     * 404: Program does not exist.
     * 413: Payload too large, over 25 students supplied.
     * 422: Invalid request, unable to enroll students.
    """
    event_method_map = {
        'GET': 'registrar.{api_version}.get_program_enrollment',
        'POST': 'registrar.{api_version}.post_program_enrollment',
        'PATCH': 'registrar.{api_version}.patch_program_enrollment',
    }
    event_parameter_map = {
        'program_key': 'program_key',
        'fmt': 'result_format',
    }

    @schema(
        parameters=[
            path_parameter('program_key', str, 'edX human-readable program key'),
            query_parameter('fmt', str, 'Response format: "json" or "csv"'),
        ],
        responses={
            202: JobAcceptanceSerializer,
            403: "No program access.",
            404: "Invalid program key.",
            **SCHEMA_COMMON_RESPONSES,
        },
    )
    def get(self, request, *args, **kwargs):
        """
        Submit a user task that retrieves program enrollment data.
        """
        return self.invoke_download_job(list_program_enrollments, self.program.key)

    def post(self, request, program_key):
        """ POST handler """
        return self.handle_enrollments()

    def patch(self, request, program_key):
        """ PATCH handler """
        return self.handle_enrollments()
Example #9
0
class CourseGradesView(CourseSpecificViewMixin, JobInvokerMixin, APIView):
    # pylint: disable=line-too-long
    """
    Invokes a Django User Task that retrieves student grade data for the given course run.

    Returns:
     * 202: Accepted, an asynchronous job was successfully started.
     * 401: User is not authenticated
     * 403: User lacks enrollment read access to organization of specified program
     * 404: Program was not found, course was not found, or course was not found in program.

    Example Response:
    {
        "job_id": "3b985cec-dcf4-4d38-9498-8545ebcf5d0f",
        "job_url": "http://localhost/api/[version]/jobs/3b985cec-dcf4-4d38-9498-8545ebcf5d0f"
    }

    Path: /api/[version]/programs/{program_key}/courses/{course_id}/grades
    """
    permission_required = [perms.API_READ_ENROLLMENTS]
    event_method_map = {
        'GET': 'registrar.v1.get_course_grades',
    }
    event_parameter_map = {
        'program_key': 'program_key',
        'course_id': 'course_id',
        'fmt': 'result_format',
    }

    @schema(
        parameters=[
            path_parameter('course_id', str,
                           'edX course run ID or external course key'),
            path_parameter('program_key', str, 'edX program key'),
            query_parameter('fmt', str, 'Response format: "json" or "csv"'),
        ],
        responses={
            202: JobAcceptanceSerializer,
            403:
            'User lacks enrollment read access to organization of specified program.',
            404:
            'Program was not found, course was not found, or course was not found in program.',
            **SCHEMA_COMMON_RESPONSES,
        },
    )
    def get(self, request, *args, **kwargs):
        """
        Request course run grade data

        Begins an asynchronous job to fetch the grades of students enrolled in the specified course run as part of the specified program.
        The endpoint returns a URL which can be used to retrieve the status of and, when complete, the result of the job.

        The resulting file will contain a JSON list of dictionaries. Each dictionary will have the following fields:

        | Field          | Data Type | Description                                                                                            |
        |----------------|-----------|--------------------------------------------------------------------------------------------------------|
        | student_key    | string    | The student ID assigned to the student by the school.                                                  |
        | letter_grade   | string    | A letter grade as defined in grading policy.                                                           |
        | percent        | number    | A number from 0-1 representing the overall grade for the course.                                       |
        | passed         | boolean   | Whether the course has been passed according to the course grading policy                              |
        | error          | string    | If there was an issue loading this user's grade, an explanation of what went wrong                     |

        """
        return self.invoke_download_job(
            get_course_run_grades,
            self.program.key,
            self.internal_course_key,
        )
Example #10
0
class CourseEnrollmentView(CourseSpecificViewMixin, JobInvokerMixin,
                           EnrollmentMixin, APIView):
    # pylint: disable=line-too-long
    """
    A view for enrolling students in a program course run.

    Path: /api/[version]/programs/{program_key}/courses/{course_id}/enrollments

    Accepts: [GET, PATCH, POST]

    ------------------------------------------------------------------------------------
    GET
    ------------------------------------------------------------------------------------

    Invokes a Django User Task that retrieves student enrollment
    data for a given program course run.

    Returns:
     * 202: Accepted, an asynchronous job was successfully started.
     * 401: User is not authenticated
     * 403: User lacks enrollment read access to the program of specified course run.
     * 404: Course run does not exist within specified program.

    Example Response:
    {
        "job_id": "3b985cec-dcf4-4d38-9498-8545ebcf5d0f",
        "job_url": "http://localhost/api/[version]/jobs/3b985cec-dcf4-4d38-9498-8545ebcf5d0f"
    }

    ------------------------------------------------------------------------------------
    POST / PATCH
    ------------------------------------------------------------------------------------

    Create or modify program course enrollments. Checks user permissions and forwards request
    to the LMS program_enrollments endpoint.  Accepts up to 25 enrollments

    Returns:
     * 200: Returns a map of students and their course enrollment status.
     * 207: Not all students enrolled. Returns resulting enrollment status.
     * 401: User is not authenticated
     * 403: User lacks enrollment write access to the  specified program.
     * 404: Program does not exist.
     * 413: Payload too large, over 25 students supplied.
     * 422: Invalid request, unable to enroll students.
    """
    event_method_map = {
        'GET': 'registrar.{api_version}.get_course_enrollment',
        'POST': 'registrar.{api_version}.post_course_enrollment',
        'PATCH': 'registrar.{api_version}.patch_course_enrollment',
    }
    event_parameter_map = {
        'program_key': 'program_key',
        'course_id': 'course_id',
        'fmt': 'result_format',
    }

    @schema(
        parameters=[
            path_parameter('course_id', str,
                           'edX course run ID or external course key'),
            path_parameter('program_key', str, 'edX program key'),
            query_parameter('fmt', str, 'Response format: "json" or "csv"'),
        ],
        responses={
            202: JobAcceptanceSerializer,
            403:
            'Requester does not have permission to view the course run’s enrollments.',
            404: 'Program key is invalid, or course ID is not within program.',
            **SCHEMA_COMMON_RESPONSES,
        },
    )
    def get(self, request, *args, **kwargs):
        """
        Request course enrollment data

        Begins an asynchronous job to fetch the list of students in the specified course run as part of the specified program.
        The endpoint returns a URL which can be used to retrieve the status of and, when complete, the result of the job.

        The resulting file will contain a JSON list of dictionaries. Each dictionary will have the following fields:

        | Field          | Data Type | Description                                                                                            |
        |----------------|-----------|--------------------------------------------------------------------------------------------------------|
        | course_id      | string    | The external course key for the course run, or the internal edX course key if there is no external key |
        | student_key    | string    | The student ID assigned to the student by the school.                                                  |
        | status         | string    | The program enrollment status string of the student. See "Enrollment Statuses" for possible values.    |
        | account_exists | boolean   | Whether an edX account is associated with the given student key.                                       |

        """
        course_role_management_enabled = waffle.flag_is_active(
            request, 'enable_course_role_management')
        return self.invoke_download_job(
            list_course_run_enrollments,
            self.program.key,
            self.internal_course_key,
            self.external_course_key,
            course_role_management_enabled,
        )

    @schema(
        body=ProgramEnrollmentRequestSerializer(many=True),
        parameters=[
            path_parameter('course_id', str,
                           'edX course run ID or external course key'),
            path_parameter('program_key', str, 'edX program key'),
        ],
        responses={
            200: 'All students were successfully listed.',
            207:
            'Some students with successfully listed, while others were not. Details are included in JSON response data.',
            400:
            'The JSON list of dictionaries in the request was malformed or missing required fields.',
            403:
            'Requester does not have permission to enroll students in the program.',
            404: 'Program key is invalid, or course ID is not within program.',
            413: 'Over 25 students supplied.',
            422:
            'None of the students were successfully listed. Details are included in JSON response data.',
            **SCHEMA_COMMON_RESPONSES,
        },
    )
    # pylint: disable=unused-argument
    def post(self, request, program_key, course_id):
        """
        Enroll students in a course

        POST calls to this endpoint will enroll the specified students in the program course, up to 25 students at a time.

        The response will be a JSON dictionary mapping each supplied student key to either:
        - An enrollment status string (either "active" or "inactive").
        - An error status string. Possible values are listed below.

        | Error status        | Meaning                                                                                     |
        |---------------------|---------------------------------------------------------------------------------------------|
        | "duplicated"        | The same student key was supplied twice in the POST data.                                   |
        | "invalid-status"    | The supplied status string was invalid.                                                     |
        | "conflict"          | A student with the given key is already enrolled in the program.                            |
        | "illegal-operation" | The supplied status string was recognized, but the student's status could not be set to it. |
        | "not-in-program"    | No such student exists in the program to which the course belongs.                          |
        | "internal-error"    | An unspecified internal error has occurred. Contact edX Support.                            |
        """
        return self.handle_enrollments(self.internal_course_key)

    @schema(
        body=ProgramEnrollmentRequestSerializer(many=True),
        parameters=[
            path_parameter('course_id', str,
                           'edX course run ID or external course key'),
            path_parameter('program_key', str, 'edX program key'),
        ],
        responses={
            200: 'All students’ admission statuses were successfully set.',
            207:
            'Some students’ statuses were successfully set, others were not. Details are included in JSON response data.',
            400:
            'The JSON list of dictionaries in the request was malformed or missing required fields.',
            403:
            'Requester does not have permission to modify program admissions.',
            404: 'Program key is invalid, or course ID is not within program.',
            413: 'Over 25 students supplied.',
            422:
            'None of the students’ statuses were successfully set. Details are included in JSON response data.',
            **SCHEMA_COMMON_RESPONSES,
        },
    )
    # pylint: disable=unused-argument
    def patch(self, request, program_key, course_id):
        """
        Modify program course enrollments

        PATCH calls to this endpoint will modify the enrollment status of students within the specified course run within the specified program.

        The response will be a JSON dictionary mapping each supplied student key to either:
        - An enrollment status string (either "active" or "inactive").
        - An error status string. Possible values are listed below.

        | Error status        | Meaning                                                                                     |
        |---------------------|---------------------------------------------------------------------------------------------|
        | "duplicated"        | The same student key was supplied twice in the PATCH data.                                  |
        | "invalid-status"    | The supplied status string was invalid.                                                     |
        | "illegal-operation" | The supplied status string was recognized, but the student's status could not be set to it. |
        | "not-found"         | No such student exists in the course.                                                       |
        | "internal-error"    | An unspecified internal error has occurred. Contact edX Support.                            |
        """
        return self.handle_enrollments(self.internal_course_key)
Example #11
0
class ProgramEnrollmentView(EnrollmentMixin, JobInvokerMixin, APIView):
    # pylint: disable=line-too-long
    """
    A view for enrolling students in a program, or retrieving/modifying program enrollment data.

    Path: /api/[version]/programs/{program_key}/enrollments

    Accepts: [GET, POST, PATCH]
    """
    event_method_map = {
        'GET': 'registrar.{api_version}.get_program_enrollment',
        'POST': 'registrar.{api_version}.post_program_enrollment',
        'PATCH': 'registrar.{api_version}.patch_program_enrollment',
    }
    event_parameter_map = {
        'program_key': 'program_key',
        'fmt': 'result_format',
    }

    @schema(
        parameters=[
            path_parameter('program_key', str,
                           'edX human-readable program key'),
            query_parameter('fmt', str, 'Response format: "json" or "csv"'),
        ],
        responses={
            202: JobAcceptanceSerializer,
            403: "User does not have permission.",
            404: "Invalid program key.",
            **SCHEMA_COMMON_RESPONSES,
        },
    )
    def get(self, request, *args, **kwargs):
        """
        Request program enrollment data

        Begins an asyncronous job to fetch the list of students in the specified program.
        The endpoint returns a URL which can be used to retrieve the status of and, when complete, the result of the job.

        The resulting file will contain a JSON list of dictionaries. Each dictionary will have the following fields:

        | Field          | Data Type | Description                                                                                         |
        |----------------|-----------|-----------------------------------------------------------------------------------------------------|
        | student_key    | string    | The student ID assigned to the student by the school.                                               |
        | status         | string    | The program enrollment status string of the student. See "Enrollment Statuses" for possible values. |
        | account_exists | boolean   | Whether an edX account is associated with the given student key.                                    |
        """
        include_username_email = waffle.flag_is_active(
            request, 'include_name_email_in_get_program_enrollment')
        return self.invoke_download_job(list_program_enrollments,
                                        self.program.key,
                                        include_username_email)

    @schema(
        body=ProgramEnrollmentRequestSerializer(many=True),
        parameters=[
            path_parameter(
                'program_key', str,
                'The identifier of the program for which students will be enrolled.'
            ),
        ],
        responses={
            200: "All students were successfully listed.",
            207:
            "Some students were successfully listed, while others were not. Details are included in JSON response data.",
            400:
            "The JSON list of dictionaries in the request was malformed or missing required fields.",
            403:
            "User does not have permission to enroll students in the program.",
            404: "Program key is invalid.",
            413: "Over 25 students supplied.",
            422:
            "None of the students were successfully listed. Details are included in JSON response data.",
            **SCHEMA_COMMON_RESPONSES,
        },
    )
    # pylint: disable=unused-argument
    def post(self, request, program_key):
        """
        Enroll students in a program

        This endpoint will enroll the specified students in the program, up to 25 students at a time.

        There are four possible states in which a program enrollment can exist. Each state is denoted by a lowercase, one-word *status string*. The valid status strings and their meanings are shown below.

        | Status string | Meaning                                                                                                |
        |---------------|--------------------------------------------------------------------------------------------------------|
        | "enrolled"    | The student has completed the program's registration and payment process and is now actively enrolled. |
        | "pending"     | The student has, in some way, not yet completed the program's registration and payment process.        |
        | "suspended"   | The student has been temporarily removed from the program.                                             |
        | "canceled"    | The student has been permanently removed from the program.                                             |
        | "ended"       | The student has finished the program.                                                                  |

        Note: Currently, all non-enrolled statuses ( pending, canceled, suspended, ended ) are functionally identical. From an edX perspective, they will result in identical behavior.
        All non-enrolled statuses will block acccess to the Student Portal. The "enrolled" status is the only status that allows learners to view their program in the Student Portal.
        This may change in the future.

        The response will be a JSON dictionary mapping each supplied student key to either:
        - An enrollment status string.
        - An error status string. Possible values are listed below.

        | Error status        | Meaning                                                                                     |
        |---------------------|---------------------------------------------------------------------------------------------|
        | "duplicated"        | The same student key was supplied twice in the POST data.                                   |
        | "invalid-status"    | The supplied status string was invalid.                                                     |
        | "conflict"          | A student with the given key is already enrolled in the program.                            |
        | "internal-error"    | An unspecified internal error has occurred. Contact edX Support.                            |
        """
        return self.handle_enrollments()

    @schema(
        body=ProgramEnrollmentRequestSerializer(many=True),
        parameters=[
            path_parameter(
                'program_key', str,
                'The identifier of the program to modify enrollments for.'),
        ],
        responses={
            200: "All students' enrollment statuses were successfully set.",
            207:
            "Some students' enrollment statuses were successfully set, while others were not. Details are included in JSON response data.",
            400:
            "The JSON list of dictionaries in the request was malformed or missing required fields.",
            403:
            "User does not have permission to modify program enrollments.",
            404: "Program key is invalid.",
            413: "Over 25 students supplied.",
            422:
            "None of the students' enrollment statuses were successfully set. Details are included in JSON response data.",
            **SCHEMA_COMMON_RESPONSES,
        },
    )
    # pylint: disable=unused-argument
    def patch(self, request, program_key):
        """
        Modify program enrollments

        This endpoint will modify the program enrollment status of specified students.

        There are four possible states in which a program enrollment can exist. Each state is denoted by a lowercase, one-word *status string*. The valid status strings and their meanings are shown below.

        | Status string | Meaning                                                                                                |
        |---------------|--------------------------------------------------------------------------------------------------------|
        | "enrolled"    | The student has completed the program's registration and payment process and is now actively enrolled. |
        | "pending"     | The student has, in some way, not yet completed the program's registration and payment process.        |
        | "suspended"   | The student has been temporarily removed from the program.                                             |
        | "canceled"    | The student has been permanently removed from the program.                                             |
        | "ended"       | The student has finished the program.                                                                  |

        Note: Currently, all non-enrolled statuses ( pending, canceled, suspended, ended ) are functionally identical. From an edX perspective, they will result in identical behavior. This may change in the future.

        The response will be a JSON dictionary mapping each supplied student key to either:
        - An enrollment status string.
        - An error status string. Possible values are listed below.

        | Error status        | Meaning                                                                                     |
        |---------------------|---------------------------------------------------------------------------------------------|
        | "duplicated"        | The same student key was supplied twice in the PATCH data.                                  |
        | "invalid-status"    | The supplied status string was invalid.                                                     |
        | "illegal-operation" | The supplied status string was recognized, but the student's status could not be set to it. |
        | "not-found"         | No such student exists in the program.                                                      |
        | "internal-error"    | An unspecified internal error has occurred. Contact edX Support.                            |
        """
        return self.handle_enrollments()
Example #12
0
    JobInvokerMixin,
    ProgramSpecificViewMixin,
)

logger = logging.getLogger(__name__)

SCHEMA_COMMON_RESPONSES = {
    401: 'User is not authenticated.',
    405: 'HTTP method not support on this path.'
}


@schema_for(
    'get',
    parameters=[
        query_parameter('org_key', str, 'Organization filter'),
        query_parameter('user_has_perm', str, 'Permission filter'),
        query_parameter('program_title', str, 'Filter by program title')
    ],
    responses={
        403: 'User lacks access to organization.',
        404: 'Organization does not exist.',
        **SCHEMA_COMMON_RESPONSES,
    },
)
class ProgramListView(TrackViewMixin, ListAPIView):
    """
    List programs

    This endpoint returns a list of all of the active programs for the school specified by `org_key`. API users may only
    make requests with `org_key`s for schools they have API access to. Programs can be filtered by `program_title`.
Example #13
0
class EdxappEnrollment(UserQueryMixin, APIView):
    """
    Handles API requests to create users
    """

    authentication_classes = (BearerAuthentication, SessionAuthentication)
    permission_classes = (EoxCoreAPIPermission, )
    renderer_classes = (JSONRenderer, BrowsableAPIRenderer)

    @apidocs.schema(
        body=EdxappCourseEnrollmentQuerySerializer,
        responses={
            200:
            EdxappCourseEnrollmentQuerySerializer,
            202:
            "User doesn't belong to site.",
            400:
            "Bad request, invalid course_id or missing either email or username.",
        },
    )
    @audit_drf_api(
        action='Create single or bulk enrollments',
        data_filter=[
            'email',
            'username',
            'course_id',
            'mode',
            'is_active',
            'enrollment_attributes',
        ],
        method_name='eox_core_api_method',
    )
    def post(self, request, *args, **kwargs):
        """
        Handle creation of single or bulk enrollments

        **Example Requests**

            POST /eox-core/api/v1/enrollment/

            Request data: {
              "username": "******",
              "course_id": "course-v1:edX+DemoX+Demo_Course",
              "mode": "audit",
              "force": "False",
              "is_active": "False",
              "enrollment_attributes": [
                {
                  "namespace": "credit",
                  "name": "provider_id",
                  "value": "institution_name"
                }
              ]
            }

        **Parameters**

        - `username` (**required**, string, _body_):
            The username used to identify the user you want to enroll.  Use either username or email.

        - `email` (**required**, string, _body_):
            The email used to identify the user you to enroll.  Use either username or email.

        - `course_id` (**required**, string, _body_):
            The id of the course in which you want to enroll the user.

        - `mode` (**required**, string, _body_):
            The course mode for the enrollment.  Must be available for the course.

        - `is_active` (boolean, _body_):
            Flag indicating whether the enrollment is active.

        - `force` (boolean, _body_):
            Flag indicating whether the platform business rules for enrollment must be skipped. When it is true, the enrollment
            is created without looking at the enrollment dates, if the course is full, if the enrollment mode is part of the modes
            allowed by that course and other course settings.

        - `enrollment_attributes` (list, _body_):
            List of enrollment attributes. An enrollment attribute can be used to add extra parameters for a specific course mode.
            It must be a dictionary containing the following:
            - namespace: namespace of the attribute
            - name: name of the attribute
            - value: value of the attribute

        In case the case of bulk enrollments, you must provide a list of dictionaries containing
        the parameters specified above; the same restrictions apply.
        For example:

            [{
              "username": "******",
              "course_id": "course-v1:edX+DemoX+Demo_Course",
              "mode": "audit",
              "is_active": "False",
              "force": "False",
              "enrollment_attributes": [
                {
                  "namespace": "credit",
                  "name": "provider_id",
                  "value": "institution_name"
                }
              ]
             },
             {
              "email": "*****@*****.**",
              "course_id": "course-v1:edX+DemoX+Demo_Course",
              "mode": "audit",
              "is_active": "True",
              "force": "False",
              "enrollment_attributes": []
             },
            ]

        **Returns**

        - 200: Success, enrollment created.
        - 202: User doesn't belong to site.
        - 400: Bad request, invalid course_id or missing either email or username.
        """
        data = request.data
        return EdxappEnrollment.prepare_multiresponse(
            data, self.single_enrollment_create)

    @apidocs.schema(
        body=EdxappCourseEnrollmentQuerySerializer,
        responses={
            200:
            EdxappCourseEnrollmentQuerySerializer,
            202:
            "User or enrollment doesn't belong to site.",
            400:
            "Bad request, invalid course_id or missing either email or username.",
        },
    )
    @audit_drf_api(
        action='Update enrollments on edxapp',
        data_filter=[
            'email',
            'username',
            'course_id',
            'mode',
            'is_active',
            'enrollment_attributes',
        ],
        method_name='eox_core_api_method',
    )
    def put(self, request, *args, **kwargs):
        """
        Update enrollments on edxapp

        **Example Requests**

            PUT /eox-core/api/v1/enrollment/

            Request data: {
              "username": "******",
              "course_id": "course-v1:edX+DemoX+Demo_Course",
              "mode": "audit",
              "is_active": "False",
              "enrollment_attributes": [
                {
                  "namespace": "credit",
                  "name": "provider_id",
                  "value": "institution_name"
                }
              ]
            }

        **Parameters**

        - `username` (**required**, string, _body_):
            The username used to identify a user enrolled on the course. Use either username or email.

        - `email` (**required**, string, _body_):
            The email used to identify a user enrolled on the course. Use either username or email.

        - `course_id` (**required**, string, _body_):
            The course id for the enrollment you want to update.

        - `mode` (**required**, string, _body_):
            The course mode for the enrollment. Must be available for the course.

        - `is_active` (boolean, _body_):
            Flag indicating whether the enrollment is active.

        - `enrollment_attributes` (list, _body_):
            An enrollment attribute can be used to add extra parameters for a specific course mode.
            It must be a dictionary containing the following:
            - namespace: namespace of the attribute
            - name: name of the attribute
            - value: value of the attribute

        **Returns**

        - 200: Success, enrollment updated.
        - 202: User or enrollment doesn't belong to site.
        - 400: Bad request, invalid course_id or missing either email or username.
        """
        data = request.data
        return EdxappEnrollment.prepare_multiresponse(
            data, self.single_enrollment_update)

    @apidocs.schema(
        parameters=[
            apidocs.query_parameter(
                name="username",
                param_type=str,
                description=
                "**required**, The username used to identify a user enrolled on the course. Use either username or email.",
            ),
            apidocs.query_parameter(
                name="email",
                param_type=str,
                description=
                "**required**, The email used to identify a user enrolled on the course. Use either username or email.",
            ),
            apidocs.query_parameter(
                name="course_id",
                param_type=str,
                description=
                "**required**, The course id for the enrollment you want to check.",
            ),
        ],
        responses={
            200: EdxappCourseEnrollmentSerializer,
            400: "Bad request, missing course_id or either email or username",
            404: "User or course not found",
        },
    )
    def get(self, request, *args, **kwargs):
        """
        Retrieves enrollment information given a user and a course_id

        **Example Requests**

            GET /eox-core/api/v1/enrollment/?username=johndoe&
            course_id=course-v1:edX+DemoX+Demo_Course

            Request data: {
              "username": "******",
              "course_id": "course-v1:edX+DemoX+Demo_Course",
            }

        **Returns**

        - 200: Success, enrollment found.
        - 400: Bad request, missing course_id or either email or username
        - 404: User or course not found
        """
        user_query = self.get_user_query(request)
        user = get_edxapp_user(**user_query)

        course_id = self.query_params.get("course_id", None)

        if not course_id:
            raise ValidationError(detail="You have to provide a course_id")

        enrollment_query = {
            "username": user.username,
            "course_id": course_id,
        }
        enrollment, errors = get_enrollment(**enrollment_query)

        if errors:
            raise NotFound(detail=errors)
        response = EdxappCourseEnrollmentSerializer(enrollment).data
        return Response(response)

    @apidocs.schema(
        parameters=[
            apidocs.query_parameter(
                name="username",
                param_type=str,
                description=
                "**required**, The username used to identify a user enrolled on the course. Use either username or email.",
            ),
            apidocs.query_parameter(
                name="email",
                param_type=str,
                description=
                "**required**, The email used to identify a user enrolled on the course. Use either username or email.",
            ),
            apidocs.query_parameter(
                name="course_id",
                param_type=str,
                description=
                "**required**, The course id for the enrollment you want to check.",
            ),
        ],
        responses={
            204: "Empty response",
            400: "Bad request, missing course_id or either email or username",
            404: "User or course not found",
        },
    )
    @audit_drf_api(action='Delete enrollment on edxapp.',
                   method_name='eox_core_api_method')
    def delete(self, request, *args, **kwargs):
        """
        Delete enrollment on edxapp

        **Example Requests**

            DELETE /eox-core/api/v1/enrollment/

            Request data: {
              "username": "******",
              "course_id": "course-v1:edX+DemoX+Demo_Course",
            }
        """
        user_query = self.get_user_query(request)
        user = get_edxapp_user(**user_query)

        course_id = self.query_params.get("course_id", None)

        if not course_id:
            raise ValidationError(detail="You have to provide a course_id")

        delete_enrollment(user=user, course_id=course_id)
        return Response(status=status.HTTP_204_NO_CONTENT)

    def single_enrollment_create(self, *args, **kwargs):
        """
        Handle one create at the time
        """
        user_query = self.get_user_query(None, query_params=kwargs)
        user = get_edxapp_user(**user_query)

        enrollments, msgs = create_enrollment(user, **kwargs)
        # This logic block is needed to convert a single bundle_id enrollment in a list
        # of course_id enrollments which are appended to the response individually
        if not isinstance(enrollments, list):
            enrollments = [enrollments]
            msgs = [msgs]
        response_data = []
        for enrollment, msg in zip(enrollments, msgs):
            data = EdxappCourseEnrollmentSerializer(enrollment).data
            if msg:
                data["messages"] = msg
            response_data.append(data)

        return response_data

    def single_enrollment_update(self, *args, **kwargs):
        """
        Handle one update at the time
        """
        user_query = self.get_user_query(None, query_params=kwargs)
        user = get_edxapp_user(**user_query)

        course_id = kwargs.pop("course_id", None)
        if not course_id:
            raise ValidationError(
                detail="You have to provide a course_id for updates")
        mode = kwargs.pop("mode", None)

        return update_enrollment(user, course_id, mode, **kwargs)

    @staticmethod
    def prepare_multiresponse(request_data, action_method):
        """
        Prepare a multiple part response according to the request_data and the action_method provided
        Args:
            request_data: Data dictionary containing the query or queries to be processed
            action_method: Function to be applied to the queries (create, update)

        Returns: List of responses
        """
        multiple_responses = []
        errors_in_bulk_response = False
        many = isinstance(request_data, list)
        serializer = EdxappCourseEnrollmentQuerySerializer(data=request_data,
                                                           many=many)
        serializer.is_valid(raise_exception=True)
        data = serializer.validated_data
        if not isinstance(data, list):
            data = [data]

        for enrollment_query in data:

            try:
                result = action_method(**enrollment_query)
                # The result can be a list if the enrollment was in a bundle
                if isinstance(result, list):
                    multiple_responses += result
                else:
                    multiple_responses.append(result)
            except APIException as error:
                errors_in_bulk_response = True
                enrollment_query["error"] = {
                    "detail": error.detail,
                }
                multiple_responses.append(enrollment_query)

        if many or "bundle_id" in request_data:
            response = multiple_responses
        else:
            response = multiple_responses[0]

        response_status = status.HTTP_200_OK
        if errors_in_bulk_response:
            response_status = status.HTTP_202_ACCEPTED
        return Response(response, status=response_status)

    def handle_exception(self, exc):
        """
        Handle exception: log it
        """
        if isinstance(exc, APIException):
            LOG.error("API Error: %s", repr(exc.detail))

        return super(EdxappEnrollment, self).handle_exception(exc)
Example #14
0
class EdxappUser(UserQueryMixin, APIView):
    """
    Handles the creation of a User on edxapp

    **Example Requests**

        POST /eox-core/api/v1/user/

        Request data: {
            "username": "******",
            "email": "*****@*****.**",
            "fullname": "John Doe",
            "password": "******",
        }

    The extra registration fields configured for the microsite, should be sent along with the rest of the parameters.
    These extra fields would be required depending on the settings.
    For example, if we have the microsite settings:

        "EDNX_CUSTOM_REGISTRATION_FIELDS": [
            {
                "label": "Personal ID",
                "name": "personal_id",
                "type": "text"
            },
        ],

        "REGISTRATION_EXTRA_FIELDS": {
            "gender": "required",
            "country": "hidden",
            "personal_id": "required",
        },

        "extended_profile_fields": [
            "personal_id",
        ],

    Then a request to create a user should look like this:

        {
            "username": "******",
            "email": "*****@*****.**",
            "fullname": "John Doe",
            "password": "******",
            "gender": "m",
            "country": "GR",
            "personal_id": "12345",
        }

    """

    authentication_classes = (BearerAuthentication, SessionAuthentication)
    permission_classes = (EoxCoreAPIPermission, )
    renderer_classes = (JSONRenderer, BrowsableAPIRenderer)

    @apidocs.schema(
        body=EdxappUserQuerySerializer,
        responses={
            200: EdxappUserSerializer,
            400:
            "Bad request, a required field is missing or has been entered with the wrong format.",
            401: "Unauthorized user to make the request.",
        },
    )
    @audit_drf_api(
        action="Create edxapp user",
        data_filter=[
            "email",
            "username",
            "fullname",
        ],
        hidden_fields=["password"],
        save_all_parameters=True,
        method_name='eox_core_api_method',
    )
    def post(self, request, *args, **kwargs):
        """
        Handles the creation of a User on edxapp

        **Example Requests**

            POST /eox-core/api/v1/user/

            Request data: {
                "username": "******",
                "email": "*****@*****.**",
                "fullname": "John Doe",
                "password": "******",
            }

        **Parameters**

        - `username` (**required**, string, _body_):
            The username to be assigned to the new user.

        - `email` (**required**, string, _body_):
            The email to be assigned to the new user.

        - `password` (**required**, string, _body_):
            The password of the new user. If `skip_password` is True, this field will be omitted.

        - `fullname` (**required**, string, _body_):
            The full name to be assigned.

        - `activate_user` (**optional**, boolean, default=False, _body_):
            Flag indicating whether the user is active.

        - `skip_password` (**optional**, boolean, default=False, _body_):
            Flag indicating whether the password should be omitted.

        If you have extra registration fields configured in your settings or extended_profile fields, you can send them with the rest of the parameters.
        These extra fields would be required depending on the site settings.
        For example:

            {
                "username": "******",
                "email": "*****@*****.**",
                "fullname": "John Doe",
                "password": "******",
                "gender": "m",
                "country": "GR",
            }


        **Returns**

        - 200: Success, user created.
        - 400: Bad request, a required field is missing or has been entered with the wrong format, or the chosen email/username already belongs to a user.
        - 401: Unauthorized user to make the request.
        """
        serializer = EdxappUserQuerySerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        data = serializer.validated_data
        data["site"] = get_current_site(request)
        user, msg = create_edxapp_user(**data)

        serialized_user = EdxappUserSerializer(user)
        response_data = serialized_user.data
        if msg:
            response_data["messages"] = msg
        return Response(response_data)

    @apidocs.schema(
        parameters=[
            apidocs.query_parameter(
                name="username",
                param_type=str,
                description=
                "**required**, The username used to identify the user. Use either username or email.",
            ),
            apidocs.query_parameter(
                name="email",
                param_type=str,
                description=
                "**required**, The email used to identify the user. Use either username or email.",
            ),
        ],
        responses={
            200: get_user_read_only_serializer(),
            400: "Bad request, missing email or username",
            401: "Unauthorized user to make the request.",
            404: "User not found",
        },
    )
    def get(self, request, *args, **kwargs):
        """
        Retrieves information about an edxapp user,
        given an email or a username.

        The username prevails over the email when both are provided to get the user.

        **Example Requests**

            GET /eox-core/api/v1/user/?username=johndoe

            Query parameters: {
              "username": "******",
            }

        **Response details**

        - `username (str)`: Username of the edxapp user
        - `is_active (str)`: Indicates if the user is active on the platform
        - `email (str)`: Email of the user
        - `gender (str)`: Gender of the user
        - `date_joined (str)`: Date for when the user was registered in the platform
        - `name (str)`: Fullname of the user
        - `country (str)`: Country of the user
        - `level_of_education (str)`: Level of education of the user
        - `year_of_birth (int)`: Year of birth of the user
        - `bio (str)`: Bio of the user
        - `goals (str)`: Goals of the user
        - `extended_profile (list)`: List of dictionaries that contains the user-profile meta fields
            - `field_name (str)`: Name of the extended profile field
            - `field_value (str)`: Value of the extended profile field
        - `mailing_address (str)`
        - `social_links (List)`: List that contains the social links of the user, if any.
        - `account_privacy (str)`: Indicates the account privacy type
        - `state (str)`: State (only for US)
        - `secondary_email (str)`: Secondary email of the user
        - `profile_image (dictionary)`:
            - `has_image (Bool)`: Indicates if user has profile image
            - `image_url_medium (str)`: Url of the profile image in medium size
            - `image_url_small (str)`: Url of the profile image in small size
            - `image_url_full (str)`: Url of the profile image in full size,
            - `image_url_large (str)`: Url of the profile image in large size
        - `secondary_email_enabled (Bool)`: Indicates if the secondary email is enable
        - `phone_number (str)`: Phone number of the user
        - `requires_parental_consent (Bool)`: Indicates whether parental consent is required for the user

        **Returns**

        - 200: Success, user found.
        - 400: Bad request, missing either email or username
        - 401: Unauthorized user to make the request.
        - 404: User not found
        """
        query = self.get_user_query(request)
        user = get_edxapp_user(**query)
        admin_fields = getattr(settings, "ACCOUNT_VISIBILITY_CONFIGURATION",
                               {}).get("admin_fields", {})
        serialized_user = EdxappUserReadOnlySerializer(
            user, custom_fields=admin_fields, context={"request": request})
        response_data = serialized_user.data
        # Show a warning if the request is providing email and username
        # to let the client know we're giving priority to the username
        if "username" and "email" in self.query_params:
            response_data[
                "warning"] = "The username prevails over the email when both are provided to get the user."

        return Response(response_data)
Example #15
0
class EdxappGrade(UserQueryMixin, APIView):
    """
    Handles API requests to manage course grades
    """

    authentication_classes = (BearerAuthentication, SessionAuthentication)
    permission_classes = (EoxCoreAPIPermission, )
    renderer_classes = (JSONRenderer, BrowsableAPIRenderer)

    @apidocs.schema(
        parameters=[
            apidocs.query_parameter(
                name="username",
                param_type=str,
                description=
                "**required**, The username used to identify a user enrolled on the course. Use either username or email.",
            ),
            apidocs.query_parameter(
                name="email",
                param_type=str,
                description=
                "**required**, The email used to identify a user enrolled on the course. Use either username or email.",
            ),
            apidocs.query_parameter(
                name="course_id",
                param_type=str,
                description=
                "**required**, The course id for the enrollment you want to check.",
            ),
            apidocs.query_parameter(
                name="detailed",
                param_type=bool,
                description=
                "**optional**, If true include detailed data for each graded subsection",
            ),
            apidocs.query_parameter(
                name="grading_policy",
                param_type=bool,
                description=
                "**optional**, If true include course grading policy.",
            ),
        ],
        responses={
            200: EdxappGradeSerializer,
            400: "Bad request, missing course_id or either email or username",
            404: "User, course or enrollment not found",
        },
    )
    def get(self, request):
        """
        Retrieves Grades information for given a user and course_id

        **Example Requests**

            GET /eox-core/api/v1/grade/?username=johndoe&course_id=course-v1:edX+DemoX+Demo_Course

            Request data: {
              "username": "******",
              "course_id": "course-v1:edX+DemoX+Demo_Course",
            }

        **Response details**

        - `earned_grade`: Final user score for the course.
        - `section_breakdown` (**optional**): Details of each grade subsection.
            - `attempted`: Whether the learner attempted the assignment.
            - `assignment_type`: General category of the assignment.
            - `percent`: Grade obtained by the user on this subsection.
            - `score_earned`: The score a user earned on this subsection.
            - `score_possible`: Highest possible score a user can earn on this subsection.
            - `subsection_name`: Name of the subsection.
        - `grading_policy` (**optional**): Course grading policy.
            - `grade_cutoff`: Score needed to reach each grade.
            - `grader`: Details of each assignment type used by the Grader.
                - `assignment_type`: General category of the assignment.
                - `count`: Number of assignments of this type.
                - `dropped`: The number of assignments of this type that the grader will drop. The grader will drop the lowest-scored assignments first.
                - `weight`: Weight of this type of assignment towards the final grade.

        More information about grading can be found in the
        [edX documentation](https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/student_progress/course_grades.html).

        **Returns**

        - 200: Success.
        - 400: Bad request, missing course_id or either email or username.
        - 404: User, course or enrollment not found.
        """
        user_query = self.get_user_query(request)
        user = get_edxapp_user(**user_query)

        course_id = self.query_params.get("course_id", None)
        detailed = self.query_params.get("detailed", False)
        grading_policy = self.query_params.get("grading_policy", False)

        if not course_id:
            raise ValidationError(detail="You have to provide a course_id")

        _, errors = get_enrollment(username=user.username, course_id=course_id)

        if errors:
            raise NotFound(errors)

        grade_factory = get_course_grade_factory()
        course_key = get_valid_course_key(course_id)
        course = get_courseware_courses().get_course_by_id(course_key)
        course_grade = grade_factory().read(user, course)
        response = {"earned_grade": course_grade.percent}

        if detailed in ("True", "true", "on", "1"):
            breakdown = self._section_breakdown(course_grade.subsection_grades)
            response["section_breakdown"] = breakdown
        if grading_policy in ("True", "true", "on", "1"):
            response["grading_policy"] = course.grading_policy

        return Response(EdxappGradeSerializer(response).data)

    def _section_breakdown(self, subsection_grades):
        """
        Given a list of subsections grades determine if the subsection is
        graded and creates a list of grade data for each subsection.

        This logic comes from `edunext-platform/lms/djangoapps/grades/rest_api/v1/gradebook_views`
        with a few changes in the return value: `label` and `module_id` were
        removed and `category` was renamed to `assignment_type` to keep
        consistency with the names used in `grading_policy`.
        """
        breakdown = []
        for subsection in subsection_grades.values():
            if not subsection.graded:
                continue

            attempted = False
            score_earned = 0
            score_possible = 0

            if subsection.attempted_graded or subsection.override:
                attempted = True
                score_earned = subsection.graded_total.earned
                score_possible = subsection.graded_total.possible

            breakdown.append({
                "attempted": attempted,
                "assignment_type": subsection.format,
                "percent": subsection.percent_graded,
                "score_earned": score_earned,
                "score_possible": score_possible,
                "subsection_name": subsection.display_name,
            })
        return breakdown
Example #16
0
class ReportsListView(ProgramSpecificViewMixin, APIView):
    """
    A view for listing metadata about reports for a program.

    Path: /api/[version]/programs/{program_key}/reports?min_created_date={min_created_date}

    Example Response:
    [
        {
            "name":"individual_report__2019-12-11.txt",
            "created_date":"2019-12-11",
            "download_url":null
        },
        {
            "name":"aggregate_report__2019-12-12.txt",
            "created_date":"2019_12_12",
            "download_url":null
        },
        {
            "name":"individual_report__2019-12-12.txt",
            "created_date":"2019-12-12",
            "download_url":null
        }
    ]

    Returns:
     * 200: Returns a list of metadata about available reports for a program.
     * 401: User is not authenticated.
     * 403: User is not authorized to view program reports.
     * 404: Program does not exist.
    """
    event_method_map = {
        'GET': 'registrar.v1.list_program_reports',
    }
    event_parameter_map = {
        'program_key': 'program_key',
        'min_created_date': 'min_created_date',
    }
    permission_required = [perms.API_READ_REPORTS]

    @schema(
        parameters=[
            path_parameter('program_key', str, 'edX program key'),
            query_parameter(
                'min_created_date', str,
                'ISO-formatted date used to filter reports based on creation date'
            )
        ],
        responses={
            200: ProgramReportMetadataSerializer(many=True),
            403: 'User lacks access to program.',
            404: 'Program does not exist.',
            **SCHEMA_COMMON_RESPONSES,
        },
        summary='List program reports',
        description=
        ('This endpoint returns a list of all reports specified by the program_key. '
         'If a min_created_date is given, only reports created after that date will be returned.'
         ))
    def get(self, request, *args, **kwargs):
        """
        Get a list of reports for a program.
        """
        filestore = get_program_reports_filestore()
        file_prefix = f'{self.program.managing_organization.key}/{self.program.discovery_uuid.hex}'
        date_format_string = '%Y-%m-%d'

        reports_metadata = []

        # list method of the filestore returns a 2-tuple containing a
        # list of directories and a list of files on the path, respectively,
        # so iterate the list of files
        reports = filestore.list(file_prefix)[1]

        for report_name in reports:
            created_date = self._get_file_created_date(report_name,
                                                       date_format_string)
            if not created_date:
                # If the file name is not in the expected format,
                # log a warning and skip the file.
                logger.warning(
                    "Under path '%s', filename '%s' is not in the expected format: "
                    "report_name__YYYY-MM-DD.extension.",
                    file_prefix,
                    report_name,
                )
                continue
            report_metadata = {
                'name': report_name,
                'created_date': created_date
            }
            reports_metadata.append(report_metadata)

        if 'min_created_date' in request.query_params:
            min_created_date = datetime.strptime(
                request.query_params['min_created_date'], date_format_string)

            reports_metadata = [
                r for r in reports_metadata
                if r['created_date'] is not None and datetime.strptime(
                    r['created_date'], date_format_string) >= min_created_date
            ]

        # wait to generate the download url until after we've done any filtering
        # to avoid generating unnecessary urls
        for report in reports_metadata:
            report['download_url'] = filestore.get_url('{}/{}'.format(
                file_prefix, report['name']))

        serializer = ProgramReportMetadataSerializer(reports_metadata,
                                                     many=True)
        return Response(serializer.data)

    def _get_file_created_date(self, filename, date_format_string):
        """
        Return the date the file was created based on the date in the filename.

        Parameters:
            - filename: the name of the file
            - date_format_string: a string representing the expected format of dates

        Returns:
            - String: the date the file was created as a formatted string that matches date_format_string
            - None: if the date is not in the filename or the date is misformatted
        """
        # pull out the date string from the filename
        pattern = re.compile(r'.*__(\d*-\d*-\d*)[.]*\w*')
        match = pattern.match(filename)

        if match is None:
            # if the filename is not as expected, return None
            return None

        date_string = match.group(1)

        try:
            # validate that the date is actually a date; otherwise,
            # return None
            date = datetime.strptime(date_string, date_format_string)
        except ValueError:
            return None
        return datetime.strftime(date, date_format_string)
Example #17
0
class LibraryRootView(APIView):
    """
    Views to list, search for, and create content libraries.
    """
    @apidocs.schema(
        parameters=[
            *LibraryApiPagination.apidoc_params,
            apidocs.query_parameter(
                'org',
                str,
                description=
                "The organization short-name used to filter libraries",
            ),
            apidocs.query_parameter(
                'text_search',
                str,
                description=
                "The string used to filter libraries by searching in title, id, org, or description",
            ),
        ], )
    def get(self, request):
        """
        Return a list of all content libraries that the user has permission to view.
        """
        serializer = ContentLibraryFilterSerializer(data=request.query_params)
        serializer.is_valid(raise_exception=True)
        org = serializer.validated_data['org']
        library_type = serializer.validated_data['type']
        text_search = serializer.validated_data['text_search']

        paginator = LibraryApiPagination()
        queryset = api.get_libraries_for_user(request.user,
                                              org=org,
                                              library_type=library_type)
        if text_search:
            result = api.get_metadata_from_index(queryset,
                                                 text_search=text_search)
            result = paginator.paginate_queryset(result, request)
        else:
            # We can paginate queryset early and prevent fetching unneeded metadata
            paginated_qs = paginator.paginate_queryset(queryset, request)
            result = api.get_metadata_from_index(paginated_qs)

        serializer = ContentLibraryMetadataSerializer(result, many=True)
        # Verify `pagination` param to maintain compatibility with older
        # non pagination-aware clients
        if request.GET.get('pagination', 'false').lower() == 'true':
            return paginator.get_paginated_response(serializer.data)
        return Response(serializer.data)

    def post(self, request):
        """
        Create a new content library.
        """
        if not request.user.has_perm(permissions.CAN_CREATE_CONTENT_LIBRARY):
            raise PermissionDenied
        serializer = ContentLibraryMetadataSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        data = dict(serializer.validated_data)
        # Converting this over because using the reserved names 'type' and 'license' would shadow the built-in
        # definitions elsewhere.
        data['library_type'] = data.pop('type')
        data['library_license'] = data.pop('license')
        # Get the organization short_name out of the "key.org" pseudo-field that the serializer added:
        org_name = data["key"]["org"]
        # Move "slug" out of the "key.slug" pseudo-field that the serializer added:
        data["slug"] = data.pop("key")["slug"]
        try:
            org = Organization.objects.get(short_name=org_name)
        except Organization.DoesNotExist:
            raise ValidationError(
                detail={
                    "org": "No such organization '{}' found.".format(org_name)
                })
        try:
            result = api.create_library(org=org, **data)
        except api.LibraryAlreadyExists:
            raise ValidationError(
                detail={"slug": "A library with that ID already exists."})
        # Grant the current user admin permissions on the library:
        api.set_library_user_permissions(result.key, request.user,
                                         api.AccessLevel.ADMIN_LEVEL)
        return Response(ContentLibraryMetadataSerializer(result).data)
Example #18
0
    ProgramSpecificViewMixin,
)


logger = logging.getLogger(__name__)

SCHEMA_COMMON_RESPONSES = {
    401: 'User is not authenticated.',
    405: 'HTTP method not support on this path.'
}


@schema_for(
    'get',
    parameters=[
        query_parameter('org_key', str, 'Organization filter'),
        query_parameter('user_has_perm', str, 'Permission filter'),
    ],
    responses={
        403: 'User lacks access to organization.',
        404: 'Organization does not exist.',
        **SCHEMA_COMMON_RESPONSES,
    },
)
class ProgramListView(AuthMixin, TrackViewMixin, ListAPIView):
    """
    A view for listing program objects.

    Path: /api/[version]/programs?org={org_key}
    """