chore: pre-fix linting (#36368)

* chore: pre-fix linting

Before making some bug fixes in this area, reformatting  these files to modern standards.
This commit is contained in:
Deborah Kaplan
2025-03-12 12:44:14 -04:00
committed by GitHub
parent d2561af9bd
commit 97675ae404
3 changed files with 446 additions and 456 deletions

View File

@@ -2,14 +2,12 @@
Serializers for all Course Enrollment related return objects.
"""
import logging
from rest_framework import serializers
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import (CourseEnrollment,
CourseEnrollmentAllowed)
from common.djangoapps.student.models import CourseEnrollment, CourseEnrollmentAllowed
log = logging.getLogger(__name__)
@@ -21,6 +19,7 @@ class StringListField(serializers.CharField):
[1,2,3]
"""
def field_to_native(self, obj, field_name): # pylint: disable=unused-argument
"""
Serialize the object's class name.
@@ -28,7 +27,7 @@ class StringListField(serializers.CharField):
if not obj.suggested_prices:
return []
items = obj.suggested_prices.split(',')
items = obj.suggested_prices.split(",")
return [int(item) for item in items]
@@ -49,7 +48,7 @@ class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-meth
class Meta:
# For disambiguating within the drf-yasg swagger schema
ref_name = 'enrollment.Course'
ref_name = "enrollment.Course"
def __init__(self, *args, **kwargs):
self.include_expired = kwargs.pop("include_expired", False)
@@ -59,15 +58,8 @@ class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-meth
"""
Retrieve course modes associated with the course.
"""
course_modes = CourseMode.modes_for_course(
obj.id,
include_expired=self.include_expired,
only_selectable=False
)
return [
ModeSerializer(mode).data
for mode in course_modes
]
course_modes = CourseMode.modes_for_course(obj.id, include_expired=self.include_expired, only_selectable=False)
return [ModeSerializer(mode).data for mode in course_modes]
def get_pacing_type(self, obj):
"""
@@ -83,8 +75,9 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
the Course block and course modes, to give a complete representation of course enrollment.
"""
course_details = CourseSerializer(source="course_overview")
user = serializers.SerializerMethodField('get_username')
user = serializers.SerializerMethodField("get_username")
def get_username(self, model):
"""Retrieves the username from the associated model."""
@@ -92,8 +85,8 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
class Meta:
model = CourseEnrollment
fields = ('created', 'mode', 'is_active', 'course_details', 'user')
lookup_field = 'username'
fields = ("created", "mode", "is_active", "course_details", "user")
lookup_field = "username"
class CourseEnrollmentsApiListSerializer(CourseEnrollmentSerializer):
@@ -101,14 +94,15 @@ class CourseEnrollmentsApiListSerializer(CourseEnrollmentSerializer):
Serializes CourseEnrollment model and returns a subset of fields returned
by the CourseEnrollmentSerializer.
"""
course_id = serializers.CharField(source='course_overview.id')
course_id = serializers.CharField(source="course_overview.id")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields.pop('course_details')
self.fields.pop("course_details")
class Meta(CourseEnrollmentSerializer.Meta):
fields = CourseEnrollmentSerializer.Meta.fields + ('course_id', )
fields = CourseEnrollmentSerializer.Meta.fields + ("course_id",)
class ModeSerializer(serializers.Serializer): # pylint: disable=abstract-method
@@ -119,6 +113,7 @@ class ModeSerializer(serializers.Serializer): # pylint: disable=abstract-method
does not handle the model object itself, but the tuple.
"""
slug = serializers.CharField(max_length=100)
name = serializers.CharField(max_length=255)
min_price = serializers.IntegerField()
@@ -137,7 +132,8 @@ class CourseEnrollmentAllowedSerializer(serializers.ModelSerializer):
Aggregates all data from the CourseEnrollmentAllowed table, and pulls in the serialization
to give a complete representation of course enrollment allowed.
"""
class Meta:
model = CourseEnrollmentAllowed
exclude = ['id']
lookup_field = 'user'
exclude = ["id"]
lookup_field = "user"

View File

@@ -3,7 +3,6 @@ URLs for the Enrollment API
"""
from django.conf import settings
from django.urls import path, re_path
@@ -14,21 +13,24 @@ from .views import (
EnrollmentListView,
EnrollmentUserRolesView,
EnrollmentView,
UnenrollmentView
UnenrollmentView,
)
urlpatterns = [
re_path(r'^enrollment/{username},{course_key}$'.format(
username=settings.USERNAME_PATTERN,
course_key=settings.COURSE_ID_PATTERN),
EnrollmentView.as_view(), name='courseenrollment'),
re_path(fr'^enrollment/{settings.COURSE_ID_PATTERN}$',
EnrollmentView.as_view(), name='courseenrollment'),
path('enrollment', EnrollmentListView.as_view(), name='courseenrollments'),
re_path(r'^enrollments/?$', CourseEnrollmentsApiListView.as_view(), name='courseenrollmentsapilist'),
re_path(fr'^course/{settings.COURSE_ID_PATTERN}$',
EnrollmentCourseDetailView.as_view(), name='courseenrollmentdetails'),
path('unenroll/', UnenrollmentView.as_view(), name='unenrollment'),
path('roles/', EnrollmentUserRolesView.as_view(), name='roles'),
path('enrollment_allowed/', EnrollmentAllowedView.as_view(), name='courseenrollmentallowed'),
re_path(
r"^enrollment/{username},{course_key}$".format(
username=settings.USERNAME_PATTERN, course_key=settings.COURSE_ID_PATTERN
),
EnrollmentView.as_view(),
name="courseenrollment",
),
re_path(rf"^enrollment/{settings.COURSE_ID_PATTERN}$", EnrollmentView.as_view(), name="courseenrollment"),
path("enrollment", EnrollmentListView.as_view(), name="courseenrollments"),
re_path(r"^enrollments/?$", CourseEnrollmentsApiListView.as_view(), name="courseenrollmentsapilist"),
re_path(
rf"^course/{settings.COURSE_ID_PATTERN}$", EnrollmentCourseDetailView.as_view(), name="courseenrollmentdetails"
),
path("unenroll/", UnenrollmentView.as_view(), name="unenrollment"),
path("roles/", EnrollmentUserRolesView.as_view(), name="roles"),
path("enrollment_allowed/", EnrollmentAllowedView.as_view(), name="courseenrollmentallowed"),
]

View File

@@ -4,19 +4,20 @@ consist primarily of authentication, request validation, and serialization.
"""
import logging
from django.core.exceptions import ( # lint-amnesty, pylint: disable=wrong-import-order
ObjectDoesNotExist,
ValidationError
ValidationError,
)
from django.db import IntegrityError # lint-amnesty, pylint: disable=wrong-import-order
from django.db import IntegrityError # lint-amnesty, pylint: disable=wrong-import-order
from django.utils.decorators import method_decorator # lint-amnesty, pylint: disable=wrong-import-order
from edx_rest_framework_extensions.auth.jwt.authentication import \
JwtAuthentication # lint-amnesty, pylint: disable=wrong-import-order
from edx_rest_framework_extensions.auth.session.authentication import \
SessionAuthenticationAllowInactiveUser # lint-amnesty, pylint: disable=wrong-import-order
from edx_rest_framework_extensions.auth.jwt.authentication import (
JwtAuthentication,
) # lint-amnesty, pylint: disable=wrong-import-order
from edx_rest_framework_extensions.auth.session.authentication import (
SessionAuthenticationAllowInactiveUser,
) # lint-amnesty, pylint: disable=wrong-import-order
from opaque_keys import InvalidKeyError # lint-amnesty, pylint: disable=wrong-import-order
from opaque_keys.edx.keys import CourseKey # lint-amnesty, pylint: disable=wrong-import-order
from rest_framework import permissions, status # lint-amnesty, pylint: disable=wrong-import-order
@@ -38,13 +39,13 @@ from openedx.core.djangoapps.enrollments import api
from openedx.core.djangoapps.enrollments.errors import (
CourseEnrollmentError,
CourseEnrollmentExistsError,
CourseModeNotFoundError
CourseModeNotFoundError,
)
from openedx.core.djangoapps.enrollments.forms import CourseEnrollmentsApiListForm
from openedx.core.djangoapps.enrollments.paginators import CourseEnrollmentsApiListPagination
from openedx.core.djangoapps.enrollments.serializers import (
CourseEnrollmentAllowedSerializer,
CourseEnrollmentsApiListSerializer
CourseEnrollmentsApiListSerializer,
)
from openedx.core.djangoapps.user_api.accounts.permissions import CanRetireUser
from openedx.core.djangoapps.user_api.models import UserRetirementStatus
@@ -58,7 +59,7 @@ from openedx.features.enterprise_support.api import (
ConsentApiServiceClient,
EnterpriseApiException,
EnterpriseApiServiceClient,
enterprise_enabled
enterprise_enabled,
)
log = logging.getLogger(__name__)
@@ -68,7 +69,8 @@ REQUIRED_ATTRIBUTES = {
class EnrollmentCrossDomainSessionAuth(SessionAuthenticationAllowInactiveUser, SessionAuthenticationCrossDomainCsrf):
"""Session authentication that allows inactive users and cross-domain requests. """
"""Session authentication that allows inactive users and cross-domain requests."""
pass # lint-amnesty, pylint: disable=unnecessary-pass
@@ -97,15 +99,15 @@ class EnrollmentUserThrottle(UserRateThrottle, ApiKeyPermissionMixIn):
# To see how the staff rate limit was selected, see https://github.com/openedx/edx-platform/pull/18360
THROTTLE_RATES = {
'user': '40/minute',
'staff': '120/minute',
"user": "40/minute",
"staff": "120/minute",
}
def allow_request(self, request, view):
# Use a special scope for staff to allow for a separate throttle rate
user = request.user
if user.is_authenticated and (user.is_staff or user.is_superuser):
self.scope = 'staff'
self.scope = "staff"
self.rate = self.get_rate()
self.num_requests, self.duration = self.parse_rate(self.rate)
@@ -115,66 +117,66 @@ class EnrollmentUserThrottle(UserRateThrottle, ApiKeyPermissionMixIn):
@can_disable_rate_limit
class EnrollmentView(APIView, ApiKeyPermissionMixIn):
"""
**Use Case**
**Use Case**
Get the user's enrollment status for a course.
Get the user's enrollment status for a course.
**Example Request**
**Example Request**
GET /api/enrollment/v1/enrollment/{username},{course_id}
GET /api/enrollment/v1/enrollment/{username},{course_id}
**Response Values**
**Response Values**
If the request for information about the user is successful, an HTTP 200 "OK" response
is returned.
If the request for information about the user is successful, an HTTP 200 "OK" response
is returned.
The HTTP 200 response has the following values.
The HTTP 200 response has the following values.
* course_details: A collection that includes the following
* course_details: A collection that includes the following
values.
* course_end: The date and time when the course closes. If
null, the course never ends.
* course_id: The unique identifier for the course.
* course_name: The name of the course.
* course_modes: An array of data about the enrollment modes
supported for the course. If the request uses the parameter
include_expired=1, the array also includes expired
enrollment modes.
Each enrollment mode collection includes the following
values.
* course_end: The date and time when the course closes. If
null, the course never ends.
* course_id: The unique identifier for the course.
* course_name: The name of the course.
* course_modes: An array of data about the enrollment modes
supported for the course. If the request uses the parameter
include_expired=1, the array also includes expired
enrollment modes.
* currency: The currency of the listed prices.
* description: A description of this mode.
* expiration_datetime: The date and time after which
users cannot enroll in the course in this mode.
* min_price: The minimum price for which a user can
enroll in this mode.
* name: The full name of the enrollment mode.
* slug: The short name for the enrollment mode.
* suggested_prices: A list of suggested prices for
this enrollment mode.
Each enrollment mode collection includes the following
values.
* course_end: The date and time at which the course closes. If
null, the course never ends.
* course_start: The date and time when the course opens. If
null, the course opens immediately when it is created.
* enrollment_end: The date and time after which users cannot
enroll for the course. If null, the enrollment period never
ends.
* enrollment_start: The date and time when users can begin
enrolling in the course. If null, enrollment opens
immediately when the course is created.
* invite_only: A value indicating whether students must be
invited to enroll in the course. Possible values are true or
false.
* currency: The currency of the listed prices.
* description: A description of this mode.
* expiration_datetime: The date and time after which
users cannot enroll in the course in this mode.
* min_price: The minimum price for which a user can
enroll in this mode.
* name: The full name of the enrollment mode.
* slug: The short name for the enrollment mode.
* suggested_prices: A list of suggested prices for
this enrollment mode.
* course_end: The date and time at which the course closes. If
null, the course never ends.
* course_start: The date and time when the course opens. If
null, the course opens immediately when it is created.
* enrollment_end: The date and time after which users cannot
enroll for the course. If null, the enrollment period never
ends.
* enrollment_start: The date and time when users can begin
enrolling in the course. If null, enrollment opens
immediately when the course is created.
* invite_only: A value indicating whether students must be
invited to enroll in the course. Possible values are true or
false.
* created: The date the user account was created.
* is_active: Whether the enrollment is currently active.
* mode: The enrollment mode of the user in this course.
* user: The ID of the user.
"""
* created: The date the user account was created.
* is_active: Whether the enrollment is currently active.
* mode: The enrollment mode of the user in this course.
* user: The ID of the user.
"""
authentication_classes = (
JwtAuthentication,
@@ -207,8 +209,11 @@ class EnrollmentView(APIView, ApiKeyPermissionMixIn):
username = username or request.user.username
# TODO Implement proper permissions
if request.user.username != username and not self.has_api_key_permissions(request) \
and not request.user.is_staff:
if (
request.user.username != username
and not self.has_api_key_permissions(request)
and not request.user.is_staff
):
# Return a 404 instead of a 403 (Unauthorized). If one user is looking up
# other users, do not let them deduce the existence of an enrollment.
return Response(status=status.HTTP_404_NOT_FOUND)
@@ -223,7 +228,7 @@ class EnrollmentView(APIView, ApiKeyPermissionMixIn):
"An error occurred while retrieving enrollments for user "
"'{username}' in course '{course_id}'"
).format(username=username, course_id=course_id)
}
},
)
@@ -251,6 +256,7 @@ class EnrollmentUserRolesView(APIView):
logged-in user, filtered by course_id if given, along with
whether or not the user is global staff
"""
authentication_classes = (
JwtAuthentication,
BearerAuthenticationAllowInactiveUser,
@@ -265,7 +271,7 @@ class EnrollmentUserRolesView(APIView):
Gets a list of all roles for the currently logged-in user, filtered by course_id if supplied
"""
try:
course_id = request.GET.get('course_id')
course_id = request.GET.get("course_id")
roles_data = api.get_user_roles(request.user.username)
if course_id:
roles_data = [role for role in roles_data if str(role.course_id) == course_id]
@@ -273,85 +279,83 @@ class EnrollmentUserRolesView(APIView):
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"message": (
"An error occurred while retrieving roles for user '{username}"
).format(username=request.user.username)
}
"message": ("An error occurred while retrieving roles for user '{username}").format(
username=request.user.username
)
},
)
return Response({
'roles': [
{
"org": role.org,
"course_id": str(role.course_id),
"role": role.role
}
for role in roles_data],
'is_staff': request.user.is_staff,
})
return Response(
{
"roles": [
{"org": role.org, "course_id": str(role.course_id), "role": role.role} for role in roles_data
],
"is_staff": request.user.is_staff,
}
)
@can_disable_rate_limit
class EnrollmentCourseDetailView(APIView):
"""
**Use Case**
**Use Case**
Get enrollment details for a course.
Get enrollment details for a course.
Response values include the course schedule and enrollment modes
supported by the course. Use the parameter include_expired=1 to
include expired enrollment modes in the response.
Response values include the course schedule and enrollment modes
supported by the course. Use the parameter include_expired=1 to
include expired enrollment modes in the response.
**Note:** Getting enrollment details for a course does not require
authentication.
**Note:** Getting enrollment details for a course does not require
authentication.
**Example Requests**
**Example Requests**
GET /api/enrollment/v1/course/{course_id}
GET /api/enrollment/v1/course/{course_id}
GET /api/enrollment/v1/course/{course_id}?include_expired=1
GET /api/enrollment/v1/course/{course_id}?include_expired=1
**Response Values**
**Response Values**
If the request is successful, an HTTP 200 "OK" response is
returned along with a collection of course enrollments for the
user or for the newly created enrollment.
If the request is successful, an HTTP 200 "OK" response is
returned along with a collection of course enrollments for the
user or for the newly created enrollment.
Each course enrollment contains the following values.
Each course enrollment contains the following values.
* course_end: The date and time when the course closes. If
null, the course never ends.
* course_id: The unique identifier for the course.
* course_name: The name of the course.
* course_modes: An array of data about the enrollment modes
supported for the course. If the request uses the parameter
include_expired=1, the array also includes expired
enrollment modes.
* course_end: The date and time when the course closes. If
null, the course never ends.
* course_id: The unique identifier for the course.
* course_name: The name of the course.
* course_modes: An array of data about the enrollment modes
supported for the course. If the request uses the parameter
include_expired=1, the array also includes expired
enrollment modes.
Each enrollment mode collection includes the following
values.
Each enrollment mode collection includes the following
values.
* currency: The currency of the listed prices.
* description: A description of this mode.
* expiration_datetime: The date and time after which
users cannot enroll in the course in this mode.
* min_price: The minimum price for which a user can
enroll in this mode.
* name: The full name of the enrollment mode.
* slug: The short name for the enrollment mode.
* suggested_prices: A list of suggested prices for
this enrollment mode.
* currency: The currency of the listed prices.
* description: A description of this mode.
* expiration_datetime: The date and time after which
users cannot enroll in the course in this mode.
* min_price: The minimum price for which a user can
enroll in this mode.
* name: The full name of the enrollment mode.
* slug: The short name for the enrollment mode.
* suggested_prices: A list of suggested prices for
this enrollment mode.
* course_start: The date and time when the course opens. If
null, the course opens immediately when it is created.
* enrollment_end: The date and time after which users cannot
enroll for the course. If null, the enrollment period never
ends.
* enrollment_start: The date and time when users can begin
enrolling in the course. If null, enrollment opens
immediately when the course is created.
* invite_only: A value indicating whether students must be
invited to enroll in the course. Possible values are true or
false.
* course_start: The date and time when the course opens. If
null, the course opens immediately when it is created.
* enrollment_end: The date and time after which users cannot
enroll for the course. If null, the enrollment period never
ends.
* enrollment_start: The date and time when users can begin
enrolling in the course. If null, enrollment opens
immediately when the course is created.
* invite_only: A value indicating whether students must be
invited to enroll in the course. Possible values are true or
false.
"""
authentication_classes = []
@@ -374,54 +378,54 @@ class EnrollmentCourseDetailView(APIView):
"""
try:
return Response(api.get_course_enrollment_details(course_id, bool(request.GET.get('include_expired', ''))))
return Response(api.get_course_enrollment_details(course_id, bool(request.GET.get("include_expired", ""))))
except CourseNotFoundError:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"message": (
"No course found for course ID '{course_id}'"
).format(course_id=course_id)
}
data={"message": ("No course found for course ID '{course_id}'").format(course_id=course_id)},
)
class UnenrollmentView(APIView):
"""
**Use Cases**
**Use Cases**
* Unenroll a single user from all courses.
* Unenroll a single user from all courses.
This command can only be issued by a privileged service user.
This command can only be issued by a privileged service user.
**Example Requests**
**Example Requests**
POST /api/enrollment/v1/enrollment {
"username": "username12345"
}
POST /api/enrollment/v1/enrollment {
"username": "username12345"
}
**POST Parameters**
**POST Parameters**
A POST request must include the following parameter.
A POST request must include the following parameter.
* username: The username of the user being unenrolled.
This will never match the username from the request,
since the request is issued as a privileged service user.
* username: The username of the user being unenrolled.
This will never match the username from the request,
since the request is issued as a privileged service user.
**POST Response Values**
**POST Response Values**
If the user has not requested retirement and does not have a retirement
request status, the request returns an HTTP 404 "Does Not Exist" response.
If the user has not requested retirement and does not have a retirement
request status, the request returns an HTTP 404 "Does Not Exist" response.
If the user is already unenrolled from all courses, the request returns
an HTTP 204 "No Content" response.
If the user is already unenrolled from all courses, the request returns
an HTTP 204 "No Content" response.
If an unexpected error occurs, the request returns an HTTP 500 response.
If an unexpected error occurs, the request returns an HTTP 500 response.
If the request is successful, an HTTP 200 "OK" response is
returned along with a list of all courses from which the user was unenrolled.
"""
permission_classes = (permissions.IsAuthenticated, CanRetireUser,)
If the request is successful, an HTTP 200 "OK" response is
returned along with a list of all courses from which the user was unenrolled.
"""
permission_classes = (
permissions.IsAuthenticated,
CanRetireUser,
)
def post(self, request):
"""
@@ -429,18 +433,18 @@ class UnenrollmentView(APIView):
"""
try:
# Get the username from the request.
username = request.data['username']
username = request.data["username"]
# Ensure that a retirement request status row exists for this username.
UserRetirementStatus.get_retirement_for_retirement_action(username)
enrollments = api.get_enrollments(username)
active_enrollments = [enrollment for enrollment in enrollments if enrollment['is_active']]
active_enrollments = [enrollment for enrollment in enrollments if enrollment["is_active"]]
if len(active_enrollments) < 1:
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(api.unenroll_user_from_all_courses(username))
except KeyError:
return Response('Username not specified.', status=status.HTTP_404_NOT_FOUND)
return Response("Username not specified.", status=status.HTTP_404_NOT_FOUND)
except UserRetirementStatus.DoesNotExist:
return Response('No retirement request status for username.', status=status.HTTP_404_NOT_FOUND)
return Response("No retirement request status for username.", status=status.HTTP_404_NOT_FOUND)
except Exception as exc: # pylint: disable=broad-except
return Response(str(exc), status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@@ -448,177 +452,178 @@ class UnenrollmentView(APIView):
@can_disable_rate_limit
class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
"""
**Use Cases**
**Use Cases**
* Get a list of all course enrollments for the currently signed in user.
* Get a list of all course enrollments for the currently signed in user.
* Enroll the currently signed in user in a course.
* Enroll the currently signed in user in a course.
Currently a user can use this command only to enroll the
user in the default course mode. If this is not
supported for the course, the request fails and returns
the available modes.
Currently a user can use this command only to enroll the
user in the default course mode. If this is not
supported for the course, the request fails and returns
the available modes.
This command can use a server-to-server call to enroll a user in
other modes, such as "verified", "professional", or "credit". If
the mode is not supported for the course, the request will fail
and return the available modes.
This command can use a server-to-server call to enroll a user in
other modes, such as "verified", "professional", or "credit". If
the mode is not supported for the course, the request will fail
and return the available modes.
You can include other parameters as enrollment attributes for a
specific course mode. For example, for credit mode, you can
include the following parameters to specify the credit provider
attribute.
You can include other parameters as enrollment attributes for a
specific course mode. For example, for credit mode, you can
include the following parameters to specify the credit provider
attribute.
* namespace: credit
* name: provider_id
* value: institution_name
* namespace: credit
* name: provider_id
* value: institution_name
**Example Requests**
**Example Requests**
GET /api/enrollment/v1/enrollment
GET /api/enrollment/v1/enrollment
POST /api/enrollment/v1/enrollment {
POST /api/enrollment/v1/enrollment {
"mode": "credit",
"course_details":{"course_id": "edX/DemoX/Demo_Course"},
"enrollment_attributes":[{"namespace": "credit","name": "provider_id","value": "hogwarts",},]
"mode": "credit",
"course_details":{"course_id": "edX/DemoX/Demo_Course"},
"enrollment_attributes":[{"namespace": "credit","name": "provider_id","value": "hogwarts",},]
}
}
**POST Parameters**
**POST Parameters**
A POST request can include the following parameters.
A POST request can include the following parameters.
* user: Optional. The username of the currently logged in user.
You cannot use the command to enroll a different user.
* user: Optional. The username of the currently logged in user.
You cannot use the command to enroll a different user.
* mode: Optional. The course mode for the enrollment. Individual
users cannot upgrade their enrollment mode from the default. Only
server-to-server requests can enroll with other modes.
* mode: Optional. The course mode for the enrollment. Individual
users cannot upgrade their enrollment mode from the default. Only
server-to-server requests can enroll with other modes.
* is_active: Optional. A Boolean value indicating whether the
enrollment is active. Only server-to-server requests are
allowed to deactivate an enrollment.
* is_active: Optional. A Boolean value indicating whether the
enrollment is active. Only server-to-server requests are
allowed to deactivate an enrollment.
* course details: A collection that includes the following
information.
* course details: A collection that includes the following
information.
* course_id: The unique identifier for the course.
* course_id: The unique identifier for the course.
* email_opt_in: Optional. A Boolean value that indicates whether
the user wants to receive email from the organization that runs
this course.
* email_opt_in: Optional. A Boolean value that indicates whether
the user wants to receive email from the organization that runs
this course.
* enrollment_attributes: A dictionary that contains the following
values.
* enrollment_attributes: A dictionary that contains the following
values.
* namespace: Namespace of the attribute
* name: Name of the attribute
* value: Value of the attribute
* namespace: Namespace of the attribute
* name: Name of the attribute
* value: Value of the attribute
* is_active: Optional. A Boolean value that indicates whether the
enrollment is active. Only server-to-server requests can
deactivate an enrollment.
* is_active: Optional. A Boolean value that indicates whether the
enrollment is active. Only server-to-server requests can
deactivate an enrollment.
* mode: Optional. The course mode for the enrollment. Individual
users cannot upgrade their enrollment mode from the default. Only
server-to-server requests can enroll with other modes.
* mode: Optional. The course mode for the enrollment. Individual
users cannot upgrade their enrollment mode from the default. Only
server-to-server requests can enroll with other modes.
* user: Optional. The user ID of the currently logged in user. You
cannot use the command to enroll a different user.
* user: Optional. The user ID of the currently logged in user. You
cannot use the command to enroll a different user.
* enterprise_course_consent: Optional. A Boolean value that
indicates the consent status for an EnterpriseCourseEnrollment
to be posted to the Enterprise service.
* enterprise_course_consent: Optional. A Boolean value that
indicates the consent status for an EnterpriseCourseEnrollment
to be posted to the Enterprise service.
**GET Response Values**
**GET Response Values**
If an unspecified error occurs when the user tries to obtain a
learner's enrollments, the request returns an HTTP 400 "Bad
Request" response.
If an unspecified error occurs when the user tries to obtain a
learner's enrollments, the request returns an HTTP 400 "Bad
Request" response.
If the user does not have permission to view enrollment data for
the requested learner, the request returns an HTTP 404 "Not Found"
response.
If the user does not have permission to view enrollment data for
the requested learner, the request returns an HTTP 404 "Not Found"
response.
**POST Response Values**
**POST Response Values**
If the user does not specify a course ID, the specified course
does not exist, or the is_active status is invalid, the request
returns an HTTP 400 "Bad Request" response.
If the user does not specify a course ID, the specified course
does not exist, or the is_active status is invalid, the request
returns an HTTP 400 "Bad Request" response.
If a user who is not an admin tries to upgrade a learner's course
mode, the request returns an HTTP 403 "Forbidden" response.
If a user who is not an admin tries to upgrade a learner's course
mode, the request returns an HTTP 403 "Forbidden" response.
If the specified user does not exist, the request returns an HTTP
406 "Not Acceptable" response.
If the specified user does not exist, the request returns an HTTP
406 "Not Acceptable" response.
**GET and POST Response Values**
**GET and POST Response Values**
If the request is successful, an HTTP 200 "OK" response is
returned along with a collection of course enrollments for the
user or for the newly created enrollment.
If the request is successful, an HTTP 200 "OK" response is
returned along with a collection of course enrollments for the
user or for the newly created enrollment.
Each course enrollment contains the following values.
Each course enrollment contains the following values.
* course_details: A collection that includes the following
* course_details: A collection that includes the following
values.
* course_end: The date and time when the course closes. If
null, the course never ends.
* course_id: The unique identifier for the course.
* course_name: The name of the course.
* course_modes: An array of data about the enrollment modes
supported for the course. If the request uses the parameter
include_expired=1, the array also includes expired
enrollment modes.
Each enrollment mode collection includes the following
values.
* course_end: The date and time when the course closes. If
null, the course never ends.
* currency: The currency of the listed prices.
* course_id: The unique identifier for the course.
* description: A description of this mode.
* course_name: The name of the course.
* expiration_datetime: The date and time after which users
cannot enroll in the course in this mode.
* course_modes: An array of data about the enrollment modes
supported for the course. If the request uses the parameter
include_expired=1, the array also includes expired
enrollment modes.
* min_price: The minimum price for which a user can enroll in
this mode.
Each enrollment mode collection includes the following
values.
* name: The full name of the enrollment mode.
* currency: The currency of the listed prices.
* slug: The short name for the enrollment mode.
* description: A description of this mode.
* suggested_prices: A list of suggested prices for this
enrollment mode.
* expiration_datetime: The date and time after which users
cannot enroll in the course in this mode.
* course_start: The date and time when the course opens. If
null, the course opens immediately when it is created.
* min_price: The minimum price for which a user can enroll in
this mode.
* enrollment_end: The date and time after which users cannot
enroll for the course. If null, the enrollment period never
ends.
* name: The full name of the enrollment mode.
* enrollment_start: The date and time when users can begin
enrolling in the course. If null, enrollment opens
immediately when the course is created.
* slug: The short name for the enrollment mode.
* invite_only: A value indicating whether students must be
invited to enroll in the course. Possible values are true or
false.
* suggested_prices: A list of suggested prices for this
enrollment mode.
* created: The date the user account was created.
* course_start: The date and time when the course opens. If
null, the course opens immediately when it is created.
* is_active: Whether the enrollment is currently active.
* enrollment_end: The date and time after which users cannot
enroll for the course. If null, the enrollment period never
ends.
* mode: The enrollment mode of the user in this course.
* enrollment_start: The date and time when users can begin
enrolling in the course. If null, enrollment opens
immediately when the course is created.
* invite_only: A value indicating whether students must be
invited to enroll in the course. Possible values are true or
false.
* created: The date the user account was created.
* is_active: Whether the enrollment is currently active.
* mode: The enrollment mode of the user in this course.
* user: The username of the user.
* user: The username of the user.
"""
authentication_classes = (
JwtAuthentication,
BearerAuthenticationAllowInactiveUser,
@@ -648,20 +653,23 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
Users who have the global staff permission can access all enrollment data for all
courses.
"""
username = request.GET.get('user', request.user.username)
username = request.GET.get("user", request.user.username)
try:
enrollment_data = api.get_enrollments(username)
except CourseEnrollmentError:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"message": (
"An error occurred while retrieving enrollments for user '{username}'"
).format(username=username)
}
"message": ("An error occurred while retrieving enrollments for user '{username}'").format(
username=username
)
},
)
if username == request.user.username or GlobalStaff().has_user(request.user) or \
self.has_api_key_permissions(request):
if (
username == request.user.username
or GlobalStaff().has_user(request.user)
or self.has_api_key_permissions(request)
):
return Response(enrollment_data)
filtered_data = []
for enrollment in enrollment_data:
@@ -679,32 +687,33 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
"""
# Get the User, Course ID, and Mode from the request.
username = request.data.get('user')
course_id = request.data.get('course_details', {}).get('course_id')
username = request.data.get("user")
course_id = request.data.get("course_details", {}).get("course_id")
if not course_id:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={"message": "Course ID must be specified to create a new enrollment."}
data={"message": "Course ID must be specified to create a new enrollment."},
)
try:
course_id = CourseKey.from_string(course_id)
except InvalidKeyError:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"message": f"No course '{course_id}' found for enrollment"
}
status=status.HTTP_400_BAD_REQUEST, data={"message": f"No course '{course_id}' found for enrollment"}
)
mode = request.data.get('mode')
mode = request.data.get("mode")
has_api_key_permissions = self.has_api_key_permissions(request)
# Check that the user specified is either the same user, or this is a server-to-server request.
if username and username != request.user.username and not has_api_key_permissions \
and not GlobalStaff().has_user(request.user):
if (
username
and username != request.user.username
and not has_api_key_permissions
and not GlobalStaff().has_user(request.user)
):
# Return a 404 instead of a 403 (Unauthorized). If one user is looking up
# other users, do not let them deduce the existence of an enrollment.
return Response(status=status.HTTP_404_NOT_FOUND)
@@ -712,7 +721,7 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
# A provided user has priority over a provided email.
# Fallback on request user if neither is provided.
if not username:
email = request.data.get('email')
email = request.data.get("email")
if email:
# Only server-to-server or staff users can use the email for the request.
if not has_api_key_permissions and not GlobalStaff().has_user(request.user):
@@ -722,22 +731,23 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
except ObjectDoesNotExist:
return Response(
status=status.HTTP_406_NOT_ACCEPTABLE,
data={
'message': f'The user with the email address {email} does not exist.'
}
data={"message": f"The user with the email address {email} does not exist."},
)
else:
username = request.user.username
if mode not in (CourseMode.AUDIT, CourseMode.HONOR, None) and not has_api_key_permissions \
and not GlobalStaff().has_user(request.user):
if (
mode not in (CourseMode.AUDIT, CourseMode.HONOR, None)
and not has_api_key_permissions
and not GlobalStaff().has_user(request.user)
):
return Response(
status=status.HTTP_403_FORBIDDEN,
data={
"message": "User does not have permission to create enrollment with mode [{mode}].".format(
mode=mode
)
}
},
)
try:
@@ -745,10 +755,7 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
user = User.objects.get(username=username)
except ObjectDoesNotExist:
return Response(
status=status.HTTP_406_NOT_ACCEPTABLE,
data={
'message': f'The user {username} does not exist.'
}
status=status.HTTP_406_NOT_ACCEPTABLE, data={"message": f"The user {username} does not exist."}
)
embargo_response = embargo_api.get_embargo_response(request, course_id, user)
@@ -757,54 +764,53 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
return embargo_response
try:
is_active = request.data.get('is_active')
is_active = request.data.get("is_active")
# Check if the requested activation status is None or a Boolean
if is_active is not None and not isinstance(is_active, bool):
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
'message': ("'{value}' is an invalid enrollment activation status.").format(value=is_active)
}
data={"message": ("'{value}' is an invalid enrollment activation status.").format(value=is_active)},
)
explicit_linked_enterprise = request.data.get('linked_enterprise_customer')
explicit_linked_enterprise = request.data.get("linked_enterprise_customer")
if explicit_linked_enterprise and has_api_key_permissions and enterprise_enabled():
enterprise_api_client = EnterpriseApiServiceClient()
consent_client = ConsentApiServiceClient()
try:
enterprise_api_client.post_enterprise_course_enrollment(username, str(course_id))
except EnterpriseApiException as error:
log.exception("An unexpected error occurred while creating the new EnterpriseCourseEnrollment "
"for user [%s] in course run [%s]", username, course_id)
log.exception(
"An unexpected error occurred while creating the new EnterpriseCourseEnrollment "
"for user [%s] in course run [%s]",
username,
course_id,
)
raise CourseEnrollmentError(str(error)) # lint-amnesty, pylint: disable=raise-missing-from
kwargs = {
'username': username,
'course_id': str(course_id),
'enterprise_customer_uuid': explicit_linked_enterprise,
"username": username,
"course_id": str(course_id),
"enterprise_customer_uuid": explicit_linked_enterprise,
}
consent_client.provide_consent(**kwargs)
enrollment_attributes = request.data.get('enrollment_attributes')
force_enrollment = request.data.get('force_enrollment')
enrollment_attributes = request.data.get("enrollment_attributes")
force_enrollment = request.data.get("force_enrollment")
# Check if the force enrollment status is None or a Boolean
if force_enrollment is not None and not isinstance(force_enrollment, bool):
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
'message': ("'{value}' is an invalid force enrollment status.").format(value=force_enrollment)
}
"message": ("'{value}' is an invalid force enrollment status.").format(value=force_enrollment)
},
)
# Only a staff user role can enroll a user forcefully
force_enrollment = force_enrollment and GlobalStaff().has_user(request.user)
enrollment = api.get_enrollment(username, str(course_id))
mode_changed = enrollment and mode is not None and enrollment['mode'] != mode
active_changed = enrollment and is_active is not None and enrollment['is_active'] != is_active
mode_changed = enrollment and mode is not None and enrollment["mode"] != mode
active_changed = enrollment and is_active is not None and enrollment["is_active"] != is_active
missing_attrs = []
if enrollment_attributes:
actual_attrs = [
"{namespace}:{name}".format(**attr)
for attr in enrollment_attributes
]
actual_attrs = ["{namespace}:{name}".format(**attr) for attr in enrollment_attributes]
missing_attrs = set(REQUIRED_ATTRIBUTES.get(mode, [])) - set(actual_attrs)
if (GlobalStaff().has_user(request.user) or has_api_key_permissions) and (mode_changed or active_changed):
if mode_changed and active_changed and not is_active:
@@ -831,7 +837,7 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
is_active=is_active,
enrollment_attributes=enrollment_attributes,
# If we are updating enrollment by authorized api caller, we should allow expired modes
include_expired=has_api_key_permissions
include_expired=has_api_key_permissions,
)
else:
# Will reactivate inactive enrollments.
@@ -841,26 +847,26 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
mode=mode,
is_active=is_active,
enrollment_attributes=enrollment_attributes,
enterprise_uuid=request.data.get('enterprise_uuid'),
enterprise_uuid=request.data.get("enterprise_uuid"),
force_enrollment=force_enrollment,
# If we are creating enrollment by staff user with force_enrollment, we should allow expired modes
include_expired=force_enrollment
include_expired=force_enrollment,
)
cohort_name = request.data.get('cohort')
cohort_name = request.data.get("cohort")
if cohort_name is not None:
cohort = get_cohort_by_name(course_id, cohort_name)
try:
add_user_to_cohort(cohort, user)
except ValueError:
# user already in cohort, probably because they were un-enrolled and re-enrolled
log.exception('Cohort re-addition')
email_opt_in = request.data.get('email_opt_in', None)
log.exception("Cohort re-addition")
email_opt_in = request.data.get("email_opt_in", None)
if email_opt_in is not None:
org = course_id.org
update_email_opt_in(request.user, org, email_opt_in)
log.info('The user [%s] has already been enrolled in course run [%s].', username, course_id)
log.info("The user [%s] has already been enrolled in course run [%s].", username, course_id)
return Response(response)
except CourseModeNotFoundError as error:
return Response(
@@ -869,21 +875,22 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
"message": (
"The [{mode}] course mode is expired or otherwise unavailable for course run [{course_id}]."
).format(mode=mode, course_id=course_id),
"course_details": error.data
})
"course_details": error.data,
},
)
except CourseNotFoundError:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"message": f"No course '{course_id}' found for enrollment"
}
status=status.HTTP_400_BAD_REQUEST, data={"message": f"No course '{course_id}' found for enrollment"}
)
except CourseEnrollmentExistsError as error:
log.warning('An enrollment already exists for user [%s] in course run [%s].', username, course_id)
log.warning("An enrollment already exists for user [%s] in course run [%s].", username, course_id)
return Response(data=error.enrollment)
except CourseEnrollmentError:
log.exception("An error occurred while creating the new course enrollment for user "
"[%s] in course run [%s]", username, course_id)
log.exception(
"An error occurred while creating the new course enrollment for user [%s] in course run [%s]",
username,
course_id,
)
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
@@ -891,100 +898,100 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
"An error occurred while creating the new course enrollment for user "
"'{username}' in course '{course_id}'"
).format(username=username, course_id=course_id)
}
},
)
except CourseUserGroup.DoesNotExist:
log.exception('Missing cohort [%s] in course run [%s]', cohort_name, course_id)
log.exception("Missing cohort [%s] in course run [%s]", cohort_name, course_id)
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"message": "An error occured while adding to cohort [%s]" % cohort_name
})
data={"message": "An error occured while adding to cohort [%s]" % cohort_name},
)
finally:
# Assumes that the ecommerce service uses an API key to authenticate.
if has_api_key_permissions:
current_enrollment = api.get_enrollment(username, str(course_id))
audit_log(
'enrollment_change_requested',
"enrollment_change_requested",
course_id=str(course_id),
requested_mode=mode,
actual_mode=current_enrollment['mode'] if current_enrollment else None,
actual_mode=current_enrollment["mode"] if current_enrollment else None,
requested_activation=is_active,
actual_activation=current_enrollment['is_active'] if current_enrollment else None,
user_id=user.id
actual_activation=current_enrollment["is_active"] if current_enrollment else None,
user_id=user.id,
)
@can_disable_rate_limit
class CourseEnrollmentsApiListView(DeveloperErrorViewMixin, ListAPIView):
"""
**Use Cases**
**Use Cases**
Get a list of all course enrollments, optionally filtered by a course ID or list of usernames.
Get a list of all course enrollments, optionally filtered by a course ID or list of usernames.
**Example Requests**
**Example Requests**
GET /api/enrollment/v1/enrollments
GET /api/enrollment/v1/enrollments
GET /api/enrollment/v1/enrollments?course_id={course_id}
GET /api/enrollment/v1/enrollments?course_id={course_id}
GET /api/enrollment/v1/enrollments?username={username},{username},{username}
GET /api/enrollment/v1/enrollments?username={username},{username},{username}
GET /api/enrollment/v1/enrollments?course_id={course_id}&username={username}
GET /api/enrollment/v1/enrollments?course_id={course_id}&username={username}
GET /api/enrollment/v1/enrollments?email={email},{email}
GET /api/enrollment/v1/enrollments?email={email},{email}
**Query Parameters for GET**
**Query Parameters for GET**
* course_id: Filters the result to course enrollments for the course corresponding to the
given course ID. The value must be URL encoded. Optional.
* course_id: Filters the result to course enrollments for the course corresponding to the
given course ID. The value must be URL encoded. Optional.
* username: List of comma-separated usernames. Filters the result to the course enrollments
of the given users. Optional.
* username: List of comma-separated usernames. Filters the result to the course enrollments
of the given users. Optional.
* email: List of comma-separated emails. Filters the result to the course enrollments
of the given users. Optional.
* email: List of comma-separated emails. Filters the result to the course enrollments
of the given users. Optional.
* page_size: Number of results to return per page. Optional.
* page_size: Number of results to return per page. Optional.
* page: Page number to retrieve. Optional.
* page: Page number to retrieve. Optional.
**Response Values**
**Response Values**
If the request for information about the course enrollments is successful, an HTTP 200 "OK" response
is returned.
If the request for information about the course enrollments is successful, an HTTP 200 "OK" response
is returned.
The HTTP 200 response has the following values.
The HTTP 200 response has the following values.
* results: A list of the course enrollments matching the request.
* results: A list of the course enrollments matching the request.
* created: Date and time when the course enrollment was created.
* created: Date and time when the course enrollment was created.
* mode: Mode for the course enrollment.
* mode: Mode for the course enrollment.
* is_active: Whether the course enrollment is active or not.
* is_active: Whether the course enrollment is active or not.
* user: Username of the user in the course enrollment.
* user: Username of the user in the course enrollment.
* course_id: Course ID of the course in the course enrollment.
* course_id: Course ID of the course in the course enrollment.
* next: The URL to the next page of results, or null if this is the
last page.
* next: The URL to the next page of results, or null if this is the
last page.
* previous: The URL to the next page of results, or null if this
is the first page.
* previous: The URL to the next page of results, or null if this
is the first page.
If the user is not logged in, a 401 error is returned.
If the user is not logged in, a 401 error is returned.
If the user is not global staff, a 403 error is returned.
If the user is not global staff, a 403 error is returned.
If the specified course_id is not valid or any of the specified usernames
are not valid, a 400 error is returned.
If the specified course_id is not valid or any of the specified usernames
are not valid, a 400 error is returned.
If the specified course_id does not correspond to a valid course or if all the specified
usernames do not correspond to valid users, an HTTP 200 "OK" response is returned with an
empty 'results' field.
If the specified course_id does not correspond to a valid course or if all the specified
usernames do not correspond to valid users, an HTTP 200 "OK" response is returned with an
empty 'results' field.
"""
authentication_classes = (
JwtAuthentication,
BearerAuthenticationAllowInactiveUser,
@@ -1005,9 +1012,9 @@ class CourseEnrollmentsApiListView(DeveloperErrorViewMixin, ListAPIView):
raise ValidationError(form.errors)
queryset = CourseEnrollment.objects.all()
course_id = form.cleaned_data.get('course_id')
usernames = form.cleaned_data.get('username')
emails = form.cleaned_data.get('email')
course_id = form.cleaned_data.get("course_id")
usernames = form.cleaned_data.get("username")
emails = form.cleaned_data.get("email")
if course_id:
queryset = queryset.filter(course_id=course_id)
@@ -1022,6 +1029,7 @@ class EnrollmentAllowedView(APIView):
"""
A view that allows the retrieval and creation of enrollment allowed for a given user email and course id.
"""
permission_classes = (permissions.IsAdminUser,)
throttle_classes = (EnrollmentUserThrottle,)
serializer_class = CourseEnrollmentAllowedSerializer
@@ -1042,7 +1050,7 @@ class EnrollmentAllowedView(APIView):
- 200: Success.
- 403: Forbidden, you need to be staff.
"""
user_email = request.query_params.get('email')
user_email = request.query_params.get("email")
if not user_email:
user_email = request.user.email
@@ -1051,10 +1059,7 @@ class EnrollmentAllowedView(APIView):
CourseEnrollmentAllowedSerializer(enrollment).data for enrollment in enrollments_allowed
]
return Response(
status=status.HTTP_200_OK,
data=serialized_enrollments_allowed
)
return Response(status=status.HTTP_200_OK, data=serialized_enrollments_allowed)
def post(self, request):
"""
@@ -1089,29 +1094,22 @@ class EnrollmentAllowedView(APIView):
- 409: Conflict, enrollment allowed already exists.
"""
is_bad_request_response, email, course_id = self.check_required_data(request)
auto_enroll = request.data.get('auto_enroll', False)
auto_enroll = request.data.get("auto_enroll", False)
if is_bad_request_response:
return is_bad_request_response
try:
enrollment_allowed = CourseEnrollmentAllowed.objects.create(
email=email,
course_id=course_id,
auto_enroll=auto_enroll
email=email, course_id=course_id, auto_enroll=auto_enroll
)
except IntegrityError:
return Response(
status=status.HTTP_409_CONFLICT,
data={
'message': f'An enrollment allowed with email {email} and course {course_id} already exists.'
}
data={"message": f"An enrollment allowed with email {email} and course {course_id} already exists."},
)
serializer = CourseEnrollmentAllowedSerializer(enrollment_allowed)
return Response(
status=status.HTTP_201_CREATED,
data=serializer.data
)
return Response(status=status.HTTP_201_CREATED, data=serializer.data)
def delete(self, request):
"""
@@ -1148,33 +1146,27 @@ class EnrollmentAllowedView(APIView):
return is_bad_request_response
try:
CourseEnrollmentAllowed.objects.get(
email=email,
course_id=course_id
).delete()
CourseEnrollmentAllowed.objects.get(email=email, course_id=course_id).delete()
return Response(
status=status.HTTP_204_NO_CONTENT,
)
except ObjectDoesNotExist:
return Response(
status=status.HTTP_404_NOT_FOUND,
data={
'message': f"An enrollment allowed with email {email} and course {course_id} doesn't exists."
}
data={"message": f"An enrollment allowed with email {email} and course {course_id} doesn't exists."},
)
def check_required_data(self, request):
"""
Check if the request has email and course_id.
"""
email = request.data.get('email')
course_id = request.data.get('course_id')
email = request.data.get("email")
course_id = request.data.get("course_id")
if not email or not course_id:
is_bad_request = Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"message": "Please provide a value for 'email' and 'course_id' in the request data."
})
data={"message": "Please provide a value for 'email' and 'course_id' in the request data."},
)
else:
is_bad_request = None
return (is_bad_request, email, course_id)