Merge pull request #11570 from edx/patch-2016-02-17
Patch release 2016-02-17
This commit is contained in:
@@ -5,10 +5,21 @@
|
||||
// We can't use JQuery's on load method because it
|
||||
// screws up RequireJS' JQuery initialization.
|
||||
var onLoadCallback = function() {
|
||||
analytics.identify("${user.id}", {
|
||||
email: "${user.email}",
|
||||
username: "${user.username}"
|
||||
});
|
||||
analytics.identify(
|
||||
"${user.id}",
|
||||
{
|
||||
email: "${user.email}",
|
||||
username: "${user.username}"
|
||||
},
|
||||
{
|
||||
integrations: {
|
||||
// Disable MailChimp because we don't want to update the user's email
|
||||
// and username in MailChimp on every page load. We only need to capture
|
||||
// this data on registration/activation.
|
||||
MailChimp: false
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
if (window.addEventListener) {
|
||||
window.addEventListener("load", onLoadCallback, false);
|
||||
|
||||
@@ -3,28 +3,27 @@ Views for the course_mode module
|
||||
"""
|
||||
|
||||
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 edxmako.shortcuts import render_to_response
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from courseware.access import has_access
|
||||
from student.models import CourseEnrollment
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
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 util.db import outer_atomic
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
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 util.db import outer_atomic
|
||||
|
||||
|
||||
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 @@ class ChooseModeView(View):
|
||||
)
|
||||
|
||||
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 @@ class ChooseModeView(View):
|
||||
"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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -136,6 +136,7 @@ class TestCreateAccount(TestCase):
|
||||
'username': self.params['username'],
|
||||
'name': self.params['name'],
|
||||
'age': 13,
|
||||
'yearOfBirth': year_of_birth,
|
||||
'education': 'Associate degree',
|
||||
'address': self.params['mailing_address'],
|
||||
'gender': 'Other/Prefer Not to Say',
|
||||
|
||||
@@ -39,7 +39,6 @@ from django.template.response import TemplateResponse
|
||||
|
||||
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 @@ from student.models import (
|
||||
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)
|
||||
|
||||
|
||||
@@ -1234,10 +1242,19 @@ def login_user(request, error=""): # pylint: disable=too-many-statements,unused
|
||||
# Track the user's sign in
|
||||
if hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY:
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
analytics.identify(user.id, {
|
||||
'email': email,
|
||||
'username': username
|
||||
})
|
||||
analytics.identify(
|
||||
user.id,
|
||||
{
|
||||
'email': email,
|
||||
'username': username
|
||||
},
|
||||
{
|
||||
# Disable MailChimp because we don't want to update the user's email
|
||||
# and username in MailChimp on every page load. We only need to capture
|
||||
# this data on registration/activation.
|
||||
'MailChimp': False
|
||||
}
|
||||
)
|
||||
|
||||
analytics.track(
|
||||
user.id,
|
||||
@@ -1691,7 +1708,9 @@ def create_account_with_params(request, params):
|
||||
'email': user.email,
|
||||
'username': user.username,
|
||||
'name': profile.name,
|
||||
'age': profile.age,
|
||||
# Mailchimp requires the age & yearOfBirth to be integers, we send a sane integer default if falsey.
|
||||
'age': profile.age or -1,
|
||||
'yearOfBirth': profile.year_of_birth or datetime.datetime.now(UTC).year,
|
||||
'education': profile.level_of_education_display,
|
||||
'address': profile.mailing_address,
|
||||
'gender': profile.gender_display,
|
||||
|
||||
8
lms/djangoapps/commerce/admin.py
Normal file
8
lms/djangoapps/commerce/admin.py
Normal file
@@ -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)
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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"
|
||||
|
||||
@@ -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 @@ class AuditLogTests(TestCase):
|
||||
# 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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -31,6 +31,7 @@ import shoppingcart
|
||||
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 @@ class TestJumpTo(ModuleStoreTestCase):
|
||||
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 @@ class TestJumpTo(ModuleStoreTestCase):
|
||||
|
||||
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 @@ class ViewsTestCase(ModuleStoreTestCase):
|
||||
in_cart_span = '<span class="add-to-cart">'
|
||||
# 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 = '<a href="{}?{}" class="add-to-cart"{}'.format(
|
||||
checkout_page,
|
||||
'sku=TEST123',
|
||||
' id="">' 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 @@ class ViewsTestCase(ModuleStoreTestCase):
|
||||
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 @@ class ViewsTestCase(ModuleStoreTestCase):
|
||||
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 @@ class ViewsTestCase(ModuleStoreTestCase):
|
||||
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 @@ class ViewsTestCase(ModuleStoreTestCase):
|
||||
|
||||
# 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': '<script>alert("hello");</script>'
|
||||
})
|
||||
@@ -398,7 +445,7 @@ class ViewsTestCase(ModuleStoreTestCase):
|
||||
|
||||
# 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': '<script>alert("hello");</script>',
|
||||
'location': 'dummy'
|
||||
})
|
||||
@@ -697,7 +744,7 @@ class TestProgressDueDate(BaseDueDateTests):
|
||||
""" 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 @@ class StartDateTests(ModuleStoreTestCase):
|
||||
"""
|
||||
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 @@ class ProgressPageTests(ModuleStoreTestCase):
|
||||
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 @@ class ProgressPageTests(ModuleStoreTestCase):
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import survey.utils
|
||||
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.url_helpers import get_redirect_url
|
||||
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,
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -21,7 +21,7 @@ from django.core.urlresolvers import reverse
|
||||
} else {
|
||||
title.attr("aria-expanded", "false");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
$('.expandable-area').slideUp();
|
||||
@@ -38,6 +38,12 @@ from django.core.urlresolvers import reverse
|
||||
$('#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
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -39,6 +39,7 @@ from openedx.core.lib.courses import course_image_url
|
||||
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 @@ from openedx.core.lib.courses import course_image_url
|
||||
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 = ""
|
||||
%>
|
||||
<a href="${reg_href}" class="add-to-cart" id="${reg_element_id}">
|
||||
${_("Add {course_name} to Cart <span>({price} USD)</span>")\
|
||||
.format(course_name=course.display_number_with_default, price=course_price)}
|
||||
|
||||
</a>
|
||||
<div id="register_error"></div>
|
||||
%else:
|
||||
<a href="#" class="register">
|
||||
<%
|
||||
if ecommerce_checkout:
|
||||
reg_href = ecommerce_checkout_link
|
||||
else:
|
||||
reg_href="#"
|
||||
if professional_mode:
|
||||
href_class = "add-to-cart"
|
||||
else:
|
||||
href_class = "register"
|
||||
%>
|
||||
<a href="${reg_href}" class="${href_class}">
|
||||
${_("Enroll in {course_name}").format(course_name=course.display_number_with_default) | h}
|
||||
</a>
|
||||
<div id="register_error"></div>
|
||||
|
||||
@@ -330,7 +330,11 @@ from student.helpers import (
|
||||
${_("It's official. It's easily shareable. It's a proven motivator to complete the course. <br>{link_start}Learn more about the verified {cert_name_long}{link_end}.").format(link_start='<a href="{}" class="verified-info" data-course-key="{}">'.format(marketing_link('WHAT_IS_VERIFIED_CERT'), enrollment.course_id), link_end="</a>", cert_name_long=cert_name_long)}
|
||||
</p>
|
||||
<div class="action-upgrade-container">
|
||||
<a class="action action-upgrade" href="${reverse('verify_student_upgrade_and_verify', kwargs={'course_id': unicode(course_overview.id)})}" data-course-id="${course_overview.id | h}" data-user="${user.username | h}">
|
||||
% if use_ecommerce_payment_flow and course_mode_info['verified_sku']:
|
||||
<a class="action action-upgrade" href="${ecommerce_payment_page}?sku=${course_mode_info['verified_sku']}">
|
||||
% else:
|
||||
<a class="action action-upgrade" href="${reverse('verify_student_upgrade_and_verify', kwargs={'course_id': unicode(course_overview.id)})}" data-course-id="${course_overview.id | h}" data-user="${user.username | h}">
|
||||
% endif
|
||||
<i class="action-upgrade-icon"></i>
|
||||
<span class="wrapper-copy">
|
||||
<span class="copy" id="upgrade-to-verified">${_("Upgrade to Verified")}</span>
|
||||
|
||||
@@ -3,10 +3,21 @@
|
||||
<script type="text/javascript">
|
||||
% if user.is_authenticated():
|
||||
$(window).load(function() {
|
||||
analytics.identify("${user.id}", {
|
||||
email: "${user.email}",
|
||||
username: "${user.username}"
|
||||
});
|
||||
analytics.identify(
|
||||
"${user.id}",
|
||||
{
|
||||
email: "${user.email}",
|
||||
username: "${user.username}"
|
||||
},
|
||||
{
|
||||
integrations: {
|
||||
// Disable MailChimp because we don't want to update the user's email
|
||||
// and username in MailChimp on every page load. We only need to capture
|
||||
// this data on registration/activation.
|
||||
MailChimp: false
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
% endif
|
||||
</script>
|
||||
|
||||
@@ -21,7 +21,7 @@ from django.core.urlresolvers import reverse
|
||||
} else {
|
||||
title.attr("aria-expanded", "false");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
$('.expandable-area').slideUp();
|
||||
@@ -38,6 +38,12 @@ from django.core.urlresolvers import reverse
|
||||
$('#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
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
Reference in New Issue
Block a user