feat: add email support to the enrollment post and get methods (#33006)

This commit is contained in:
María Fernanda Magallanes
2023-09-15 13:43:14 -05:00
committed by GitHub
parent 02dd36cdc1
commit cddfc02fbc
4 changed files with 214 additions and 7 deletions

View File

@@ -15,9 +15,10 @@ class CourseEnrollmentsApiListForm(Form):
"""
A form that validates the query string parameters for the CourseEnrollmentsApiListView.
"""
MAX_USERNAME_COUNT = 100
MAX_INPUT_COUNT = 100
username = CharField(required=False)
course_id = CharField(required=False)
email = CharField(required=False)
def clean_course_id(self):
"""
@@ -38,14 +39,31 @@ class CourseEnrollmentsApiListForm(Form):
usernames_csv_string = self.cleaned_data.get('username')
if usernames_csv_string:
usernames = usernames_csv_string.split(',')
if len(usernames) > self.MAX_USERNAME_COUNT:
if len(usernames) > self.MAX_INPUT_COUNT:
raise ValidationError(
"Too many usernames in a single request - {}. A maximum of {} is allowed".format(
len(usernames),
self.MAX_USERNAME_COUNT,
self.MAX_INPUT_COUNT,
)
)
for username in usernames:
validate_username(username)
return usernames
return usernames_csv_string
def clean_email(self):
"""
Validate a string of comma-separated emails and return a list of emails.
"""
emails_csv_string = self.cleaned_data.get('email')
if emails_csv_string:
emails = emails_csv_string.split(',')
if len(emails) > self.MAX_INPUT_COUNT:
raise ValidationError(
"Too many emails in a single request - {}. A maximum of {} is allowed".format(
len(emails),
self.MAX_INPUT_COUNT,
)
)
return emails
return emails_csv_string

View File

@@ -114,6 +114,63 @@
]
],
[
{
"email": "student1@example.com"
},
[
{
"course_id": "course-v1:e+d+X",
"is_active": true,
"mode": "honor",
"user": "student1",
"created": "2018-01-01T00:00:01Z"
}
]
],
[
{
"email": "student1@example.com,student2@example.com"
},
[
{
"course_id": "course-v1:e+d+X",
"is_active": true,
"mode": "honor",
"user": "student1",
"created": "2018-01-01T00:00:01Z"
},
{
"course_id": "course-v1:e+d+X",
"is_active": true,
"mode": "honor",
"user": "student2",
"created": "2018-01-01T00:00:01Z"
},
{
"course_id": "course-v1:x+y+Z",
"is_active": true,
"mode": "honor",
"user": "student2",
"created": "2018-01-01T00:00:01Z"
}
]
],
[
{
"email": "student2@example.com",
"course_id": "course-v1:e+d+X"
},
[
{
"course_id": "course-v1:e+d+X",
"is_active": true,
"mode": "honor",
"user": "student2",
"created": "2018-01-01T00:00:01Z"
}
]
],
[
null,
[

View File

@@ -237,6 +237,112 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, Ente
assert is_active
assert course_mode == enrollment_mode
def test_enroll_with_email_staff(self):
# Create enrollments with email are allowed if you are staff.
self.client.logout()
AdminFactory.create(username='global_staff', email='global_staff@example.com', password=self.PASSWORD)
self.client.login(username="global_staff", password=self.PASSWORD)
resp = self.client.post(
reverse('courseenrollments'),
{
'course_details': {
'course_id': str(self.course.id)
},
'email': self.user.email
},
format='json'
)
assert resp.status_code == status.HTTP_200_OK
@patch('openedx.core.djangoapps.enrollments.views.EnrollmentListView.has_api_key_permissions')
def test_enroll_with_email_server(self, has_api_key_permissions_mock):
# Create enrollments with email are allowed if it is a server-to-server request.
has_api_key_permissions_mock.return_value = True
resp = self.client.post(
reverse('courseenrollments'),
{
'course_details': {
'course_id': str(self.course.id)
},
'email': self.user.email
},
format='json'
)
assert resp.status_code == status.HTTP_200_OK
def test_enroll_with_email_without_staff(self):
# If you are not staff or server request you can't create enrollments with email.
resp = self.client.post(
reverse('courseenrollments'),
{
'course_details': {
'course_id': str(self.course.id)
},
'email': self.other_user.email
},
format='json'
)
assert resp.status_code == status.HTTP_404_NOT_FOUND
def test_enroll_with_user_and_email(self):
# Creating enrollments the user has priority over the email.
resp = self.client.post(
reverse('courseenrollments'),
{
'course_details': {
'course_id': str(self.course.id)
},
'user': self.user.username,
'email': self.other_user.email
},
format='json'
)
self.assertContains(resp, self.user.username, status_code=status.HTTP_200_OK)
def test_enroll_with_user_without_permissions_and_email(self):
resp = self.client.post(
reverse('courseenrollments'),
{
'course_details': {
'course_id': str(self.course.id)
},
'user': self.other_user.username,
'email': self.user.email
},
format='json'
)
assert resp.status_code == status.HTTP_404_NOT_FOUND
def test_enroll_with_user_as_self_user(self):
resp = self.client.post(
reverse('courseenrollments'),
{
'course_details': {
'course_id': str(self.course.id)
},
'user': self.user.username
},
format='json'
)
self.assertContains(resp, self.user.username, status_code=status.HTTP_200_OK)
def test_enroll_without_user(self):
# To check if it takes the request.user.
resp = self.client.post(
reverse('courseenrollments'),
{
'course_details': {
'course_id': str(self.course.id)
}
},
format='json'
)
self.assertContains(resp, self.user.username, status_code=status.HTTP_200_OK)
@ddt.data(
# Default (no course modes in the database)
# Expect that users are automatically enrolled as the default

View File

@@ -676,7 +676,7 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
"""
# Get the User, Course ID, and Mode from the request.
username = request.data.get('user', request.user.username)
username = request.data.get('user')
course_id = request.data.get('course_details', {}).get('course_id')
if not course_id:
@@ -700,14 +700,32 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
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 not username:
username = request.user.username
if username != request.user.username and not has_api_key_permissions \
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)
# 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')
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):
return Response(status=status.HTTP_404_NOT_FOUND)
try:
username = User.objects.get(email=email).username
except ObjectDoesNotExist:
return Response(
status=status.HTTP_406_NOT_ACCEPTABLE,
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):
return Response(
@@ -911,6 +929,8 @@ class CourseEnrollmentsApiListView(DeveloperErrorViewMixin, ListAPIView):
GET /api/enrollment/v1/enrollments?course_id={course_id}&username={username}
GET /api/enrollment/v1/enrollments?email={email},{email}
**Query Parameters for GET**
* course_id: Filters the result to course enrollments for the course corresponding to the
@@ -919,6 +939,9 @@ class CourseEnrollmentsApiListView(DeveloperErrorViewMixin, ListAPIView):
* 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.
* page_size: Number of results to return per page. Optional.
* page: Page number to retrieve. Optional.
@@ -981,9 +1004,12 @@ class CourseEnrollmentsApiListView(DeveloperErrorViewMixin, ListAPIView):
queryset = CourseEnrollment.objects.all()
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)
if usernames:
queryset = queryset.filter(user__username__in=usernames)
if emails:
queryset = queryset.filter(user__email__in=emails)
return queryset