From fc918fb3d12061fce2489d063b1390f0a35e7f82 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 15 Apr 2015 13:15:23 -0400 Subject: [PATCH 1/2] Expose course start/end date in the enrollment API --- common/djangoapps/enrollment/api.py | 36 ++++++++++------ common/djangoapps/enrollment/serializers.py | 2 + .../djangoapps/enrollment/tests/test_views.py | 41 +++++++++++++++++++ common/djangoapps/enrollment/views.py | 29 +++++++++---- 4 files changed, 88 insertions(+), 20 deletions(-) diff --git a/common/djangoapps/enrollment/api.py b/common/djangoapps/enrollment/api.py index 24d058b9cc..02055ccea5 100644 --- a/common/djangoapps/enrollment/api.py +++ b/common/djangoapps/enrollment/api.py @@ -36,7 +36,10 @@ def get_enrollments(user_id): "user": "Bob", "course": { "course_id": "edX/DemoX/2014T2", - "enrollment_end": 2014-12-20T20:18:00Z, + "enrollment_end": "2014-12-20T20:18:00Z", + "enrollment_start": "2014-10-15T20:18:00Z", + "course_start": "2015-02-03T00:00:00Z", + "course_end": "2015-05-06T00:00:00Z", "course_modes": [ { "slug": "honor", @@ -49,7 +52,6 @@ def get_enrollments(user_id): "sku": null } ], - "enrollment_start": 2014-10-15T20:18:00Z, "invite_only": False } }, @@ -60,7 +62,10 @@ def get_enrollments(user_id): "user": "Bob", "course": { "course_id": "edX/edX-Insider/2014T2", - "enrollment_end": 2014-12-20T20:18:00Z, + "enrollment_end": "2014-12-20T20:18:00Z", + "enrollment_start": "2014-10-15T20:18:00Z", + "course_start": "2015-02-03T00:00:00Z", + "course_end": "2015-05-06T00:00:00Z", "course_modes": [ { "slug": "honor", @@ -73,7 +78,6 @@ def get_enrollments(user_id): "sku": null } ], - "enrollment_start": 2014-10-15T20:18:00Z, "invite_only": True } } @@ -104,7 +108,10 @@ def get_enrollment(user_id, course_id): "user": "Bob", "course": { "course_id": "edX/DemoX/2014T2", - "enrollment_end": 2014-12-20T20:18:00Z, + "enrollment_end": "2014-12-20T20:18:00Z", + "enrollment_start": "2014-10-15T20:18:00Z", + "course_start": "2015-02-03T00:00:00Z", + "course_end": "2015-05-06T00:00:00Z", "course_modes": [ { "slug": "honor", @@ -117,7 +124,6 @@ def get_enrollment(user_id, course_id): "sku": null } ], - "enrollment_start": 2014-10-15T20:18:00Z, "invite_only": False } } @@ -151,7 +157,10 @@ def add_enrollment(user_id, course_id, mode='honor', is_active=True): "user": "Bob", "course": { "course_id": "edX/DemoX/2014T2", - "enrollment_end": 2014-12-20T20:18:00Z, + "enrollment_end": "2014-12-20T20:18:00Z", + "enrollment_start": "2014-10-15T20:18:00Z", + "course_start": "2015-02-03T00:00:00Z", + "course_end": "2015-05-06T00:00:00Z", "course_modes": [ { "slug": "honor", @@ -164,7 +173,6 @@ def add_enrollment(user_id, course_id, mode='honor', is_active=True): "sku": null } ], - "enrollment_start": 2014-10-15T20:18:00Z, "invite_only": False } } @@ -196,7 +204,10 @@ def update_enrollment(user_id, course_id, mode=None, is_active=None): "user": "Bob", "course": { "course_id": "edX/DemoX/2014T2", - "enrollment_end": 2014-12-20T20:18:00Z, + "enrollment_end": "2014-12-20T20:18:00Z", + "enrollment_start": "2014-10-15T20:18:00Z", + "course_start": "2015-02-03T00:00:00Z", + "course_end": "2015-05-06T00:00:00Z", "course_modes": [ { "slug": "honor", @@ -209,7 +220,6 @@ def update_enrollment(user_id, course_id, mode=None, is_active=None): "sku": null } ], - "enrollment_start": 2014-10-15T20:18:00Z, "invite_only": False } } @@ -239,7 +249,10 @@ def get_course_enrollment_details(course_id): >>> get_course_enrollment_details("edX/DemoX/2014T2") { "course_id": "edX/DemoX/2014T2", - "enrollment_end": 2014-12-20T20:18:00Z, + "enrollment_end": "2014-12-20T20:18:00Z", + "enrollment_start": "2014-10-15T20:18:00Z", + "course_start": "2015-02-03T00:00:00Z", + "course_end": "2015-05-06T00:00:00Z", "course_modes": [ { "slug": "honor", @@ -252,7 +265,6 @@ def get_course_enrollment_details(course_id): "sku": null } ], - "enrollment_start": 2014-10-15T20:18:00Z, "invite_only": False } diff --git a/common/djangoapps/enrollment/serializers.py b/common/djangoapps/enrollment/serializers.py index e93e4208d9..4f9d78bf63 100644 --- a/common/djangoapps/enrollment/serializers.py +++ b/common/djangoapps/enrollment/serializers.py @@ -46,6 +46,8 @@ class CourseField(serializers.RelatedField): "course_id": course_id, "enrollment_start": course.enrollment_start, "enrollment_end": course.enrollment_end, + "course_start": course.start, + "course_end": course.end, "invite_only": course.invitation_only, "course_modes": course_modes, } diff --git a/common/djangoapps/enrollment/tests/test_views.py b/common/djangoapps/enrollment/tests/test_views.py index 25e1ed2f63..5e76626e0b 100644 --- a/common/djangoapps/enrollment/tests/test_views.py +++ b/common/djangoapps/enrollment/tests/test_views.py @@ -3,6 +3,7 @@ Tests for user enrollment. """ import json import unittest +import datetime import ddt from django.core.cache import cache @@ -305,6 +306,46 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase): self.assertEqual(mode['sku'], '123') self.assertEqual(mode['name'], CourseMode.HONOR) + @ddt.data( + # NOTE: Studio requires a start date, but this is not + # enforced at the data layer, so we need to handle the case + # in which no dates are specified. + (None, None, None, None), + (datetime.datetime(2015, 1, 2, 3, 4, 5), None, "2015-01-02T03:04:05Z", None), + (None, datetime.datetime(2015, 1, 2, 3, 4, 5), None, "2015-01-02T03:04:05Z"), + (datetime.datetime(2014, 6, 7, 8, 9, 10), datetime.datetime(2015, 1, 2, 3, 4, 5), "2014-06-07T08:09:10Z", "2015-01-02T03:04:05Z"), + ) + @ddt.unpack + def test_get_course_details_course_dates(self, start_datetime, end_datetime, expected_start, expected_end): + course = CourseFactory.create(start=start_datetime, end=end_datetime) + self.assert_enrollment_status(course_id=unicode(course.id)) + + # Check course details + url = reverse('courseenrollmentdetails', kwargs={"course_id": unicode(course.id)}) + resp = self.client.get(url) + self.assertEqual(resp.status_code, status.HTTP_200_OK) + + data = json.loads(resp.content) + self.assertEqual(data['course_start'], expected_start) + self.assertEqual(data['course_end'], expected_end) + + # Check enrollment course details + url = reverse('courseenrollment', kwargs={"course_id": unicode(course.id)}) + resp = self.client.get(url) + self.assertEqual(resp.status_code, status.HTTP_200_OK) + + data = json.loads(resp.content) + self.assertEqual(data['course_details']['course_start'], expected_start) + self.assertEqual(data['course_details']['course_end'], expected_end) + + # Check enrollment list course details + resp = self.client.get(reverse('courseenrollments')) + self.assertEqual(resp.status_code, status.HTTP_200_OK) + + data = json.loads(resp.content) + self.assertEqual(data[0]['course_details']['course_start'], expected_start) + self.assertEqual(data[0]['course_details']['course_end'], expected_end) + def test_with_invalid_course_id(self): self.assert_enrollment_status(course_id='entirely/fake/course', expected_status=status.HTTP_400_BAD_REQUEST) diff --git a/common/djangoapps/enrollment/views.py b/common/djangoapps/enrollment/views.py index 6ec535bae1..406c4a6b36 100644 --- a/common/djangoapps/enrollment/views.py +++ b/common/djangoapps/enrollment/views.py @@ -86,7 +86,13 @@ class EnrollmentView(APIView, ApiKeyPermissionMixIn): * course_id: The unique identifier for the course. - * enrollment_end: The date and time after which users cannot enroll for the course. + * enrollment_start: The date and time that users can begin enrolling in the course. If null, enrollment opens immediately when the course is created. + + * enrollment_end: The date and time after which users cannot enroll for the course. If null, the enrollment period never ends. + + * course_start: The date and time at which the course opens. If null, the course opens immediately when created. + + * 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: @@ -98,8 +104,6 @@ class EnrollmentView(APIView, ApiKeyPermissionMixIn): * expiration_datetime: The date and time after which users cannot enroll in the course in this mode. * description: A description of this mode. - * enrollment_start: The date and time that users can begin enrolling in the course. - * invite_only: Whether students must be invited to enroll in the course; true or false. * user: The ID of the user. @@ -170,7 +174,13 @@ class EnrollmentCourseDetailView(APIView): * course_id: The unique identifier of the course. - * enrollment_end: The date and time after which users cannot enroll for the course. + * enrollment_start: The date and time that users can begin enrolling in the course. If null, enrollment opens immediately when the course is created. + + * enrollment_end: The date and time after which users cannot enroll for the course. If null, the enrollment period never ends. + + * course_start: The date and time at which the course opens. If null, the course opens immediately when created. + + * 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: @@ -182,8 +192,6 @@ class EnrollmentCourseDetailView(APIView): * expiration_datetime: The date and time after which users cannot enroll in the course in this mode. * description: A description of this mode. - * enrollment_start: The date and time that users can begin enrolling in the course. - * invite_only: Whether students must be invited to enroll in the course; true or false. """ @@ -270,7 +278,13 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn): * course_id: The unique identifier for the course. - * enrollment_end: The date and time after which users cannot enroll for the course. + * enrollment_start: The date and time that users can begin enrolling in the course. If null, enrollment opens immediately when the course is created. + + * enrollment_end: The date and time after which users cannot enroll for the course. If null, the enrollment period never ends. + + * course_start: The date and time at which the course opens. If null, the course opens immediately when created. + + * 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: @@ -282,7 +296,6 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn): * expiration_datetime: The date and time after which users cannot enroll in the course in this mode. * description: A description of this mode. - * enrollment_start: The date and time that users can begin enrolling in the course. * invite_only: Whether students must be invited to enroll in the course; true or false. From e065a9c94796c528cb91fce979c050f7467272d7 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Tue, 21 Apr 2015 09:38:53 -0400 Subject: [PATCH 2/2] Mark test_split_test_LMS_staff_view as flaky --- common/test/acceptance/tests/studio/test_studio_split_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/test/acceptance/tests/studio/test_studio_split_test.py b/common/test/acceptance/tests/studio/test_studio_split_test.py index 18d10c3390..5068863781 100644 --- a/common/test/acceptance/tests/studio/test_studio_split_test.py +++ b/common/test/acceptance/tests/studio/test_studio_split_test.py @@ -8,6 +8,8 @@ from unittest import skip from nose.plugins.attrib import attr from selenium.webdriver.support.ui import Select +from flaky import flaky + from xmodule.partitions.partitions import Group from bok_choy.promise import Promise, EmptyPromise @@ -1044,6 +1046,7 @@ class GroupConfigurationsTest(ContainerBase, SplitTestMixin): rendered_group_names = self.get_select_options(page=courseware_page, selector=".split-test-select") self.assertListEqual(group_names, rendered_group_names) + @flaky # TODO fix this, see TNL-2035 def test_split_test_LMS_staff_view(self): """ Scenario: Ensure that split test is correctly rendered in LMS staff mode as it is