Skip to content

Commit

Permalink
Add donation end-point
Browse files Browse the repository at this point in the history
Make donations configurable

Added donation button to dashboard

Generalize merchant defined data for payment processor
  • Loading branch information
Will Daly committed Oct 7, 2014
1 parent 743326d commit f8365a2
Show file tree
Hide file tree
Showing 22 changed files with 1,101 additions and 105 deletions.
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

0 comments on commit f8365a2

Please sign in to comment.