Example #1
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 #2
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 #3
0
from rest_framework.viewsets import ModelViewSet, ViewSet

from edx_api_doc_tools import (
    exclude_schema,
    exclude_schema_for,
    exclude_schema_for_all,
    path_parameter,
    query_parameter,
    schema,
    schema_for,
)

from .data import get_hedgehogs
from .serializers import HedgehogSerializer

HEDGEHOG_KEY_PARAMETER = path_parameter(
    'hedgehog_key', str, "Key identifying the hog. Lowercase letters only.")
HEDGEHOG_404_RESPONSE = {
    404: 'Hedgehog with given key not found.',
}
HEDGEHOG_ERROR_RESPONSES = {
    401: 'Not authenticated',
    403: 'Operation not permitted.',
    **HEDGEHOG_404_RESPONSE
}


@schema_for(
    'list',
    """
    Fetch the list of edX hedgehogs.
Example #4
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 #5
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 #6
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 #7
0
        Raises 404 for bad permission query param.
        """
        perm_query_param = self.request.GET.get('user_has_perm', None)
        if not perm_query_param:
            return perms.API_READ_METADATA
        try:
            return perms.API_PERMISSIONS_BY_NAME[perm_query_param]
        except KeyError as ex:
            self.add_tracking_data(failure='no_such_perm')
            raise Http404() from ex


@schema_for(
    'get',
    parameters=[
        path_parameter('program_key', str, 'Program filter'),
    ],
    responses={
        200: DetailedProgramSerializer,
        403: 'User lacks access to program.',
        404: 'Program does not exist.',
        **SCHEMA_COMMON_RESPONSES,
    },
)
class ProgramRetrieveView(ProgramSpecificViewMixin, RetrieveAPIView):
    """
    Retrieve a program

    This endpoint returns a single program specified by the program_key.

    Path: /api/[version]/programs/{program_key}
Example #8
0
class CourseAppsView(DeveloperErrorViewMixin, views.APIView):
    """
    A view for getting a list of all apps available for a course.
    """

    authentication_classes = (
        JwtAuthentication,
        BearerAuthenticationAllowInactiveUser,
        SessionAuthenticationAllowInactiveUser,
    )
    permission_classes = (HasStudioWriteAccess, )

    @schema(
        parameters=[
            path_parameter("course_id", str, description="Course Key"),
        ],
        responses={
            200: CourseAppSerializer,
            401: "The requester is not authenticated.",
            403:
            "The requester does not have staff access access to the specified course",
            404: "The requested course does not exist.",
        },
    )
    @verify_course_exists("Requested apps for unknown course {course}")
    def get(self, request: Request, course_id: str):
        """
        Get a list of all the course apps available for a course.

        **Example Response**

            GET /api/course_apps/v1/apps/{course_id}

        ```json
        [
            {
                "allowed_operations": {
                    "configure": false,
                    "enable": true
                },
                "description": "Provide an in-browser calculator that supports simple and complex calculations.",
                "enabled": false,
                "id": "calculator",
                "name": "Calculator"
            },
            {
                "allowed_operations": {
                    "configure": true,
                    "enable": true
                },
                "description": "Encourage participation and engagement in your course with discussion forums.",
                "enabled": false,
                "id": "discussion",
                "name": "Discussion"
            },
            ...
        ]
        ```
        """
        course_key = CourseKey.from_string(course_id)
        course_apps = CourseAppsPluginManager.get_apps_available_for_course(
            course_key)
        serializer = CourseAppSerializer(course_apps,
                                         many=True,
                                         context={
                                             "course_key": course_key,
                                             "user": request.user
                                         })
        return Response(serializer.data)

    @schema(
        parameters=[
            path_parameter("course_id", str, description="Course Key"),
        ],
        responses={
            200: CourseAppSerializer,
            401: "The requester is not authenticated.",
            403:
            "The requester does not have staff access access to the specified course",
            404: "The requested course does not exist.",
        },
    )
    @verify_course_exists("Requested apps for unknown course {course}")
    def patch(self, request: Request, course_id: str):
        """
        Enable/disable a course app.

        **Example Response**

            PATCH /api/course_apps/v1/apps/{course_id} {
                "id": "wiki",
                "enabled": true
            }

        ```json
        {
            "allowed_operations": {
                "configure": false,
                "enable": false
            },
            "description": "Enable learners to access, and collaborate on information about your course.",
            "enabled": true,
            "id": "wiki",
            "name": "Wiki"
        }
        ```
        """
        course_key = CourseKey.from_string(course_id)
        app_id = request.data.get("id")
        enabled = request.data.get("enabled")
        if app_id is None:
            raise ValidationError({"id": "App id is missing"})
        if enabled is None:
            raise ValidationError(
                {"enabled": "Must provide value for `enabled` field."})
        try:
            course_app = CourseAppsPluginManager.get_plugin(app_id)
        except PluginError:
            course_app = None
        if not course_app or not course_app.is_available(course_key):
            raise ValidationError({"id": "Invalid app ID"})
        set_course_app_enabled(course_key=course_key,
                               app_id=app_id,
                               enabled=enabled,
                               user=request.user)
        serializer = CourseAppSerializer(course_app,
                                         context={
                                             "course_key": course_key,
                                             "user": request.user
                                         })
        return Response(serializer.data)
Example #9
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 #10
0
class CourseLiveConfigurationView(APIView):
    """
    View for configuring CourseLive settings.
    """
    authentication_classes = (JwtAuthentication,
                              BearerAuthenticationAllowInactiveUser,
                              SessionAuthenticationAllowInactiveUser)
    permission_classes = (IsStaffOrInstructor, )

    @apidocs.schema(
        parameters=[
            apidocs.path_parameter(
                'course_id',
                str,
                description="The course for which to get provider list",
            )
        ],
        responses={
            200: CourseLiveConfigurationSerializer,
            401: "The requester is not authenticated.",
            403: "The requester cannot access the specified course.",
            404: "The requested course does not exist.",
        },
    )
    @ensure_valid_course_key
    @verify_course_exists()
    def get(self, request: Request, course_id: str) -> Response:
        """
        Handle HTTP/GET requests
        """
        configuration = CourseLiveConfiguration.get(
            course_id) or CourseLiveConfiguration()
        serializer = CourseLiveConfigurationSerializer(
            configuration,
            context={
                "pii_sharing_allowed":
                get_lti_pii_sharing_state_for_course(course_id),
                "course_id":
                course_id
            })

        return Response(serializer.data)

    @apidocs.schema(
        parameters=[
            apidocs.path_parameter(
                'course_id',
                str,
                description="The course for which to get provider list",
            ),
            apidocs.path_parameter(
                'lti_1p1_client_key',
                str,
                description="The LTI provider's client key",
            ),
            apidocs.path_parameter(
                'lti_1p1_client_secret',
                str,
                description="The LTI provider's client secretL",
            ),
            apidocs.path_parameter(
                'lti_1p1_launch_url',
                str,
                description="The LTI provider's launch URL",
            ),
            apidocs.path_parameter(
                'provider_type',
                str,
                description="The LTI provider's launch URL",
            ),
            apidocs.parameter(
                'lti_config',
                apidocs.ParameterLocation.QUERY,
                object,
                description=
                "The lti_config object with required additional parameters ",
            ),
        ],
        responses={
            200: CourseLiveConfigurationSerializer,
            400: "Required parameters are missing.",
            401: "The requester is not authenticated.",
            403: "The requester cannot access the specified course.",
            404: "The requested course does not exist.",
        },
    )
    @ensure_valid_course_key
    @verify_course_exists()
    def post(self, request, course_id: str) -> Response:
        """
        Handle HTTP/POST requests
        """
        pii_sharing_allowed = get_lti_pii_sharing_state_for_course(course_id)
        if not pii_sharing_allowed and provider_requires_pii_sharing(
                request.data.get('provider_type', '')):
            return Response({
                "pii_sharing_allowed":
                pii_sharing_allowed,
                "message":
                "PII sharing is not allowed on this course"
            })

        configuration = CourseLiveConfiguration.get(course_id)
        serializer = CourseLiveConfigurationSerializer(
            configuration,
            data=request.data,
            context={
                "pii_sharing_allowed": pii_sharing_allowed,
                "course_id": course_id
            })
        if not serializer.is_valid():
            raise ValidationError(serializer.errors)
        serializer.save()
        return Response(serializer.data)