Create a staff-only API to list all course enrollments

This data can be used by external frontends or other apps. The list
of course enrollments can optionally be filtered by course id or
usernames.
This commit is contained in:
Guruprasad Lakshmi Narayanan
2018-08-27 09:45:17 +05:30
parent a86d499592
commit df45fe8317
7 changed files with 505 additions and 4 deletions

View File

@@ -0,0 +1,157 @@
[
[
{
"course_id": "e/d/X"
},
[
{
"course_id": "e/d/X",
"is_active": true,
"mode": "honor",
"user": "student1",
"created": "2018-01-01T00:00:01Z"
},
{
"course_id": "e/d/X",
"is_active": true,
"mode": "honor",
"user": "student2",
"created": "2018-01-01T00:00:01Z"
}
]
],
[
{
"course_id": "x/y/Z"
},
[
{
"course_id": "x/y/Z",
"is_active": true,
"mode": "verified",
"user": "staff",
"created": "2018-01-01T00:00:01Z"
},
{
"course_id": "x/y/Z",
"is_active": true,
"mode": "honor",
"user": "student2",
"created": "2018-01-01T00:00:01Z"
},
{
"course_id": "x/y/Z",
"is_active": true,
"mode": "verified",
"user": "student3",
"created": "2018-01-01T00:00:01Z"
}
]
],
[
{
"course_id": "x/y/Z",
"username": "student2,student3"
},
[
{
"course_id": "x/y/Z",
"is_active": true,
"mode": "honor",
"user": "student2",
"created": "2018-01-01T00:00:01Z"
},
{
"course_id": "x/y/Z",
"is_active": true,
"mode": "verified",
"user": "student3",
"created": "2018-01-01T00:00:01Z"
}
]
],
[
{
"course_id": "x/y/Z",
"username": "student1,student2"
},
[
{
"course_id": "x/y/Z",
"is_active": true,
"mode": "honor",
"user": "student2",
"created": "2018-01-01T00:00:01Z"
}
]
],
[
{
"username": "student2,staff"
},
[
{
"course_id": "x/y/Z",
"is_active": true,
"mode": "verified",
"user": "staff",
"created": "2018-01-01T00:00:01Z"
},
{
"course_id": "e/d/X",
"is_active": true,
"mode": "honor",
"user": "student2",
"created": "2018-01-01T00:00:01Z"
},
{
"course_id": "x/y/Z",
"is_active": true,
"mode": "honor",
"user": "student2",
"created": "2018-01-01T00:00:01Z"
}
]
],
[
null,
[
{
"course_id": "e/d/X",
"is_active": true,
"mode": "honor",
"user": "student1",
"created": "2018-01-01T00:00:01Z"
},
{
"course_id": "e/d/X",
"is_active": true,
"mode": "honor",
"user": "student2",
"created": "2018-01-01T00:00:01Z"
},
{
"course_id": "x/y/Z",
"is_active": true,
"mode": "verified",
"user": "student3",
"created": "2018-01-01T00:00:01Z"
},
{
"course_id": "x/y/Z",
"is_active": true,
"mode": "honor",
"user": "student2",
"created": "2018-01-01T00:00:01Z"
},
{
"course_id": "x/y/Z",
"is_active": true,
"mode": "verified",
"user": "staff",
"created": "2018-01-01T00:00:01Z"
}
]
]
]

View File

