Merge pull request #8235 from edx/nickersoft-include-expired
Added optional GET parameter to the enrollment API that includes expired course modes
This commit is contained in:
@@ -166,7 +166,7 @@ class CourseMode(models.Model):
|
||||
return [mode.to_tuple() for mode in found_course_modes]
|
||||
|
||||
@classmethod
|
||||
def modes_for_course(cls, course_id, only_selectable=True):
|
||||
def modes_for_course(cls, course_id, include_expired=False, only_selectable=True):
|
||||
"""
|
||||
Returns a list of the non-expired modes for a given course id
|
||||
|
||||
@@ -176,6 +176,9 @@ class CourseMode(models.Model):
|
||||
course_id (CourseKey): Search for course modes for this course.
|
||||
|
||||
Keyword Arguments:
|
||||
include_expired (bool): If True, expired course modes will be included
|
||||
in the returned JSON data. If False, these modes will be omitted.
|
||||
|
||||
only_selectable (bool): If True, include only modes that are shown
|
||||
to users on the track selection page. (Currently, "credit" modes
|
||||
aren't available to users until they complete the course, so
|
||||
@@ -186,9 +189,14 @@ class CourseMode(models.Model):
|
||||
|
||||
"""
|
||||
now = datetime.now(pytz.UTC)
|
||||
found_course_modes = cls.objects.filter(
|
||||
Q(course_id=course_id) & (Q(expiration_datetime__isnull=True) | Q(expiration_datetime__gte=now))
|
||||
)
|
||||
|
||||
found_course_modes = cls.objects.filter(course_id=course_id)
|
||||
|
||||
# Filter out expired course modes if include_expired is not set
|
||||
if not include_expired:
|
||||
found_course_modes = found_course_modes.filter(
|
||||
Q(expiration_datetime__isnull=True) | Q(expiration_datetime__gte=now)
|
||||
)
|
||||
|
||||
# Credit course modes are currently not shown on the track selection page;
|
||||
# they're available only when students complete a course. For this reason,
|
||||
|
||||
@@ -235,7 +235,7 @@ def update_enrollment(user_id, course_id, mode=None, is_active=None):
|
||||
return enrollment
|
||||
|
||||
|
||||
def get_course_enrollment_details(course_id):
|
||||
def get_course_enrollment_details(course_id, include_expired=False):
|
||||
"""Get the course modes for course. Also get enrollment start and end date, invite only, etc.
|
||||
|
||||
Given a course_id, return a serializable dictionary of properties describing course enrollment information.
|
||||
@@ -243,6 +243,9 @@ def get_course_enrollment_details(course_id):
|
||||
Args:
|
||||
course_id (str): The Course to get enrollment information for.
|
||||
|
||||
include_expired (bool): Boolean denoting whether expired course modes
|
||||
should be included in the returned JSON data.
|
||||
|
||||
Returns:
|
||||
A serializable dictionary of course enrollment information.
|
||||
|
||||
@@ -270,8 +273,10 @@ def get_course_enrollment_details(course_id):
|
||||
}
|
||||
|
||||
"""
|
||||
cache_key = u"enrollment.course.details.{course_id}".format(course_id=course_id)
|
||||
|
||||
cache_key = u'enrollment.course.details.{course_id}.{include_expired}'.format(
|
||||
course_id=course_id,
|
||||
include_expired=include_expired
|
||||
)
|
||||
cached_enrollment_data = None
|
||||
try:
|
||||
cached_enrollment_data = cache.get(cache_key)
|
||||
@@ -283,7 +288,7 @@ def get_course_enrollment_details(course_id):
|
||||
log.info(u"Get enrollment data for course %s (cached)", course_id)
|
||||
return cached_enrollment_data
|
||||
|
||||
course_enrollment_details = _data_api().get_course_enrollment_info(course_id)
|
||||
course_enrollment_details = _data_api().get_course_enrollment_info(course_id, include_expired)
|
||||
|
||||
try:
|
||||
cache_time_out = getattr(settings, 'ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT', 60)
|
||||
|
||||
@@ -142,7 +142,7 @@ def _update_enrollment(enrollment, is_active=None, mode=None):
|
||||
return CourseEnrollmentSerializer(enrollment).data # pylint: disable=no-member
|
||||
|
||||
|
||||
def get_course_enrollment_info(course_id):
|
||||
def get_course_enrollment_info(course_id, include_expired=False):
|
||||
"""Returns all course enrollment information for the given course.
|
||||
|
||||
Based on the course id, return all related course information..
|
||||
@@ -150,6 +150,9 @@ def get_course_enrollment_info(course_id):
|
||||
Args:
|
||||
course_id (str): The course to retrieve enrollment information for.
|
||||
|
||||
include_expired (bool): Boolean denoting whether expired course modes
|
||||
should be included in the returned JSON data.
|
||||
|
||||
Returns:
|
||||
A serializable dictionary representing the course's enrollment information.
|
||||
|
||||
@@ -163,4 +166,4 @@ def get_course_enrollment_info(course_id):
|
||||
msg = u"Requested enrollment information for unknown course {course}".format(course=course_id)
|
||||
log.warning(msg)
|
||||
raise CourseNotFoundError(msg)
|
||||
return CourseField().to_native(course)
|
||||
return CourseField().to_native(course, include_expired=include_expired)
|
||||
|
||||
@@ -38,10 +38,10 @@ class CourseField(serializers.RelatedField):
|
||||
|
||||
"""
|
||||
|
||||
def to_native(self, course):
|
||||
def to_native(self, course, **kwargs):
|
||||
course_id = unicode(course.id)
|
||||
course_modes = ModeSerializer(
|
||||
CourseMode.modes_for_course(course.id, only_selectable=False)
|
||||
CourseMode.modes_for_course(course.id, kwargs.get('include_expired', False), only_selectable=False)
|
||||
).data # pylint: disable=no-member
|
||||
|
||||
return {
|
||||
@@ -94,7 +94,7 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
|
||||
"""Retrieves the username from the associated model."""
|
||||
return model.username
|
||||
|
||||
class Meta: # pylint: disable=missing-docstring
|
||||
class Meta(object): # pylint: disable=missing-docstring
|
||||
model = CourseEnrollment
|
||||
fields = ('created', 'mode', 'is_active', 'course_details', 'user')
|
||||
lookup_field = 'username'
|
||||
|
||||
@@ -46,7 +46,7 @@ def update_course_enrollment(student_id, course_id, mode=None, is_active=None):
|
||||
return enrollment
|
||||
|
||||
|
||||
def get_course_enrollment_info(course_id):
|
||||
def get_course_enrollment_info(course_id, include_expired=False):
|
||||
"""Stubbed out Enrollment data request."""
|
||||
return _get_fake_course_info(course_id)
|
||||
|
||||
|
||||
@@ -476,6 +476,40 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
|
||||
self.assertTrue(is_active)
|
||||
self.assertEqual(course_mode, 'professional')
|
||||
|
||||
def test_enrollment_includes_expired_verified(self):
|
||||
"""With the right API key, request that expired course verifications are still returned. """
|
||||
# Create a honor mode for a course.
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug=CourseMode.HONOR,
|
||||
mode_display_name=CourseMode.HONOR,
|
||||
)
|
||||
|
||||
# Create a verified mode for a course.
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug=CourseMode.VERIFIED,
|
||||
mode_display_name=CourseMode.VERIFIED,
|
||||
expiration_datetime='1970-01-01 05:00:00'
|
||||
)
|
||||
|
||||
# Passes the include_expired parameter to the API call
|
||||
v_response = self.client.get(
|
||||
reverse('courseenrollmentdetails', kwargs={"course_id": unicode(self.course.id)}), {'include_expired': True}
|
||||
)
|
||||
v_data = json.loads(v_response.content)
|
||||
|
||||
# Ensure that both course modes are returned
|
||||
self.assertEqual(len(v_data['course_modes']), 2)
|
||||
|
||||
# Omits the include_expired parameter from the API call
|
||||
h_response = self.client.get(reverse('courseenrollmentdetails', kwargs={"course_id": unicode(self.course.id)}))
|
||||
h_data = json.loads(h_response.content)
|
||||
|
||||
# Ensure that only one course mode is returned and that it is honor
|
||||
self.assertEqual(len(h_data['course_modes']), 1)
|
||||
self.assertEqual(h_data['course_modes'][0]['slug'], CourseMode.HONOR)
|
||||
|
||||
def test_update_enrollment_with_mode(self):
|
||||
"""With the right API key, update an existing enrollment with a new mode. """
|
||||
# Create an honor and verified mode for a course. This allows an update.
|
||||
|
||||
@@ -163,12 +163,17 @@ class EnrollmentCourseDetailView(APIView):
|
||||
|
||||
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.
|
||||
|
||||
**Note:** Getting enrollment details for a course does not require authentication.
|
||||
|
||||
**Example Requests**:
|
||||
|
||||
GET /api/enrollment/v1/course/{course_id}
|
||||
|
||||
GET /api/v1/enrollment/course/{course_id}?include_expired=1
|
||||
|
||||
|
||||
**Response Values**
|
||||
|
||||
@@ -184,7 +189,10 @@ class EnrollmentCourseDetailView(APIView):
|
||||
|
||||
* course_end: The date and time at which the course closes. If null, the course never ends.
|
||||
|
||||
* course_modes: An array of data about the enrollment modes supported for the course. Each enrollment mode collection includes:
|
||||
* course_modes: An array containing details 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:
|
||||
|
||||
* slug: The short name for the enrollment mode.
|
||||
* name: The full name of the enrollment mode.
|
||||
@@ -217,7 +225,7 @@ class EnrollmentCourseDetailView(APIView):
|
||||
|
||||
"""
|
||||
try:
|
||||
return Response(api.get_course_enrollment_details(course_id))
|
||||
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,
|
||||
|
||||
@@ -53,6 +53,7 @@ class CourseModeFactory(DjangoModelFactory):
|
||||
min_price = 0
|
||||
suggested_prices = ''
|
||||
currency = 'usd'
|
||||
expiration_datetime = None
|
||||
|
||||
|
||||
class RegistrationFactory(DjangoModelFactory):
|
||||
|
||||
@@ -6,6 +6,7 @@ import datetime
|
||||
from pytz import UTC
|
||||
from uuid import uuid4
|
||||
from nose.plugins.attrib import attr
|
||||
from flaky import flaky
|
||||
|
||||
from .helpers import BaseDiscussionTestCase
|
||||
from ..helpers import UniqueCourseTest
|
||||
@@ -217,6 +218,7 @@ class DiscussionTabSingleThreadTest(BaseDiscussionTestCase, DiscussionResponsePa
|
||||
self.thread_page = self.create_single_thread_page(thread_id) # pylint: disable=attribute-defined-outside-init
|
||||
self.thread_page.visit()
|
||||
|
||||
@flaky # TODO fix this, see TNL-2419
|
||||
def test_mathjax_rendering(self):
|
||||
thread_id = "test_thread_{}".format(uuid4().hex)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ Acceptance tests for Content Libraries in Studio
|
||||
from ddt import ddt, data
|
||||
from unittest import skip
|
||||
from nose.plugins.attrib import attr
|
||||
from flaky import flaky
|
||||
|
||||
from .base_studio_test import StudioLibraryTest
|
||||
from ...fixtures.course import XBlockFixtureDesc
|
||||
@@ -129,6 +130,7 @@ class LibraryEditPageTest(StudioLibraryTest):
|
||||
"""
|
||||
self.assertFalse(self.browser.find_elements_by_css_selector('span.large-discussion-icon'))
|
||||
|
||||
@flaky # TODO fix this, see TNL-2322
|
||||
def test_library_pagination(self):
|
||||
"""
|
||||
Scenario: Ensure that adding several XBlocks to a library results in pagination.
|
||||
|
||||
Reference in New Issue
Block a user