Stanford paid course registration

With tests, some settings changes
(all should default to not breaking anything for edx)

Added styling for shopping cart User Experience
- Styled shoppingcart list page
- Styled navigation shopping cart button
- Styled receipt page
- Styled course about page for shopping cart courses

Addressed HTML/SCSS issues

Remove offending body class and unnecessary sass changes

Addresses many review comments on stanford shopping cart

* framework for generating order instructions on receipt page
  in shoppingcart.models
* move user_cart_has_item into shoppingcart.models
* move min_course_price_for_currency into course_modes.models
* remove auto activation on purchase
* 2-space indents in templates
* etc

revert indentation on navigation.html for ease of review

pep8 pylint

move logging/error handling from shoppingcart view to model

Addressing @dave changes
This commit is contained in:
Jason Bau
2013-09-23 22:05:53 -07:00
parent 78829a37b5
commit 667a1eadb5
23 changed files with 693 additions and 151 deletions

View File

@@ -37,7 +37,7 @@ class CourseMode(models.Model):
currency = models.CharField(default="usd", max_length=8)
# turn this mode off after the given expiration date
expiration_date = models.DateField(default=None, null=True)
expiration_date = models.DateField(default=None, null=True, blank=True)
DEFAULT_MODE = Mode('honor', _('Honor Code Certificate'), 0, '', 'usd')
DEFAULT_MODE_SLUG = 'honor'
@@ -86,6 +86,15 @@ class CourseMode(models.Model):
else:
return None
@classmethod
def min_course_price_for_currency(cls, course_id, currency):
"""
Returns the minimum price of the course in the appropriate currency over all the course's modes.
If there is no mode found, will return the price of DEFAULT_MODE, which is 0
"""
modes = cls.modes_for_course(course_id)
return min(mode.min_price for mode in modes if mode.currency == currency)
def __unicode__(self):
return u"{} : {}, min={}, prices={}".format(
self.course_id, self.mode_slug, self.min_price, self.suggested_prices

View File

@@ -73,6 +73,24 @@ class CourseModeModelTest(TestCase):
self.assertEqual(mode2, CourseMode.mode_for_course(self.course_id, u'verified'))
self.assertIsNone(CourseMode.mode_for_course(self.course_id, 'DNE'))
def test_min_course_price_for_currency(self):
"""
Get the min course price for a course according to currency
"""
# no modes, should get 0
self.assertEqual(0, CourseMode.min_course_price_for_currency(self.course_id, 'usd'))
# create some modes
mode1 = Mode(u'honor', u'Honor Code Certificate', 10, '', 'usd')
mode2 = Mode(u'verified', u'Verified Certificate', 20, '', 'usd')
mode3 = Mode(u'honor', u'Honor Code Certificate', 80, '', 'cny')
set_modes = [mode1, mode2, mode3]
for mode in set_modes:
self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices, mode.currency)
self.assertEqual(10, CourseMode.min_course_price_for_currency(self.course_id, 'usd'))
self.assertEqual(80, CourseMode.min_course_price_for_currency(self.course_id, 'cny'))
def test_modes_for_course_expired(self):
expired_mode, _status = self.create_mode('verified', 'Verified Certificate')
expired_mode.expiration_date = datetime.now(pytz.UTC) + timedelta(days=-1)

View File

@@ -11,20 +11,29 @@ import unittest
from django.conf import settings
from django.test import TestCase
from django.test.utils import override_settings
from django.test.client import RequestFactory
from django.contrib.auth.models import User
from django.contrib.auth.hashers import UNUSABLE_PASSWORD
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import int_to_base36
from django.core.urlresolvers import reverse
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from courseware.tests.tests import TEST_DATA_MIXED_MODULESTORE
from mock import Mock, patch
from textwrap import dedent
from student.models import unique_id_for_user, CourseEnrollment
from student.views import process_survey_link, _cert_info, password_reset, password_reset_confirm_wrapper
from student.views import (process_survey_link, _cert_info, password_reset, password_reset_confirm_wrapper,
change_enrollment)
from student.tests.factories import UserFactory
from student.tests.test_email import mock_render_to_string
import shoppingcart
COURSE_1 = 'edX/toy/2012_Fall'
COURSE_2 = 'edx/full/6.002_Spring_2012'
@@ -343,3 +352,32 @@ class EnrollInCourseTest(TestCase):
# for that user/course_id combination
CourseEnrollment.enroll(user, course_id)
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class PaidRegistrationTest(ModuleStoreTestCase):
"""
Tests for paid registration functionality (not verified student), involves shoppingcart
"""
# arbitrary constant
COURSE_SLUG = "100"
COURSE_NAME = "test_course"
COURSE_ORG = "EDX"
def setUp(self):
# Create course
self.req_factory = RequestFactory()
self.course = CourseFactory.create(org=self.COURSE_ORG, display_name=self.COURSE_NAME, number=self.COURSE_SLUG)
self.assertIsNotNone(self.course)
self.user = User.objects.create(username="jack", email="jack@fake.edx.org")
@unittest.skipUnless(settings.MITX_FEATURES.get('ENABLE_SHOPPING_CART'), "Shopping Cart not enabled in settings")
def test_change_enrollment_add_to_cart(self):
request = self.req_factory.post(reverse('change_enrollment'), {'course_id': self.course.id,
'enrollment_action': 'add_to_cart'})
request.user = self.user
response = change_enrollment(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, reverse('shoppingcart.views.show_cart'))
self.assertTrue(shoppingcart.models.PaidCourseRegistration.contained_in_order(
shoppingcart.models.Order.get_cart_for_user(self.user), self.course.id))

View File

@@ -58,6 +58,7 @@ from external_auth.models import ExternalAuthMap
import external_auth.views
from bulk_email.models import Optout
import shoppingcart
import track.views
@@ -405,6 +406,19 @@ def change_enrollment(request):
return HttpResponse()
elif action == "add_to_cart":
# Pass the request handling to shoppingcart.views
# The view in shoppingcart.views performs error handling and logs different errors. But this elif clause
# is only used in the "auto-add after user reg/login" case, i.e. it's always wrapped in try_change_enrollment.
# This means there's no good way to display error messages to the user. So we log the errors and send
# the user to the shopping cart page always, where they can reasonably discern the status of their cart,
# whether things got added, etc
shoppingcart.views.add_course_to_cart(request, course_id)
return HttpResponse(
reverse("shoppingcart.views.show_cart")
)
elif action == "unenroll":
try:
CourseEnrollment.unenroll(user, course_id)