@@ -16,6 +16,7 @@ from django.core.handlers.wsgi import WSGIRequest
from django.urls import reverse
from django.test import Client
from django.test.utils import override_settings
from freezegun import freeze_time
from mock import patch
from rest_framework import status
from rest_framework.test import APITestCase
@@ -24,7 +25,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from enrollment import api
from enrollment import api, data
from enrollment.errors import CourseEnrollmentError
from enrollment.views import EnrollmentUserThrottle
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
@@ -1522,3 +1523,162 @@ class UserRoleTest(ModuleStoreTestCase):
}
response_data = json.loads(response.content)
self.assertEqual(response_data, expected_response)
@ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class CourseEnrollmentsApiListTest(APITestCase, ModuleStoreTestCase):
"""
Test the course enrollments list API.
"""
shard = 3
CREATED_DATA = datetime.datetime(2018, 1, 1, 0, 0, 1, tzinfo=pytz.UTC)
def setUp(self):
super(CourseEnrollmentsApiListTest, self).setUp()
self.rate_limit_config = RateLimitConfiguration.current()
self.rate_limit_config.enabled = False
self.rate_limit_config.save()
throttle = EnrollmentUserThrottle()
self.rate_limit, __ = throttle.parse_rate(throttle.rate)
self.course = CourseFactory.create(org='e', number='d', run='X', emit_signals=True)
self.course2 = CourseFactory.create(org='x', number='y', run='Z', emit_signal=True)
for mode_slug in ('honor', 'verified', 'audit'):
CourseModeFactory.create(
course_id=self.course.id,
mode_slug=mode_slug,
mode_display_name=mode_slug
)
self.staff_user = AdminFactory(
username='staff',
email='staff@example.com',
password='edx'
)
self.student1 = UserFactory(
username='student1',
email='student1@example.com',
password='edx'
)
self.student2 = UserFactory(
username='student2',
email='student2@example.com',
password='edx'
)
self.student3 = UserFactory(
username='student3',
email='student3@example.com',
password='edx'
)
with freeze_time(self.CREATED_DATA):
data.create_course_enrollment(
self.student1.username,
unicode(self.course.id),
'honor',
True
)
data.create_course_enrollment(
self.student2.username,
unicode(self.course.id),
'honor',
True
)
data.create_course_enrollment(
self.student3.username,
unicode(self.course2.id),
'verified',
True
)
data.create_course_enrollment(
self.student2.username,
unicode(self.course2.id),
'honor',
True
)
data.create_course_enrollment(
self.staff_user.username,
unicode(self.course2.id),
'verified',
True
)
self.url = reverse('courseenrollmentsapilist')
def _login_as_staff(self):
self.client.login(username=self.staff_user.username, password='edx')
def _make_request(self, query_params=None):
return self.client.get(self.url, query_params)
def _assert_list_of_enrollments(self, query_params=None, expected_status=status.HTTP_200_OK, error_fields=None):
"""
Make a request to the CourseEnrolllmentApiList endpoint and run assertions on the response
using the optional parameters 'query_params', 'expected_status' and 'error_fields'.
"""
response = self._make_request(query_params)
self.assertEqual(response.status_code, expected_status)
content = json.loads(response.content)
if error_fields is not None:
self.assertIn('field_errors', content)
for error_field in error_fields:
self.assertIn(error_field, content['field_errors'])
return content
def test_user_not_authenticated(self):
self.client.logout()
response = self.client.get(self.url, {'course_id': self.course.id})
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_user_not_authorized(self):
self.client.login(username=self.student1.username, password='edx')
response = self.client.get(self.url, {'course_id': self.course.id})
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@ddt.data(
({'course_id': '1'}, ['course_id', ]),
({'course_id': '1', 'username': 'staff'}, ['course_id', ]),
({'username': '1*2'}, ['username', ]),
({'username': '1*2', 'course_id': 'org.0/course_0/Run_0'}, ['username', ]),
({'username': '1*2', 'course_id': '1'}, ['username', 'course_id']),
({'username': ','.join(str(x) for x in range(101))}, ['username', ])
)
@ddt.unpack
def test_query_string_parameters_invalid_errors(self, query_params, error_fields):
self._login_as_staff()
self._assert_list_of_enrollments(query_params, status.HTTP_400_BAD_REQUEST, error_fields)
@ddt.data(
# Non-existent user
({'username': 'nobody'}, ),
({'username': 'nobody', 'course_id': 'e/d/X'}, ),
# Non-existent course
({'course_id': 'a/b/c'}, ),
({'course_id': 'a/b/c', 'username': 'student1'}, ),
# Non-existent course and user
({'course_id': 'a/b/c', 'username': 'dummy'}, )
)
@ddt.unpack
def test_non_existent_course_user(self, query_params):
self._login_as_staff()
content = self._assert_list_of_enrollments(query_params, status.HTTP_200_OK)
self.assertEqual(len(content['results']), 0)
@ddt.file_data('fixtures/course-enrollments-api-list-valid-data.json')
@ddt.unpack
def test_response_valid_queries(self, args):
query_params = args[0]
expected_results = args[1]
self._login_as_staff()
content = self._assert_list_of_enrollments(query_params, status.HTTP_200_OK)
results = content['results']
self.assertItemsEqual(results, expected_results)