Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add donation button to the enrollment success message #5502

Merged
merged 1 commit into from
Oct 8, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions common/djangoapps/config_models/decorators.py
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions common/djangoapps/course_modes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -127,6 +130,22 @@ def verified_mode_for_course(cls, course_id):
# 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):
"""
Expand Down
96 changes: 61 additions & 35 deletions common/djangoapps/student/tests/test_recent_enrollments.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,32 @@
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')
Expand All @@ -35,7 +43,7 @@ def setUp(self):

# 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. """
Expand All @@ -47,12 +55,17 @@ def _create_course_and_enrollment(self, course_location):
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)
Expand All @@ -64,8 +77,7 @@ def test_zero_second_delta(self):
"""
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)

Expand All @@ -78,50 +90,64 @@ def test_enrollments_sorted_most_recent(self):
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)

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")
63 changes: 49 additions & 14 deletions common/djangoapps/student/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,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
Expand All @@ -421,7 +421,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
Expand Down Expand Up @@ -472,9 +474,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)

Expand All @@ -496,8 +506,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(
Expand Down Expand Up @@ -567,7 +590,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,
Expand Down Expand Up @@ -595,23 +618,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}
)


Expand All @@ -624,14 +659,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
Expand Down
5 changes: 4 additions & 1 deletion lms/djangoapps/shoppingcart/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
Allows django admin site to add PaidCourseRegistrationAnnotations
"""
from ratelimitbackend import admin
from shoppingcart.models import PaidCourseRegistrationAnnotation, Coupon
from shoppingcart.models import (
PaidCourseRegistrationAnnotation, Coupon, DonationConfiguration
)


class SoftDeleteCouponAdmin(admin.ModelAdmin):
Expand Down Expand Up @@ -49,3 +51,4 @@ def delete_model(self, request, obj):

admin.site.register(PaidCourseRegistrationAnnotation)
admin.site.register(Coupon, SoftDeleteCouponAdmin)
admin.site.register(DonationConfiguration)
Loading