Merge pull request #5502 from edx/will/per-course-donation-button
Add donation button to the enrollment success message
This commit is contained in:
26
common/djangoapps/config_models/decorators.py
Normal file
26
common/djangoapps/config_models/decorators.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Decorators for model-based configuration. """
|
||||
from functools import wraps
|
||||
from django.http import HttpResponseNotFound
|
||||
|
||||
|
||||
def require_config(config_model):
|
||||
"""View decorator that enables/disables a view based on configuration.
|
||||
|
||||
Arguments:
|
||||
config_model (ConfigurationModel subclass): The class of the configuration
|
||||
model to check.
|
||||
|
||||
Returns:
|
||||
HttpResponse: 404 if the configuration model is disabled,
|
||||
otherwise returns the response from the decorated view.
|
||||
|
||||
"""
|
||||
def _decorator(func):
|
||||
@wraps(func)
|
||||
def _inner(*args, **kwargs):
|
||||
if not config_model.current().enabled:
|
||||
return HttpResponseNotFound()
|
||||
else:
|
||||
return func(*args, **kwargs)
|
||||
return _inner
|
||||
return _decorator
|
||||
@@ -59,6 +59,9 @@ class CourseMode(models.Model):
|
||||
DEFAULT_MODE = Mode('honor', _('Honor Code Certificate'), 0, '', 'usd', None, None)
|
||||
DEFAULT_MODE_SLUG = 'honor'
|
||||
|
||||
# Modes that allow a student to pursue a verified certificate
|
||||
VERIFIED_MODES = ["verified", "professional"]
|
||||
|
||||
class Meta:
|
||||
""" meta attributes of this model """
|
||||
unique_together = ('course_id', 'mode_slug', 'currency')
|
||||
@@ -127,6 +130,22 @@ class CourseMode(models.Model):
|
||||
# we prefer professional over verify
|
||||
return professional_mode if professional_mode else verified_mode
|
||||
|
||||
@classmethod
|
||||
def has_verified_mode(cls, course_mode_dict):
|
||||
"""Check whether the modes for a course allow a student to pursue a verfied certificate.
|
||||
|
||||
Args:
|
||||
course_mode_dict (dictionary mapping course mode slugs to Modes)
|
||||
|
||||
Returns:
|
||||
bool: True iff the course modes contain a verified track.
|
||||
|
||||
"""
|
||||
for mode in cls.VERIFIED_MODES:
|
||||
if mode in course_mode_dict:
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def min_course_price_for_verified_for_currency(cls, course_id, currency):
|
||||
"""
|
||||
|
||||
@@ -8,24 +8,32 @@ from django.test import Client
|
||||
from opaque_keys.edx import locator
|
||||
from pytz import UTC
|
||||
import unittest
|
||||
import ddt
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from student.models import CourseEnrollment, DashboardConfiguration
|
||||
from student.views import get_course_enrollment_pairs, _get_recently_enrolled_courses
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@ddt.ddt
|
||||
class TestRecentEnrollments(ModuleStoreTestCase):
|
||||
"""
|
||||
Unit tests for getting the list of courses for a logged in user
|
||||
"""
|
||||
PASSWORD = 'test'
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Add a student
|
||||
"""
|
||||
super(TestRecentEnrollments, self).setUp()
|
||||
self.student = UserFactory()
|
||||
self.student.set_password(self.PASSWORD)
|
||||
self.student.save()
|
||||
|
||||
# Old Course
|
||||
old_course_location = locator.CourseLocator('Org0', 'Course0', 'Run0')
|
||||
@@ -35,7 +43,7 @@ class TestRecentEnrollments(ModuleStoreTestCase):
|
||||
|
||||
# New Course
|
||||
course_location = locator.CourseLocator('Org1', 'Course1', 'Run1')
|
||||
self._create_course_and_enrollment(course_location)
|
||||
self.course, _ = self._create_course_and_enrollment(course_location)
|
||||
|
||||
def _create_course_and_enrollment(self, course_location):
|
||||
""" Creates a course and associated enrollment. """
|
||||
@@ -47,12 +55,17 @@ class TestRecentEnrollments(ModuleStoreTestCase):
|
||||
enrollment = CourseEnrollment.enroll(self.student, course.id)
|
||||
return course, enrollment
|
||||
|
||||
def _configure_message_timeout(self, timeout):
|
||||
"""Configure the amount of time the enrollment message will be displayed. """
|
||||
config = DashboardConfiguration(recent_enrollment_time_delta=timeout)
|
||||
config.save()
|
||||
|
||||
def test_recently_enrolled_courses(self):
|
||||
"""
|
||||
Test if the function for filtering recent enrollments works appropriately.
|
||||
"""
|
||||
config = DashboardConfiguration(recent_enrollment_time_delta=60)
|
||||
config.save()
|
||||
self._configure_message_timeout(60)
|
||||
|
||||
# get courses through iterating all courses
|
||||
courses_list = list(get_course_enrollment_pairs(self.student, None, []))
|
||||
self.assertEqual(len(courses_list), 2)
|
||||
@@ -64,8 +77,7 @@ class TestRecentEnrollments(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests that the recent enrollment list is empty if configured to zero seconds.
|
||||
"""
|
||||
config = DashboardConfiguration(recent_enrollment_time_delta=0)
|
||||
config.save()
|
||||
self._configure_message_timeout(0)
|
||||
courses_list = list(get_course_enrollment_pairs(self.student, None, []))
|
||||
self.assertEqual(len(courses_list), 2)
|
||||
|
||||
@@ -78,30 +90,21 @@ class TestRecentEnrollments(ModuleStoreTestCase):
|
||||
recent enrollments first.
|
||||
|
||||
"""
|
||||
config = DashboardConfiguration(recent_enrollment_time_delta=600)
|
||||
config.save()
|
||||
self._configure_message_timeout(600)
|
||||
|
||||
# Create a number of new enrollments and courses, and force their creation behind
|
||||
# the first enrollment
|
||||
course_location = locator.CourseLocator('Org2', 'Course2', 'Run2')
|
||||
_, enrollment2 = self._create_course_and_enrollment(course_location)
|
||||
enrollment2.created = datetime.datetime.now(UTC) - datetime.timedelta(seconds=5)
|
||||
enrollment2.save()
|
||||
|
||||
course_location = locator.CourseLocator('Org3', 'Course3', 'Run3')
|
||||
_, enrollment3 = self._create_course_and_enrollment(course_location)
|
||||
enrollment3.created = datetime.datetime.now(UTC) - datetime.timedelta(seconds=10)
|
||||
enrollment3.save()
|
||||
|
||||
course_location = locator.CourseLocator('Org4', 'Course4', 'Run4')
|
||||
_, enrollment4 = self._create_course_and_enrollment(course_location)
|
||||
enrollment4.created = datetime.datetime.now(UTC) - datetime.timedelta(seconds=15)
|
||||
enrollment4.save()
|
||||
|
||||
course_location = locator.CourseLocator('Org5', 'Course5', 'Run5')
|
||||
_, enrollment5 = self._create_course_and_enrollment(course_location)
|
||||
enrollment5.created = datetime.datetime.now(UTC) - datetime.timedelta(seconds=20)
|
||||
enrollment5.save()
|
||||
courses = []
|
||||
for idx, seconds_past in zip(range(2, 6), [5, 10, 15, 20]):
|
||||
course_location = locator.CourseLocator(
|
||||
'Org{num}'.format(num=idx),
|
||||
'Course{num}'.format(num=idx),
|
||||
'Run{num}'.format(num=idx)
|
||||
)
|
||||
course, enrollment = self._create_course_and_enrollment(course_location)
|
||||
enrollment.created = datetime.datetime.now(UTC) - datetime.timedelta(seconds=seconds_past)
|
||||
enrollment.save()
|
||||
courses.append(course)
|
||||
|
||||
courses_list = list(get_course_enrollment_pairs(self.student, None, []))
|
||||
self.assertEqual(len(courses_list), 6)
|
||||
@@ -109,19 +112,42 @@ class TestRecentEnrollments(ModuleStoreTestCase):
|
||||
recent_course_list = _get_recently_enrolled_courses(courses_list)
|
||||
self.assertEqual(len(recent_course_list), 5)
|
||||
|
||||
self.assertEqual(recent_course_list[1][1], enrollment2)
|
||||
self.assertEqual(recent_course_list[2][1], enrollment3)
|
||||
self.assertEqual(recent_course_list[3][1], enrollment4)
|
||||
self.assertEqual(recent_course_list[4][1], enrollment5)
|
||||
self.assertEqual(recent_course_list[1], courses[0])
|
||||
self.assertEqual(recent_course_list[2], courses[1])
|
||||
self.assertEqual(recent_course_list[3], courses[2])
|
||||
self.assertEqual(recent_course_list[4], courses[3])
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
def test_dashboard_rendering(self):
|
||||
"""
|
||||
Tests that the dashboard renders the recent enrollment messages appropriately.
|
||||
"""
|
||||
config = DashboardConfiguration(recent_enrollment_time_delta=600)
|
||||
config.save()
|
||||
self.client = Client()
|
||||
self.client.login(username=self.student.username, password='test')
|
||||
self._configure_message_timeout(600)
|
||||
self.client.login(username=self.student.username, password=self.PASSWORD)
|
||||
response = self.client.get(reverse("dashboard"))
|
||||
self.assertContains(response, "You have successfully enrolled in")
|
||||
|
||||
@ddt.data(
|
||||
(['audit', 'honor', 'verified'], False),
|
||||
(['professional'], False),
|
||||
(['verified'], False),
|
||||
(['audit'], True),
|
||||
(['honor'], True),
|
||||
([], True)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_donate_button(self, course_modes, show_donate):
|
||||
# Enable the enrollment success message
|
||||
self._configure_message_timeout(10000)
|
||||
|
||||
# Create the course mode(s)
|
||||
for mode in course_modes:
|
||||
CourseModeFactory(mode_slug=mode, course_id=self.course.id)
|
||||
|
||||
# Check that the donate button is or is not displayed
|
||||
self.client.login(username=self.student.username, password=self.PASSWORD)
|
||||
response = self.client.get(reverse("dashboard"))
|
||||
|
||||
if show_donate:
|
||||
self.assertContains(response, "donate-container")
|
||||
else:
|
||||
self.assertNotContains(response, "donate-container")
|
||||
|
||||
@@ -415,7 +415,7 @@ def register_user(request, extra_context=None):
|
||||
return render_to_response('register.html', context)
|
||||
|
||||
|
||||
def complete_course_mode_info(course_id, enrollment):
|
||||
def complete_course_mode_info(course_id, enrollment, modes=None):
|
||||
"""
|
||||
We would like to compute some more information from the given course modes
|
||||
and the user's current enrollment
|
||||
@@ -424,7 +424,9 @@ def complete_course_mode_info(course_id, enrollment):
|
||||
- whether to show the course upsell information
|
||||
- numbers of days until they can't upsell anymore
|
||||
"""
|
||||
modes = CourseMode.modes_for_course_dict(course_id)
|
||||
if modes is None:
|
||||
modes = CourseMode.modes_for_course_dict(course_id)
|
||||
|
||||
mode_info = {'show_upsell': False, 'days_for_upsell': None}
|
||||
# we want to know if the user is already verified and if verified is an
|
||||
# option
|
||||
@@ -475,9 +477,17 @@ def dashboard(request):
|
||||
# enrollments, because it could have been a data push snafu.
|
||||
course_enrollment_pairs = list(get_course_enrollment_pairs(user, course_org_filter, org_filter_out_set))
|
||||
|
||||
# Check to see if the student has recently enrolled in a course. If so, display a notification message confirming
|
||||
# the enrollment.
|
||||
enrollment_message = _create_recent_enrollment_message(course_enrollment_pairs)
|
||||
# Retrieve the course modes for each course
|
||||
course_modes_by_course = {
|
||||
course.id: CourseMode.modes_for_course_dict(course.id)
|
||||
for course, __ in course_enrollment_pairs
|
||||
}
|
||||
|
||||
# Check to see if the student has recently enrolled in a course.
|
||||
# If so, display a notification message confirming the enrollment.
|
||||
enrollment_message = _create_recent_enrollment_message(
|
||||
course_enrollment_pairs, course_modes_by_course
|
||||
)
|
||||
|
||||
course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True)
|
||||
|
||||
@@ -499,8 +509,21 @@ def dashboard(request):
|
||||
show_courseware_links_for = frozenset(course.id for course, _enrollment in course_enrollment_pairs
|
||||
if has_access(request.user, 'load', course))
|
||||
|
||||
course_modes = {course.id: complete_course_mode_info(course.id, enrollment) for course, enrollment in course_enrollment_pairs}
|
||||
cert_statuses = {course.id: cert_info(request.user, course) for course, _enrollment in course_enrollment_pairs}
|
||||
# Construct a dictionary of course mode information
|
||||
# used to render the course list. We re-use the course modes dict
|
||||
# we loaded earlier to avoid hitting the database.
|
||||
course_mode_info = {
|
||||
course.id: complete_course_mode_info(
|
||||
course.id, enrollment,
|
||||
modes=course_modes_by_course[course.id]
|
||||
)
|
||||
for course, enrollment in course_enrollment_pairs
|
||||
}
|
||||
|
||||
cert_statuses = {
|
||||
course.id: cert_info(request.user, course)
|
||||
for course, _enrollment in course_enrollment_pairs
|
||||
}
|
||||
|
||||
# only show email settings for Mongo course and when bulk email is turned on
|
||||
show_email_settings_for = frozenset(
|
||||
@@ -570,7 +593,7 @@ def dashboard(request):
|
||||
'staff_access': staff_access,
|
||||
'errored_courses': errored_courses,
|
||||
'show_courseware_links_for': show_courseware_links_for,
|
||||
'all_course_modes': course_modes,
|
||||
'all_course_modes': course_mode_info,
|
||||
'cert_statuses': cert_statuses,
|
||||
'show_email_settings_for': show_email_settings_for,
|
||||
'reverifications': reverifications,
|
||||
@@ -598,23 +621,35 @@ def dashboard(request):
|
||||
return render_to_response('dashboard.html', context)
|
||||
|
||||
|
||||
def _create_recent_enrollment_message(course_enrollment_pairs):
|
||||
def _create_recent_enrollment_message(course_enrollment_pairs, course_modes):
|
||||
"""Builds a recent course enrollment message
|
||||
|
||||
Constructs a new message template based on any recent course enrollments for the student.
|
||||
|
||||
Args:
|
||||
course_enrollment_pairs (list): A list of tuples containing courses, and the associated enrollment information.
|
||||
course_modes (dict): Mapping of course ID's to course mode dictionaries.
|
||||
|
||||
Returns:
|
||||
A string representing the HTML message output from the message template.
|
||||
None if there are no recently enrolled courses.
|
||||
|
||||
"""
|
||||
recent_course_enrollment_pairs = _get_recently_enrolled_courses(course_enrollment_pairs)
|
||||
if recent_course_enrollment_pairs:
|
||||
recently_enrolled_courses = _get_recently_enrolled_courses(course_enrollment_pairs)
|
||||
|
||||
if recently_enrolled_courses:
|
||||
messages = [
|
||||
{
|
||||
"course_id": course.id,
|
||||
"course_name": course.display_name,
|
||||
"allow_donation": not CourseMode.has_verified_mode(course_modes[course.id])
|
||||
}
|
||||
for course in recently_enrolled_courses
|
||||
]
|
||||
|
||||
return render_to_string(
|
||||
'enrollment/course_enrollment_message.html',
|
||||
{'recent_course_enrollment_pairs': recent_course_enrollment_pairs,}
|
||||
{'course_enrollment_messages': messages}
|
||||
)
|
||||
|
||||
|
||||
@@ -627,14 +662,14 @@ def _get_recently_enrolled_courses(course_enrollment_pairs):
|
||||
course_enrollment_pairs (list): A list of tuples containing courses, and the associated enrollment information.
|
||||
|
||||
Returns:
|
||||
A list of tuples for the course and enrollment.
|
||||
A list of courses
|
||||
|
||||
"""
|
||||
seconds = DashboardConfiguration.current().recent_enrollment_time_delta
|
||||
sorted_list = sorted(course_enrollment_pairs, key=lambda created: created[1].created, reverse=True)
|
||||
time_delta = (datetime.datetime.now(UTC) - datetime.timedelta(seconds=seconds))
|
||||
return [
|
||||
(course, enrollment) for course, enrollment in sorted_list
|
||||
course for course, enrollment in sorted_list
|
||||
# If the enrollment has no created date, we are explicitly excluding the course
|
||||
# from the list of recent enrollments.
|
||||
if enrollment.is_active and enrollment.created > time_delta
|
||||
|
||||
Reference in New Issue
Block a user