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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user