diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py
index ddbf57158788..eb23262f2150 100644
--- a/common/djangoapps/course_modes/views.py
+++ b/common/djangoapps/course_modes/views.py
@@ -3,28 +3,27 @@
"""
import decimal
-from ipware.ip import get_ip
+from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse
from django.db import transaction
from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import redirect
-from django.views.generic.base import View
-from django.utils.translation import ugettext as _
-from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
+from django.utils.translation import ugettext as _
+from django.views.generic.base import View
+from ipware.ip import get_ip
+from opaque_keys.edx.keys import CourseKey
+from opaque_keys.edx.locations import SlashSeparatedCourseKey
+from xmodule.modulestore.django import modulestore
-from edxmako.shortcuts import render_to_response
-
+from lms.djangoapps.commerce.utils import EcommerceService
from course_modes.models import CourseMode
from courseware.access import has_access
+from edxmako.shortcuts import render_to_response
+from embargo import api as embargo_api
from student.models import CourseEnrollment
-from opaque_keys.edx.locations import SlashSeparatedCourseKey
-from opaque_keys.edx.keys import CourseKey
from util.db import outer_atomic
-from xmodule.modulestore.django import modulestore
-
-from embargo import api as embargo_api
class ChooseModeView(View):
@@ -39,7 +38,14 @@ class ChooseModeView(View):
"""
@method_decorator(transaction.non_atomic_requests)
- def dispatch(self, *args, **kwargs): # pylint: disable=missing-docstring
+ def dispatch(self, *args, **kwargs):
+ """Disable atomicity for the view.
+
+ Otherwise, we'd be unable to commit to the database until the
+ request had concluded; Django will refuse to commit when an
+ atomic() block is active, since that would break atomicity.
+
+ """
return super(ChooseModeView, self).dispatch(*args, **kwargs)
@method_decorator(login_required)
@@ -117,7 +123,10 @@ def get(self, request, course_id, error=None):
)
context = {
- "course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_key.to_deprecated_string()}),
+ "course_modes_choose_url": reverse(
+ "course_modes_choose",
+ kwargs={'course_id': course_key.to_deprecated_string()}
+ ),
"modes": modes,
"has_credit_upsell": has_credit_upsell,
"course_name": course.display_name_with_default_escaped,
@@ -129,15 +138,22 @@ def get(self, request, course_id, error=None):
"nav_hidden": True,
}
if "verified" in modes:
+ verified_mode = modes["verified"]
context["suggested_prices"] = [
decimal.Decimal(x.strip())
- for x in modes["verified"].suggested_prices.split(",")
+ for x in verified_mode.suggested_prices.split(",")
if x.strip()
]
- context["currency"] = modes["verified"].currency.upper()
- context["min_price"] = modes["verified"].min_price
- context["verified_name"] = modes["verified"].name
- context["verified_description"] = modes["verified"].description
+ context["currency"] = verified_mode.currency.upper()
+ context["min_price"] = verified_mode.min_price
+ context["verified_name"] = verified_mode.name
+ context["verified_description"] = verified_mode.description
+
+ if verified_mode.sku:
+ ecommerce_service = EcommerceService()
+ context["use_ecommerce_payment_flow"] = ecommerce_service.is_enabled()
+ context["ecommerce_payment_page"] = ecommerce_service.payment_page_url()
+ context["sku"] = verified_mode.sku
return render_to_response("course_modes/choose.html", context)
diff --git a/common/djangoapps/student/helpers.py b/common/djangoapps/student/helpers.py
index 4e882e5dcca4..a8c4e500fffe 100644
--- a/common/djangoapps/student/helpers.py
+++ b/common/djangoapps/student/helpers.py
@@ -207,7 +207,7 @@ def get_next_url_for_login_page(request):
"""
Determine the URL to redirect to following login/registration/third_party_auth
- The user is currently on a login or reigration page.
+ The user is currently on a login or registration page.
If 'course_id' is set, or other POST_AUTH_PARAMS, we will need to send the user to the
/account/finish_auth/ view following login, which will take care of auto-enrollment in
the specified course.
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index f6a4b5f2c283..86c8d8d5564c 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -39,7 +39,6 @@
from ratelimitbackend.exceptions import RateLimitException
-
from social.apps.django_app import utils as social_utils
from social.backends import oauth as social_oauth
from social.exceptions import AuthException, AuthAlreadyAssociated
@@ -55,6 +54,7 @@
create_comments_service_user, PasswordHistory, UserSignupSource,
DashboardConfiguration, LinkedInAddToProfileConfiguration, ManualEnrollmentAudit, ALLOWEDTOENROLL_TO_ENROLLED)
from student.forms import AccountCreationForm, PasswordResetFormNoActive, get_registration_extension_form
+from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification # pylint: disable=import-error
from certificates.models import CertificateStatuses, certificate_status_for_student
from certificates.api import ( # pylint: disable=import-error
@@ -502,6 +502,7 @@ def complete_course_mode_info(course_id, enrollment, modes=None):
# if verified is an option.
if CourseMode.VERIFIED in modes and enrollment.mode in CourseMode.UPSELL_TO_VERIFIED_MODES:
mode_info['show_upsell'] = True
+ mode_info['verified_sku'] = modes['verified'].sku
# if there is an expiration date, find out how long from now it is
if modes['verified'].expiration_datetime:
today = datetime.datetime.now(UTC).date()
@@ -737,6 +738,13 @@ def dashboard(request):
'xseries_credentials': xseries_credentials,
}
+ ecommerce_service = EcommerceService()
+ if ecommerce_service.is_enabled():
+ context.update({
+ 'use_ecommerce_payment_flow': True,
+ 'ecommerce_payment_page': ecommerce_service.payment_page_url(),
+ })
+
return render_to_response('dashboard.html', context)
diff --git a/lms/djangoapps/commerce/admin.py b/lms/djangoapps/commerce/admin.py
new file mode 100644
index 000000000000..ad49323a1399
--- /dev/null
+++ b/lms/djangoapps/commerce/admin.py
@@ -0,0 +1,8 @@
+""" Admin site bindings for commerce app. """
+
+from django.contrib import admin
+
+from commerce.models import CommerceConfiguration
+from config_models.admin import ConfigurationModelAdmin
+
+admin.site.register(CommerceConfiguration, ConfigurationModelAdmin)
diff --git a/lms/djangoapps/commerce/migrations/0002_commerceconfiguration.py b/lms/djangoapps/commerce/migrations/0002_commerceconfiguration.py
new file mode 100644
index 000000000000..f5bc5ef90984
--- /dev/null
+++ b/lms/djangoapps/commerce/migrations/0002_commerceconfiguration.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('commerce', '0001_data__add_ecommerce_service_user'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='CommerceConfiguration',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
+ ('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
+ ('checkout_on_ecommerce_service', models.BooleanField(default=False, help_text='Use the checkout page hosted by the E-Commerce service.')),
+ ('single_course_checkout_page', models.CharField(default=b'/basket/single-item/', help_text='Path to single course checkout page hosted by the E-Commerce service.', max_length=255)),
+ ('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
+ ],
+ options={
+ 'ordering': ('-change_date',),
+ 'abstract': False,
+ },
+ ),
+ ]
diff --git a/lms/djangoapps/commerce/models.py b/lms/djangoapps/commerce/models.py
index 32ec058432aa..3a78bc084165 100644
--- a/lms/djangoapps/commerce/models.py
+++ b/lms/djangoapps/commerce/models.py
@@ -1,3 +1,25 @@
"""
-This file is intentionally empty. Django 1.6 and below require a models.py file for all apps.
+Commerce-related models.
"""
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+from config_models.models import ConfigurationModel
+
+
+class CommerceConfiguration(ConfigurationModel):
+ """ Commerce configuration """
+
+ checkout_on_ecommerce_service = models.BooleanField(
+ default=False,
+ help_text=_('Use the checkout page hosted by the E-Commerce service.')
+ )
+
+ single_course_checkout_page = models.CharField(
+ max_length=255,
+ default='/basket/single-item/',
+ help_text=_('Path to single course checkout page hosted by the E-Commerce service.')
+ )
+
+ def __unicode__(self):
+ return "Commerce configuration"
diff --git a/lms/djangoapps/commerce/tests/test_utils.py b/lms/djangoapps/commerce/tests/test_utils.py
index df25b7e7b9e6..90c446982e1a 100644
--- a/lms/djangoapps/commerce/tests/test_utils.py
+++ b/lms/djangoapps/commerce/tests/test_utils.py
@@ -1,8 +1,10 @@
"""Tests of commerce utilities."""
from django.test import TestCase
+from django.test.utils import override_settings
from mock import patch
-from commerce.utils import audit_log
+from commerce.utils import audit_log, EcommerceService
+from commerce.models import CommerceConfiguration
class AuditLogTests(TestCase):
@@ -16,3 +18,46 @@ def test_log_message(self, mock_log):
# key-value pairs ordered alphabetically by key.
message = 'foo: bar="baz", qux="quux"'
self.assertTrue(mock_log.info.called_with(message))
+
+
+class EcommerceServiceTests(TestCase):
+ """Tests for the EcommerceService helper class."""
+ SKU = 'TESTSKU'
+
+ def setUp(self):
+ CommerceConfiguration.objects.create(
+ checkout_on_ecommerce_service=True,
+ single_course_checkout_page='/test_basket/'
+ )
+ super(EcommerceServiceTests, self).setUp()
+
+ def test_is_enabled(self):
+ """Verify that is_enabled() returns True when ecomm checkout is enabled. """
+ is_enabled = EcommerceService().is_enabled()
+ self.assertTrue(is_enabled)
+
+ config = CommerceConfiguration.current()
+ config.checkout_on_ecommerce_service = False
+ config.save()
+ is_not_enabled = EcommerceService().is_enabled()
+ self.assertFalse(is_not_enabled)
+
+ @patch('openedx.core.djangoapps.theming.helpers.is_request_in_themed_site')
+ def test_is_enabled_for_microsites(self, is_microsite):
+ """Verify that is_enabled() returns False if used for a microsite."""
+ is_microsite.return_value = True
+ is_not_enabled = EcommerceService().is_enabled()
+ self.assertFalse(is_not_enabled)
+
+ @override_settings(ECOMMERCE_PUBLIC_URL_ROOT='http://ecommerce_url')
+ def test_payment_page_url(self):
+ """Verify that the proper URL is returned."""
+ url = EcommerceService().payment_page_url()
+ self.assertEqual(url, 'http://ecommerce_url/test_basket/')
+
+ @override_settings(ECOMMERCE_PUBLIC_URL_ROOT='http://ecommerce_url')
+ def test_checkout_page_url(self):
+ """ Verify the checkout page URL is properly constructed and returned. """
+ url = EcommerceService().checkout_page_url(self.SKU)
+ expected_url = 'http://ecommerce_url/test_basket/?sku={}'.format(self.SKU)
+ self.assertEqual(url, expected_url)
diff --git a/lms/djangoapps/commerce/utils.py b/lms/djangoapps/commerce/utils.py
index 4d25a58a41d1..00ff3bb99f40 100644
--- a/lms/djangoapps/commerce/utils.py
+++ b/lms/djangoapps/commerce/utils.py
@@ -1,6 +1,11 @@
"""Utilities to assist with commerce tasks."""
import logging
+from urlparse import urljoin
+from django.conf import settings
+
+from commerce.models import CommerceConfiguration
+from openedx.core.djangoapps.theming import helpers
log = logging.getLogger(__name__)
@@ -32,3 +37,29 @@ def audit_log(name, **kwargs):
message = u'{name}: {payload}'.format(name=name, payload=payload)
log.info(message)
+
+
+class EcommerceService(object):
+ """ Helper class for ecommerce service integration. """
+ def __init__(self):
+ self.config = CommerceConfiguration.current()
+
+ def is_enabled(self):
+ """ Check if the service is enabled and that the site is not a microsite. """
+ return self.config.checkout_on_ecommerce_service and not helpers.is_request_in_themed_site()
+
+ def payment_page_url(self):
+ """ Return the URL for the checkout page.
+
+ Example:
+ http://localhost:8002/basket/single_item/
+ """
+ return urljoin(settings.ECOMMERCE_PUBLIC_URL_ROOT, self.config.single_course_checkout_page)
+
+ def checkout_page_url(self, sku):
+ """ Construct the URL to the ecommerce checkout page and include a product.
+
+ Example:
+ http://localhost:8002/basket/single_item/?sku=5H3HG5
+ """
+ return "{}?sku={}".format(self.payment_page_url(), sku)
diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py
index 5914b8367126..889fafa4f66c 100644
--- a/lms/djangoapps/courseware/tests/test_views.py
+++ b/lms/djangoapps/courseware/tests/test_views.py
@@ -31,6 +31,7 @@
from certificates import api as certs_api
from certificates.models import CertificateStatuses, CertificateGenerationConfiguration
from certificates.tests.factories import GeneratedCertificateFactory
+from commerce.models import CommerceConfiguration
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from courseware.model_data import set_score
@@ -69,21 +70,21 @@ def test_jumpto_invalid_location(self):
location = self.course_key.make_usage_key(None, 'NoSuchPlace')
# This is fragile, but unfortunately the problem is that within the LMS we
# can't use the reverse calls from the CMS
- jumpto_url = '{0}/{1}/jump_to/{2}'.format('/courses', self.course_key.to_deprecated_string(), location.to_deprecated_string())
+ jumpto_url = '{0}/{1}/jump_to/{2}'.format('/courses', unicode(self.course_key), unicode(location))
response = self.client.get(jumpto_url)
self.assertEqual(response.status_code, 404)
@unittest.skip
def test_jumpto_from_chapter(self):
location = self.course_key.make_usage_key('chapter', 'Overview')
- jumpto_url = '{0}/{1}/jump_to/{2}'.format('/courses', self.course_key.to_deprecated_string(), location.to_deprecated_string())
+ jumpto_url = '{0}/{1}/jump_to/{2}'.format('/courses', unicode(self.course_key), unicode(location))
expected = 'courses/edX/toy/2012_Fall/courseware/Overview/'
response = self.client.get(jumpto_url)
self.assertRedirects(response, expected, status_code=302, target_status_code=302)
@unittest.skip
def test_jumpto_id(self):
- jumpto_url = '{0}/{1}/jump_to_id/{2}'.format('/courses', self.course_key.to_deprecated_string(), 'Overview')
+ jumpto_url = '{0}/{1}/jump_to_id/{2}'.format('/courses', unicode(self.course_key), 'Overview')
expected = 'courses/edX/toy/2012_Fall/courseware/Overview/'
response = self.client.get(jumpto_url)
self.assertRedirects(response, expected, status_code=302, target_status_code=302)
@@ -173,7 +174,7 @@ def test_jumpto_from_nested_module(self):
def test_jumpto_id_invalid_location(self):
location = Location('edX', 'toy', 'NoSuchPlace', None, None, None)
- jumpto_url = '{0}/{1}/jump_to_id/{2}'.format('/courses', self.course_key.to_deprecated_string(), location.to_deprecated_string())
+ jumpto_url = '{0}/{1}/jump_to_id/{2}'.format('/courses', unicode(self.course_key), unicode(location))
response = self.client.get(jumpto_url)
self.assertEqual(response.status_code, 404)
@@ -212,26 +213,72 @@ def test_course_about_in_cart(self):
in_cart_span = ''
# don't mock this course due to shopping cart existence checking
course = CourseFactory.create(org="new", number="unenrolled", display_name="course")
- request = self.request_factory.get(reverse('about_course', args=[course.id.to_deprecated_string()]))
+ request = self.request_factory.get(reverse('about_course', args=[unicode(course.id)]))
request.user = AnonymousUser()
mako_middleware_process_request(request)
- response = views.course_about(request, course.id.to_deprecated_string())
+ response = views.course_about(request, unicode(course.id))
self.assertEqual(response.status_code, 200)
self.assertNotIn(in_cart_span, response.content)
# authenticated user with nothing in cart
request.user = self.user
- response = views.course_about(request, course.id.to_deprecated_string())
+ response = views.course_about(request, unicode(course.id))
self.assertEqual(response.status_code, 200)
self.assertNotIn(in_cart_span, response.content)
# now add the course to the cart
cart = shoppingcart.models.Order.get_cart_for_user(self.user)
shoppingcart.models.PaidCourseRegistration.add_to_order(cart, course.id)
- response = views.course_about(request, course.id.to_deprecated_string())
+ response = views.course_about(request, unicode(course.id))
self.assertEqual(response.status_code, 200)
self.assertIn(in_cart_span, response.content)
+ def assert_enrollment_link_present(self, is_anonymous, _id=False):
+ """
+ Prepare ecommerce checkout data and assert if the ecommerce link is contained in the response.
+
+ Arguments:
+ is_anonymous(bool): Tell the method to use an anonymous user or the logged in one.
+ _id(bool): Tell the method to either expect an id in the href or not.
+
+ """
+ checkout_page = '/test_basket/'
+ sku = 'TEST123'
+ CommerceConfiguration.objects.create(
+ checkout_on_ecommerce_service=True,
+ single_course_checkout_page=checkout_page
+ )
+ course = CourseFactory.create()
+ CourseModeFactory(mode_slug=CourseMode.PROFESSIONAL, course_id=course.id, sku=sku, min_price=1)
+
+ request = self.request_factory.get(reverse('about_course', args=[unicode(course.id)]))
+ request.user = AnonymousUser() if is_anonymous else self.user
+ mako_middleware_process_request(request)
+
+ # Construct the link for each of the four possibilities:
+ # (1) shopping cart is disabled and the user is not logged in
+ # (2) shopping cart is disabled and the user is logged in
+ # (3) shopping cart is enabled and the user is not logged in
+ # (4) shopping cart is enabled and the user is logged in
+ href = '' if _id else ">"
+ )
+ response = views.course_about(request, unicode(course.id))
+ self.assertEqual(response.status_code, 200)
+ self.assertIn(href, response.content)
+
+ @ddt.data(True, False)
+ def test_ecommerce_checkout(self, is_anonymous):
+ self.assert_enrollment_link_present(is_anonymous=is_anonymous)
+
+ @ddt.data(True, False)
+ @unittest.skipUnless(settings.FEATURES.get('ENABLE_SHOPPING_CART'), 'Shopping Cart not enabled in settings')
+ @patch.dict(settings.FEATURES, {'ENABLE_PAID_COURSE_REGISTRATION': True})
+ def test_ecommerce_checkout_shopping_cart_enabled(self, is_anonymous):
+ self.assert_enrollment_link_present(is_anonymous=is_anonymous, _id=True)
+
def test_user_groups(self):
# depreciated function
mock_user = MagicMock()
@@ -268,7 +315,7 @@ def test_incomplete_course_id(self):
def test_index_invalid_position(self):
request_url = '/'.join([
'/courses',
- self.course.id.to_deprecated_string(),
+ unicode(self.course.id),
'courseware',
self.chapter.location.name,
self.section.location.name,
@@ -281,7 +328,7 @@ def test_index_invalid_position(self):
def test_unicode_handling_in_url(self):
url_parts = [
'/courses',
- self.course.id.to_deprecated_string(),
+ unicode(self.course.id),
'courseware',
self.chapter.location.name,
self.section.location.name,
@@ -373,9 +420,9 @@ def test_submission_history_accepts_valid_ids(self):
self.client.login(username=admin.username, password='test')
url = reverse('submission_history', kwargs={
- 'course_id': self.course_key.to_deprecated_string(),
+ 'course_id': unicode(self.course_key),
'student_username': 'dummy',
- 'location': self.component.location.to_deprecated_string(),
+ 'location': unicode(self.component.location),
})
response = self.client.get(url)
# Tests that we do not get an "Invalid x" response when passing correct arguments to view
@@ -389,7 +436,7 @@ def test_submission_history_xss(self):
# try it with an existing user and a malicious location
url = reverse('submission_history', kwargs={
- 'course_id': self.course_key.to_deprecated_string(),
+ 'course_id': unicode(self.course_key),
'student_username': 'dummy',
'location': ''
})
@@ -398,7 +445,7 @@ def test_submission_history_xss(self):
# try it with a malicious user and a non-existent location
url = reverse('submission_history', kwargs={
- 'course_id': self.course_key.to_deprecated_string(),
+ 'course_id': unicode(self.course_key),
'student_username': '',
'location': 'dummy'
})
@@ -697,7 +744,7 @@ def get_text(self, course):
""" Returns the HTML for the progress page """
mako_middleware_process_request(self.request)
- return views.progress(self.request, course_id=course.id.to_deprecated_string(), student_id=self.user.id).content
+ return views.progress(self.request, course_id=unicode(course.id), student_id=self.user.id).content
class TestAccordionDueDate(BaseDueDateTests):
@@ -742,7 +789,7 @@ def get_about_text(self, course_key):
"""
Get the text of the /about page for the course.
"""
- text = views.course_about(self.request, course_key.to_deprecated_string()).content
+ text = views.course_about(self.request, unicode(course_key)).content
return text
@patch('util.date_utils.pgettext', fake_pgettext(translations={
@@ -816,7 +863,7 @@ def test_progress_page_xss_prevent(self, malicious_code):
def test_pure_ungraded_xblock(self):
ItemFactory.create(category='acid', parent_location=self.vertical.location)
- resp = views.progress(self.request, course_id=self.course.id.to_deprecated_string())
+ resp = views.progress(self.request, course_id=unicode(self.course.id))
self.assertEqual(resp.status_code, 200)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
@@ -845,12 +892,12 @@ def test_student_progress_with_valid_and_invalid_id(self, default_store):
# Enroll student into course
CourseEnrollment.enroll(self.user, self.course.id)
- resp = views.progress(self.request, course_id=self.course.id.to_deprecated_string(), student_id=self.user.id)
+ resp = views.progress(self.request, course_id=unicode(self.course.id), student_id=self.user.id)
# Assert that valid 'student_id' returns 200 status
self.assertEqual(resp.status_code, 200)
def test_non_asci_grade_cutoffs(self):
- resp = views.progress(self.request, course_id=self.course.id.to_deprecated_string())
+ resp = views.progress(self.request, course_id=unicode(self.course.id))
self.assertEqual(resp.status_code, 200)
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index 11d69b9176ca..60e16b873e51 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -39,6 +39,7 @@
import survey.views
from certificates import api as certs_api
from openedx.core.lib.gating import api as gating_api
+from commerce.utils import EcommerceService
from course_modes.models import CourseMode
from courseware import grades
from courseware.access import has_access, has_ccx_coach_role, _adjust_start_date_for_beta_testers
@@ -63,13 +64,13 @@
from courseware.user_state_client import DjangoXBlockUserStateClient
from edxmako.shortcuts import render_to_response, render_to_string, marketing_link
from instructor.enrollment import uses_shib
-from microsite_configuration import microsite
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.credit.api import (
get_credit_requirement_status,
is_user_eligible_for_credit,
is_credit_course
)
+from openedx.core.djangoapps.theming import helpers as theming_helpers
from shoppingcart.models import CourseRegistrationCode
from shoppingcart.utils import is_shopping_cart_enabled
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
@@ -143,8 +144,10 @@ def courses(request):
if not settings.FEATURES.get('ENABLE_COURSE_DISCOVERY'):
courses_list = get_courses(request.user)
- if microsite.get_value("ENABLE_COURSE_SORTING_BY_START_DATE",
- settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]):
+ if theming_helpers.get_value(
+ "ENABLE_COURSE_SORTING_BY_START_DATE",
+ settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]
+ ):
courses_list = sort_by_start_date(courses_list)
else:
courses_list = sort_by_announcement(courses_list)
@@ -508,7 +511,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
return redirect(reverse('courseware', args=[course.id.to_deprecated_string()]))
raise Http404
- ## Allow chromeless operation
+ # Allow chromeless operation
if section_descriptor.chrome:
chrome = [s.strip() for s in section_descriptor.chrome.lower().split(",")]
if 'accordion' not in chrome:
@@ -855,8 +858,9 @@ def course_about(request, course_id):
with modulestore().bulk_operations(course_key):
permission = get_permission_for_course_about()
course = get_course_with_access(request.user, permission, course_key)
+ modes = CourseMode.modes_for_course_dict(course_key)
- if microsite.get_value('ENABLE_MKTG_SITE', settings.FEATURES.get('ENABLE_MKTG_SITE', False)):
+ if theming_helpers.get_value('ENABLE_MKTG_SITE', settings.FEATURES.get('ENABLE_MKTG_SITE', False)):
return redirect(reverse('info', args=[course.id.to_deprecated_string()]))
registered = registered_for_course(course, request.user)
@@ -871,10 +875,9 @@ def course_about(request, course_id):
show_courseware_link = bool(
(
- has_access(request.user, 'load', course)
- and has_access(request.user, 'view_courseware_with_prerequisites', course)
- )
- or settings.FEATURES.get('ENABLE_LMS_MIGRATION')
+ has_access(request.user, 'load', course) and
+ has_access(request.user, 'view_courseware_with_prerequisites', course)
+ ) or settings.FEATURES.get('ENABLE_LMS_MIGRATION')
)
# Note: this is a flow for payment for course registration, not the Verified Certificate flow.
@@ -884,15 +887,31 @@ def course_about(request, course_id):
_is_shopping_cart_enabled = is_shopping_cart_enabled()
if _is_shopping_cart_enabled:
- registration_price = CourseMode.min_course_price_for_currency(course_key,
- settings.PAID_COURSE_REGISTRATION_CURRENCY[0])
+ registration_price = CourseMode.min_course_price_for_currency(
+ course_key,
+ settings.PAID_COURSE_REGISTRATION_CURRENCY[0]
+ )
if request.user.is_authenticated():
cart = shoppingcart.models.Order.get_cart_for_user(request.user)
in_cart = shoppingcart.models.PaidCourseRegistration.contained_in_order(cart, course_key) or \
shoppingcart.models.CourseRegCodeItem.contained_in_order(cart, course_key)
reg_then_add_to_cart_link = "{reg_url}?course_id={course_id}&enrollment_action=add_to_cart".format(
- reg_url=reverse('register_user'), course_id=urllib.quote(str(course_id)))
+ reg_url=reverse('register_user'), course_id=urllib.quote(str(course_id))
+ )
+
+ # If the ecommerce checkout flow is enabled and the mode of the course is
+ # professional or no id professional, we construct links for the enrollment
+ # button to add the course to the ecommerce basket.
+ ecommerce_checkout_link = ''
+ professional_mode = ''
+ ecomm_service = EcommerceService()
+ if ecomm_service.is_enabled() and (
+ CourseMode.PROFESSIONAL in modes or CourseMode.NO_ID_PROFESSIONAL_MODE in modes
+ ):
+ professional_mode = modes.get(CourseMode.PROFESSIONAL, '') or \
+ modes.get(CourseMode.NO_ID_PROFESSIONAL_MODE, '')
+ ecommerce_checkout_link = ecomm_service.checkout_page_url(professional_mode.sku)
course_price = get_cosmetic_display_price(course, registration_price)
can_add_course_to_cart = _is_shopping_cart_enabled and registration_price
@@ -925,6 +944,9 @@ def course_about(request, course_id):
'is_cosmetic_price_enabled': settings.FEATURES.get('ENABLE_COSMETIC_DISPLAY_PRICE'),
'course_price': course_price,
'in_cart': in_cart,
+ 'ecommerce_checkout': ecomm_service.is_enabled(),
+ 'ecommerce_checkout_link': ecommerce_checkout_link,
+ 'professional_mode': professional_mode,
'reg_then_add_to_cart_link': reg_then_add_to_cart_link,
'show_courseware_link': show_courseware_link,
'is_course_full': is_course_full,
@@ -1577,12 +1599,12 @@ def financial_assistance_form(request):
enrolled_courses = [
{'name': enrollment.course_overview.display_name, 'value': unicode(enrollment.course_id)}
for enrollment in CourseEnrollment.enrollments_for_user(user).order_by('-created')
- if CourseMode.objects.filter(
+
+ if enrollment.mode != CourseMode.VERIFIED and CourseMode.objects.filter(
Q(_expiration_datetime__isnull=True) | Q(_expiration_datetime__gt=datetime.now(UTC())),
course_id=enrollment.course_id,
mode_slug=CourseMode.VERIFIED
).exists()
- and enrollment.mode != CourseMode.VERIFIED
]
return render_to_response('financial-assistance/apply.html', {
'header_text': FINANCIAL_ASSISTANCE_HEADER,
diff --git a/lms/static/js/student_account/views/FinishAuthView.js b/lms/static/js/student_account/views/FinishAuthView.js
index 72d66bf29858..8ec1da6ab70a 100644
--- a/lms/static/js/student_account/views/FinishAuthView.js
+++ b/lms/static/js/student_account/views/FinishAuthView.js
@@ -51,7 +51,7 @@
enrollmentAction: $.url( '?enrollment_action' ),
courseId: $.url( '?course_id' ),
courseMode: $.url( '?course_mode' ),
- emailOptIn: $.url( '?email_opt_in')
+ emailOptIn: $.url( '?email_opt_in' )
};
for (var key in queryParams) {
if (queryParams[key]) {
diff --git a/lms/templates/course_modes/choose.html b/lms/templates/course_modes/choose.html
index b15250a73355..f79fae05dc19 100644
--- a/lms/templates/course_modes/choose.html
+++ b/lms/templates/course_modes/choose.html
@@ -21,7 +21,7 @@
} else {
title.attr("aria-expanded", "false");
}
- }
+ };
$(document).ready(function() {
$('.expandable-area').slideUp();
@@ -38,6 +38,12 @@
$('#contribution-other').attr('checked',true);
});
+ % if use_ecommerce_payment_flow:
+ $('input[name=verified_mode]').click(function(e){
+ e.preventDefault();
+ window.location.href = '${ecommerce_payment_page}?sku=${sku}';
+ });
+ % endif
});
%block>
diff --git a/lms/templates/courseware/course_about.html b/lms/templates/courseware/course_about.html
index 26fc56afd278..b24c1510b7b1 100644
--- a/lms/templates/courseware/course_about.html
+++ b/lms/templates/courseware/course_about.html
@@ -39,6 +39,7 @@
location.href = "${reg_then_add_to_cart_link}";
}
};
+
$("#add_to_cart_post").click(function(event){
$.ajax({
url: "${reverse('add_course_to_cart', args=[course.id.to_deprecated_string()])}",
@@ -152,14 +153,27 @@
reg_href = reg_then_add_to_cart_link
reg_element_id = "reg_then_add_to_cart"
%>
+ <% if ecommerce_checkout:
+ reg_href = ecommerce_checkout_link
+ reg_element_id = ""
+ %>
${_("Add {course_name} to Cart ({price} USD)")\
.format(course_name=course.display_number_with_default, price=course_price)}
-
%else:
-
+ <%
+ if ecommerce_checkout:
+ reg_href = ecommerce_checkout_link
+ else:
+ reg_href="#"
+ if professional_mode:
+ href_class = "add-to-cart"
+ else:
+ href_class = "register"
+ %>
+
${_("Enroll in {course_name}").format(course_name=course.display_number_with_default) | h}
diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html
index 1852a21db25c..b862f3556acb 100644
--- a/lms/templates/dashboard/_dashboard_course_listing.html
+++ b/lms/templates/dashboard/_dashboard_course_listing.html
@@ -330,7 +330,11 @@
${_('Your verification will expire soon!')}
${_("It's official. It's easily shareable. It's a proven motivator to complete the course.
{link_start}Learn more about the verified {cert_name_long}{link_end}.").format(link_start=''.format(marketing_link('WHAT_IS_VERIFIED_CERT'), enrollment.course_id), link_end="", cert_name_long=cert_name_long)}