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):
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.
......
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