Add in-place enrollment to course home page.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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('<a href="{url_to_enroll}">{enroll_link_label}</a>').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('<button class="enroll-btn btn-link">'),
|
||||
enroll_link_end=HTML('</button>')
|
||||
)
|
||||
)
|
||||
)
|
||||
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):
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<button class="enroll-btn btn-link">Enroll Now</button>
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user