diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py
index 18d5e66968..2c7cd36de5 100644
--- a/lms/djangoapps/courseware/courses.py
+++ b/lms/djangoapps/courseware/courses.py
@@ -22,6 +22,7 @@ from courseware.module_render import get_module
from django.conf import settings
from django.core.urlresolvers import reverse
from django.http import Http404, QueryDict
+from enrollment.api import get_course_enrollment_details
from edxmako.shortcuts import render_to_string
from fs.errors import ResourceNotFoundError
from lms.djangoapps.courseware.courseware_access_exception import CoursewareAccessException
@@ -173,6 +174,30 @@ def can_self_enroll_in_course(course_key):
return True
+def course_open_for_self_enrollment(course_key):
+ """
+ For a given course_key, determine if the course is available for enrollment
+ """
+ # Check to see if learners can enroll themselves.
+ if not can_self_enroll_in_course(course_key):
+ return False
+
+ # Check the enrollment start and end dates.
+ course_details = get_course_enrollment_details(unicode(course_key))
+ now = datetime.now().replace(tzinfo=pytz.UTC)
+ start = course_details['enrollment_start']
+ end = course_details['enrollment_end']
+
+ start = start if start is not None else now
+ end = end if end is not None else now
+
+ # If we are not within the start and end date for enrollment.
+ if now < start or end < now:
+ return False
+
+ return True
+
+
def find_file(filesystem, dirs, filename):
"""
Looks for a filename in a list of dirs on a filesystem, in the specified order.
diff --git a/lms/djangoapps/courseware/tests/test_courses.py b/lms/djangoapps/courseware/tests/test_courses.py
index a8ccccc8c4..f9229dbda5 100644
--- a/lms/djangoapps/courseware/tests/test_courses.py
+++ b/lms/djangoapps/courseware/tests/test_courses.py
@@ -4,8 +4,10 @@ Tests for course access
"""
import itertools
+import datetime
import ddt
import mock
+import pytz
from django.conf import settings
from django.core.urlresolvers import reverse
from django.test.client import RequestFactory
@@ -13,6 +15,7 @@ from django.test.utils import override_settings
from nose.plugins.attrib import attr
from courseware.courses import (
+ course_open_for_self_enrollment,
get_cms_block_link,
get_cms_course_link,
get_course_about_section,
@@ -322,6 +325,52 @@ class CoursesRenderTest(ModuleStoreTestCase):
self.assertIn("this module is temporarily unavailable", course_about)
+class CourseEnrollmentOpenTests(ModuleStoreTestCase):
+ def setUp(self):
+ super(CourseEnrollmentOpenTests, self).setUp()
+ self.now = datetime.datetime.now().replace(tzinfo=pytz.UTC)
+
+ def test_course_enrollment_open(self):
+ start = self.now - datetime.timedelta(days=1)
+ end = self.now + datetime.timedelta(days=1)
+ course = CourseFactory(enrollment_start=start, enrollment_end=end)
+ self.assertTrue(course_open_for_self_enrollment(course.id))
+
+ def test_course_enrollment_closed_future(self):
+ start = self.now + datetime.timedelta(days=1)
+ end = self.now + datetime.timedelta(days=2)
+ course = CourseFactory(enrollment_start=start, enrollment_end=end)
+ self.assertFalse(course_open_for_self_enrollment(course.id))
+
+ def test_course_enrollment_closed_past(self):
+ start = self.now - datetime.timedelta(days=2)
+ end = self.now - datetime.timedelta(days=1)
+ course = CourseFactory(enrollment_start=start, enrollment_end=end)
+ self.assertFalse(course_open_for_self_enrollment(course.id))
+
+ def test_course_enrollment_dates_missing(self):
+ course = CourseFactory()
+ self.assertTrue(course_open_for_self_enrollment(course.id))
+
+ def test_course_enrollment_dates_missing_start(self):
+ end = self.now + datetime.timedelta(days=1)
+ course = CourseFactory(enrollment_end=end)
+ self.assertTrue(course_open_for_self_enrollment(course.id))
+
+ end = self.now - datetime.timedelta(days=1)
+ course = CourseFactory(enrollment_end=end)
+ self.assertFalse(course_open_for_self_enrollment(course.id))
+
+ def test_course_enrollment_dates_missing_end(self):
+ start = self.now - datetime.timedelta(days=1)
+ course = CourseFactory(enrollment_start=start)
+ self.assertTrue(course_open_for_self_enrollment(course.id))
+
+ start = self.now + datetime.timedelta(days=1)
+ course = CourseFactory(enrollment_start=start)
+ self.assertFalse(course_open_for_self_enrollment(course.id))
+
+
@attr(shard=1)
@ddt.ddt
class CourseInstantiationTests(ModuleStoreTestCase):
diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py
index e51675b544..691792de19 100644
--- a/lms/djangoapps/courseware/views/views.py
+++ b/lms/djangoapps/courseware/views/views.py
@@ -19,6 +19,7 @@ from courseware.access import has_access, has_ccx_coach_role
from courseware.access_utils import check_course_open_for_learner
from courseware.courses import (
can_self_enroll_in_course,
+ course_open_for_self_enrollment,
get_course,
get_course_overview_with_access,
get_course_with_access,
@@ -312,6 +313,7 @@ def course_info(request, course_id):
'request': request,
'masquerade_user': user,
'course_id': course_key.to_deprecated_string(),
+ 'url_to_enroll': CourseTabView.url_to_enroll(course_key),
'cache': None,
'course': course,
'staff_access': staff_access,
@@ -321,7 +323,6 @@ def course_info(request, course_id):
'show_enroll_banner': show_enroll_banner,
'user_is_enrolled': user_is_enrolled,
'dates_fragment': dates_fragment,
- 'url_to_enroll': CourseTabView.url_to_enroll(course_key),
'course_tools': course_tools,
}
context.update(
@@ -449,15 +450,22 @@ class CourseTabView(EdxFragmentView):
)
)
elif not is_enrolled and not is_staff:
- PageLevelMessages.register_warning_message(
- request,
- Text(_('You must be enrolled in the course to see course content. {enroll_link}.')).format(
- enroll_link=HTML('{enroll_link_label}').format(
- url_to_enroll=CourseTabView.url_to_enroll(course_key),
- enroll_link_label=_("Enroll now"),
+ # Only show enroll button if course is open for enrollment.
+ if course_open_for_self_enrollment(course_key):
+ enroll_message = _('You must be enrolled in the course to see course content. \
+ {enroll_link_start}Enroll now{enroll_link_end}.')
+ PageLevelMessages.register_warning_message(
+ request,
+ Text(enroll_message).format(
+ enroll_link_start=HTML('')
)
)
- )
+ else:
+ PageLevelMessages.register_warning_message(
+ request,
+ Text(_('You must be enrolled in the course to see course content.'))
+ )
@staticmethod
def handle_exceptions(request, course, exception):
diff --git a/openedx/features/course_experience/static/course_experience/fixtures/enrollment-button.html b/openedx/features/course_experience/static/course_experience/fixtures/enrollment-button.html
new file mode 100644
index 0000000000..1e55c18cb7
--- /dev/null
+++ b/openedx/features/course_experience/static/course_experience/fixtures/enrollment-button.html
@@ -0,0 +1 @@
+
diff --git a/openedx/features/course_experience/static/course_experience/js/Enrollment.js b/openedx/features/course_experience/static/course_experience/js/Enrollment.js
new file mode 100644
index 0000000000..e28fd5e063
--- /dev/null
+++ b/openedx/features/course_experience/static/course_experience/js/Enrollment.js
@@ -0,0 +1,45 @@
+
+/*
+ * Course Enrollment on the Course Home page
+ */
+export class CourseEnrollment { // eslint-disable-line import/prefer-default-export
+ /**
+ * Redirect to a URL. Mainly useful for mocking out in tests.
+ * @param {string} url The URL to redirect to.
+ */
+ static redirect(url) {
+ window.location.href = url;
+ }
+
+ static refresh() {
+ window.location.reload(false);
+ }
+
+ static createEnrollment(courseId) {
+ const data = JSON.stringify({
+ course_details: { course_id: courseId },
+ });
+ const enrollmentAPI = '/api/enrollment/v1/enrollment';
+ const trackSelection = '/course_modes/choose/';
+
+ return () =>
+ $.ajax(
+ {
+ type: 'POST',
+ url: enrollmentAPI,
+ data,
+ contentType: 'application/json',
+ }).done(() => {
+ window.analytics.track('edx.bi.user.course-home.enrollment');
+ CourseEnrollment.refresh();
+ }).fail(() => {
+ // If the simple enrollment we attempted failed, go to the track selection page,
+ // which is better for handling more complex enrollment situations.
+ CourseEnrollment.redirect(trackSelection + courseId);
+ });
+ }
+
+ constructor(buttonClass, courseId) {
+ $(buttonClass).click(CourseEnrollment.createEnrollment(courseId));
+ }
+}
diff --git a/openedx/features/course_experience/static/course_experience/js/spec/Enrollment_spec.js b/openedx/features/course_experience/static/course_experience/js/spec/Enrollment_spec.js
new file mode 100644
index 0000000000..6220736b89
--- /dev/null
+++ b/openedx/features/course_experience/static/course_experience/js/spec/Enrollment_spec.js
@@ -0,0 +1,48 @@
+/* globals $, loadFixtures */
+
+import {
+ expectRequest,
+ requests as mockRequests,
+ respondWithJson,
+ respondWithError,
+} from 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers';
+import { CourseEnrollment } from '../Enrollment';
+
+
+describe('CourseEnrollment tests', () => {
+ describe('Ensure button behavior', () => {
+ const endpointUrl = '/api/enrollment/v1/enrollment';
+ const courseId = 'course-v1:edX+DemoX+Demo_Course';
+ const enrollButtonClass = '.enroll-btn';
+
+ window.analytics = jasmine.createSpyObj('analytics', ['page', 'track', 'trackLink']);
+
+ beforeEach(() => {
+ loadFixtures('course_experience/fixtures/enrollment-button.html');
+ new CourseEnrollment('.enroll-btn', courseId); // eslint-disable-line no-new
+ });
+ it('Verify that we reload on success', () => {
+ const requests = mockRequests(this);
+ $(enrollButtonClass).click();
+ expectRequest(
+ requests,
+ 'POST',
+ endpointUrl,
+ `{"course_details":{"course_id":"${courseId}"}}`,
+ );
+ spyOn(CourseEnrollment, 'refresh');
+ respondWithJson(requests);
+ expect(CourseEnrollment.refresh).toHaveBeenCalled();
+ expect(window.analytics.track).toHaveBeenCalled();
+ requests.restore();
+ });
+ it('Verify that we redirect to track selection on fail', () => {
+ const requests = mockRequests(this);
+ $(enrollButtonClass).click();
+ spyOn(CourseEnrollment, 'redirect');
+ respondWithError(requests, 403);
+ expect(CourseEnrollment.redirect).toHaveBeenCalled();
+ requests.restore();
+ });
+ });
+});
diff --git a/openedx/features/course_experience/templates/course_experience/course-home-fragment.html b/openedx/features/course_experience/templates/course_experience/course-home-fragment.html
index e8fb3d8dd8..93e38c1670 100644
--- a/openedx/features/course_experience/templates/course_experience/course-home-fragment.html
+++ b/openedx/features/course_experience/templates/course_experience/course-home-fragment.html
@@ -112,3 +112,7 @@ from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, SHOW_REV
courseToolLink: ".course-tool-link",
});
%static:webpack>
+
+<%static:webpack entry="Enrollment">
+ new CourseEnrollment('.enroll-btn', '${course_key | n, js_escaped_string}');
+%static:webpack>
diff --git a/webpack.config.js b/webpack.config.js
index b34e53ce7b..9ba5257aea 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -23,6 +23,7 @@ var wpconfig = {
CourseSock: './openedx/features/course_experience/static/course_experience/js/CourseSock.js',
CourseTalkReviews: './openedx/features/course_experience/static/course_experience/js/CourseTalkReviews.js',
WelcomeMessage: './openedx/features/course_experience/static/course_experience/js/WelcomeMessage.js',
+ Enrollment: './openedx/features/course_experience/static/course_experience/js/Enrollment.js',
Import: './cms/static/js/features/import/factories/import.js'
},