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:
@@ -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"
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user