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 }); 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)}

- + % if use_ecommerce_payment_flow and course_mode_info['verified_sku']: + + % else: + + % endif ${_("Upgrade to Verified")} diff --git a/themes/edx.org/lms/templates/course_modes/choose.html b/themes/edx.org/lms/templates/course_modes/choose.html index 41ad75eec87d..d023d02d9c05 100644 --- a/themes/edx.org/lms/templates/course_modes/choose.html +++ b/themes/edx.org/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 });