Commit 767ccbd1 by Tyler Nickerson

Merge pull request #8235 from edx/nickersoft-include-expired

Added optional GET parameter to the enrollment API that includes expired course modes
parents 40b4bc65 8c1d3ae4
...@@ -166,7 +166,7 @@ class CourseMode(models.Model): ...@@ -166,7 +166,7 @@ class CourseMode(models.Model):
return [mode.to_tuple() for mode in found_course_modes] return [mode.to_tuple() for mode in found_course_modes]
@classmethod @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 Returns a list of the non-expired modes for a given course id
...@@ -176,6 +176,9 @@ class CourseMode(models.Model): ...@@ -176,6 +176,9 @@ class CourseMode(models.Model):
course_id (CourseKey): Search for course modes for this course. course_id (CourseKey): Search for course modes for this course.
Keyword Arguments: 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 only_selectable (bool): If True, include only modes that are shown
to users on the track selection page. (Currently, "credit" modes to users on the track selection page. (Currently, "credit" modes
aren't available to users until they complete the course, so aren't available to users until they complete the course, so
...@@ -186,9 +189,14 @@ class CourseMode(models.Model): ...@@ -186,9 +189,14 @@ class CourseMode(models.Model):
""" """
now = datetime.now(pytz.UTC) 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; # Credit course modes are currently not shown on the track selection page;
# they're available only when students complete a course. For this reason, # 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): ...@@ -235,7 +235,7 @@ def update_enrollment(user_id, course_id, mode=None, is_active=None):
return enrollment 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. """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. 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): ...@@ -243,6 +243,9 @@ def get_course_enrollment_details(course_id):
Args: Args:
course_id (str): The Course to get enrollment information for. 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: Returns:
A serializable dictionary of course enrollment information. A serializable dictionary of course enrollment information.
...@@ -270,8 +273,10 @@ def get_course_enrollment_details(course_id): ...@@ -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 cached_enrollment_data = None
try: try:
cached_enrollment_data = cache.get(cache_key) cached_enrollment_data = cache.get(cache_key)
...@@ -283,7 +288,7 @@ def get_course_enrollment_details(course_id): ...@@ -283,7 +288,7 @@ def get_course_enrollment_details(course_id):
log.info(u"Get enrollment data for course %s (cached)", course_id) log.info(u"Get enrollment data for course %s (cached)", course_id)
return cached_enrollment_data 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: try:
cache_time_out = getattr(settings, 'ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT', 60) cache_time_out = getattr(settings, 'ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT', 60)
......
...@@ -142,7 +142,7 @@ def _update_enrollment(enrollment, is_active=None, mode=None): ...@@ -142,7 +142,7 @@ def _update_enrollment(enrollment, is_active=None, mode=None):
return CourseEnrollmentSerializer(enrollment).data # pylint: disable=no-member 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. """Returns all course enrollment information for the given course.
Based on the course id, return all related course information.. Based on the course id, return all related course information..
...@@ -150,6 +150,9 @@ def get_course_enrollment_info(course_id): ...@@ -150,6 +150,9 @@ def get_course_enrollment_info(course_id):
Args: Args:
course_id (str): The course to retrieve enrollment information for. 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: Returns:
A serializable dictionary representing the course's enrollment information. A serializable dictionary representing the course's enrollment information.
...@@ -163,4 +166,4 @@ def get_course_enrollment_info(course_id): ...@@ -163,4 +166,4 @@ def get_course_enrollment_info(course_id):
msg = u"Requested enrollment information for unknown course {course}".format(course=course_id) msg = u"Requested enrollment information for unknown course {course}".format(course=course_id)
log.warning(msg) log.warning(msg)
raise CourseNotFoundError(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): ...@@ -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_id = unicode(course.id)
course_modes = ModeSerializer( 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 ).data # pylint: disable=no-member
return { return {
...@@ -94,7 +94,7 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer): ...@@ -94,7 +94,7 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
"""Retrieves the username from the associated model.""" """Retrieves the username from the associated model."""
return model.username return model.username
class Meta: # pylint: disable=missing-docstring class Meta(object): # pylint: disable=missing-docstring
model = CourseEnrollment model = CourseEnrollment
fields = ('created', 'mode', 'is_active', 'course_details', 'user') fields = ('created', 'mode', 'is_active', 'course_details', 'user')
lookup_field = 'username' lookup_field = 'username'
......
...@@ -46,7 +46,7 @@ def update_course_enrollment(student_id, course_id, mode=None, is_active=None): ...@@ -46,7 +46,7 @@ def update_course_enrollment(student_id, course_id, mode=None, is_active=None):
return enrollment return enrollment
def get_course_enrollment_info(course_id): def get_course_enrollment_info(course_id, include_expired=False):
"""Stubbed out Enrollment data request.""" """Stubbed out Enrollment data request."""
return _get_fake_course_info(course_id) return _get_fake_course_info(course_id)
......
...@@ -476,6 +476,40 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase): ...@@ -476,6 +476,40 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
self.assertTrue(is_active) self.assertTrue(is_active)
self.assertEqual(course_mode, 'professional') 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): def test_update_enrollment_with_mode(self):
"""With the right API key, update an existing enrollment with a new mode. """ """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. # Create an honor and verified mode for a course. This allows an update.
......
...@@ -163,12 +163,17 @@ class EnrollmentCourseDetailView(APIView): ...@@ -163,12 +163,17 @@ class EnrollmentCourseDetailView(APIView):
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.
**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/v1/enrollment/course/{course_id}?include_expired=1
**Response Values** **Response Values**
...@@ -184,7 +189,10 @@ class EnrollmentCourseDetailView(APIView): ...@@ -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_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. * slug: The short name for the enrollment mode.
* name: The full name of the enrollment mode. * name: The full name of the enrollment mode.
...@@ -217,7 +225,7 @@ class EnrollmentCourseDetailView(APIView): ...@@ -217,7 +225,7 @@ class EnrollmentCourseDetailView(APIView):
""" """
try: 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: except CourseNotFoundError:
return Response( return Response(
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
......
...@@ -53,6 +53,7 @@ class CourseModeFactory(DjangoModelFactory): ...@@ -53,6 +53,7 @@ class CourseModeFactory(DjangoModelFactory):
min_price = 0 min_price = 0
suggested_prices = '' suggested_prices = ''
currency = 'usd' currency = 'usd'
expiration_datetime = None
class RegistrationFactory(DjangoModelFactory): class RegistrationFactory(DjangoModelFactory):
......
...@@ -6,6 +6,7 @@ import datetime ...@@ -6,6 +6,7 @@ import datetime
from pytz import UTC from pytz import UTC
from uuid import uuid4 from uuid import uuid4
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from flaky import flaky
from .helpers import BaseDiscussionTestCase from .helpers import BaseDiscussionTestCase
from ..helpers import UniqueCourseTest from ..helpers import UniqueCourseTest
...@@ -217,6 +218,7 @@ class DiscussionTabSingleThreadTest(BaseDiscussionTestCase, DiscussionResponsePa ...@@ -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 = self.create_single_thread_page(thread_id) # pylint: disable=attribute-defined-outside-init
self.thread_page.visit() self.thread_page.visit()
@flaky # TODO fix this, see TNL-2419
def test_mathjax_rendering(self): def test_mathjax_rendering(self):
thread_id = "test_thread_{}".format(uuid4().hex) thread_id = "test_thread_{}".format(uuid4().hex)
......
...@@ -4,6 +4,7 @@ Acceptance tests for Content Libraries in Studio ...@@ -4,6 +4,7 @@ Acceptance tests for Content Libraries in Studio
from ddt import ddt, data from ddt import ddt, data
from unittest import skip from unittest import skip
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from flaky import flaky
from .base_studio_test import StudioLibraryTest from .base_studio_test import StudioLibraryTest
from ...fixtures.course import XBlockFixtureDesc from ...fixtures.course import XBlockFixtureDesc
...@@ -129,6 +130,7 @@ class LibraryEditPageTest(StudioLibraryTest): ...@@ -129,6 +130,7 @@ class LibraryEditPageTest(StudioLibraryTest):
""" """
self.assertFalse(self.browser.find_elements_by_css_selector('span.large-discussion-icon')) 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): def test_library_pagination(self):
""" """
Scenario: Ensure that adding several XBlocks to a library results in pagination. Scenario: Ensure that adding several XBlocks to a library results in pagination.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment