Commit f1538132 by Matt Drayer

Merge pull request #12315 from edx/mattdrayer/course-mode-bulk-sku

mattdrayer/course-mode-bulk-sku: Add new CourseMode field
parents f81e90fb a9b7e4c6
......@@ -184,7 +184,8 @@ class CourseModeAdmin(admin.ModelAdmin):
'currency',
'_expiration_datetime',
'verification_deadline',
'sku'
'sku',
'bulk_sku'
)
search_fields = ('course_id',)
......@@ -195,7 +196,8 @@ class CourseModeAdmin(admin.ModelAdmin):
'mode_slug',
'min_price',
'expiration_datetime_custom',
'sku'
'sku',
'bulk_sku'
)
def expiration_datetime_custom(self, obj):
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course_modes', '0006_auto_20160208_1407'),
]
operations = [
migrations.AddField(
model_name='coursemode',
name='bulk_sku',
field=models.CharField(help_text='This is the bulk SKU (stock keeping unit) of this mode in the external ecommerce service.', max_length=255, null=True, verbose_name=b'Bulk SKU', blank=True),
),
]
......@@ -22,6 +22,7 @@ Mode = namedtuple('Mode',
'expiration_datetime',
'description',
'sku',
'bulk_sku',
])
......@@ -96,6 +97,17 @@ class CourseMode(models.Model):
)
)
# Optional bulk order SKU for integration with the ecommerce service
bulk_sku = models.CharField(
max_length=255,
null=True,
blank=True,
verbose_name="Bulk SKU",
help_text=_(
u"This is the bulk SKU (stock keeping unit) of this mode in the external ecommerce service."
)
)
HONOR = 'honor'
PROFESSIONAL = 'professional'
VERIFIED = "verified"
......@@ -103,7 +115,7 @@ class CourseMode(models.Model):
NO_ID_PROFESSIONAL_MODE = "no-id-professional"
CREDIT_MODE = "credit"
DEFAULT_MODE = Mode(AUDIT, _('Audit'), 0, '', 'usd', None, None, None)
DEFAULT_MODE = Mode(AUDIT, _('Audit'), 0, '', 'usd', None, None, None, None)
DEFAULT_MODE_SLUG = AUDIT
# Modes that allow a student to pursue a verified certificate
......@@ -123,7 +135,7 @@ class CourseMode(models.Model):
# "honor" to "audit", we still need to have the shoppingcart
# use "honor"
DEFAULT_SHOPPINGCART_MODE_SLUG = HONOR
DEFAULT_SHOPPINGCART_MODE = Mode(HONOR, _('Honor'), 0, '', 'usd', None, None, None)
DEFAULT_SHOPPINGCART_MODE = Mode(HONOR, _('Honor'), 0, '', 'usd', None, None, None, None)
class Meta(object):
unique_together = ('course_id', 'mode_slug', 'currency')
......@@ -625,7 +637,8 @@ class CourseMode(models.Model):
self.currency,
self.expiration_datetime,
self.description,
self.sku
self.sku,
self.bulk_sku
)
def __unicode__(self):
......
......@@ -77,7 +77,7 @@ class CourseModeModelTest(TestCase):
self.create_mode('verified', 'Verified Certificate', 10)
modes = CourseMode.modes_for_course(self.course_key)
mode = Mode(u'verified', u'Verified Certificate', 10, '', 'usd', None, None, None)
mode = Mode(u'verified', u'Verified Certificate', 10, '', 'usd', None, None, None, None)
self.assertEqual([mode], modes)
modes_dict = CourseMode.modes_for_course_dict(self.course_key)
......@@ -89,8 +89,8 @@ class CourseModeModelTest(TestCase):
"""
Finding the modes when there's multiple modes
"""
mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None)
mode2 = Mode(u'verified', u'Verified Certificate', 10, '', 'usd', None, None, None)
mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None, None)
mode2 = Mode(u'verified', u'Verified Certificate', 10, '', 'usd', None, None, None, None)
set_modes = [mode1, mode2]
for mode in set_modes:
self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices)
......@@ -109,9 +109,9 @@ class CourseModeModelTest(TestCase):
self.assertEqual(0, CourseMode.min_course_price_for_currency(self.course_key, 'usd'))
# create some modes
mode1 = Mode(u'honor', u'Honor Code Certificate', 10, '', 'usd', None, None, None)
mode2 = Mode(u'verified', u'Verified Certificate', 20, '', 'usd', None, None, None)
mode3 = Mode(u'honor', u'Honor Code Certificate', 80, '', 'cny', None, None, None)
mode1 = Mode(u'honor', u'Honor Code Certificate', 10, '', 'usd', None, None, None, None)
mode2 = Mode(u'verified', u'Verified Certificate', 20, '', 'usd', None, None, None, None)
mode3 = Mode(u'honor', u'Honor Code Certificate', 80, '', 'cny', None, None, None, None)
set_modes = [mode1, mode2, mode3]
for mode in set_modes:
self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices, mode.currency)
......@@ -126,7 +126,7 @@ class CourseModeModelTest(TestCase):
modes = CourseMode.modes_for_course(self.course_key)
self.assertEqual([CourseMode.DEFAULT_MODE], modes)
mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None)
mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None, None)
self.create_mode(mode1.slug, mode1.name, mode1.min_price, mode1.suggested_prices)
modes = CourseMode.modes_for_course(self.course_key)
self.assertEqual([mode1], modes)
......@@ -134,7 +134,17 @@ class CourseModeModelTest(TestCase):
expiration_datetime = datetime.now(pytz.UTC) + timedelta(days=1)
expired_mode.expiration_datetime = expiration_datetime
expired_mode.save()
expired_mode_value = Mode(u'verified', u'Verified Certificate', 10, '', 'usd', expiration_datetime, None, None)
expired_mode_value = Mode(
u'verified',
u'Verified Certificate',
10,
'',
'usd',
expiration_datetime,
None,
None,
None
)
modes = CourseMode.modes_for_course(self.course_key)
self.assertEqual([expired_mode_value, mode1], modes)
......
......@@ -101,7 +101,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
# Create the course modes
prof_course = CourseFactory.create()
CourseModeFactory(mode_slug=CourseMode.NO_ID_PROFESSIONAL_MODE, course_id=prof_course.id,
min_price=100, sku='TEST')
min_price=100, sku='TEST', bulk_sku="BULKTEST")
ecomm_test_utils.update_commerce_config(enabled=True)
# Enroll the user in the test course
CourseEnrollmentFactory(
......@@ -304,7 +304,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
self.assertEquals(response.status_code, 200)
expected_mode = [Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None)]
expected_mode = [Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None, None)]
course_mode = CourseMode.modes_for_course(self.course.id)
self.assertEquals(course_mode, expected_mode)
......@@ -328,7 +328,19 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
self.assertEquals(response.status_code, 200)
expected_mode = [Mode(mode_slug, mode_display_name, min_price, suggested_prices, currency, None, None, None)]
expected_mode = [
Mode(
mode_slug,
mode_display_name,
min_price,
suggested_prices,
currency,
None,
None,
None,
None
)
]
course_mode = CourseMode.modes_for_course(self.course.id)
self.assertEquals(course_mode, expected_mode)
......@@ -351,8 +363,8 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
url = reverse('create_mode', args=[unicode(self.course.id)])
self.client.get(url, parameters)
honor_mode = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None)
verified_mode = Mode(u'verified', u'Verified Certificate', 10, '10,20', 'usd', None, None, None)
honor_mode = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None, None)
verified_mode = Mode(u'verified', u'Verified Certificate', 10, '10,20', 'usd', None, None, None, None)
expected_modes = [honor_mode, verified_mode]
course_modes = CourseMode.modes_for_course(self.course.id)
......
......@@ -161,6 +161,7 @@ class ChooseModeView(View):
context["use_ecommerce_payment_flow"] = ecommerce_service.is_enabled(request.user)
context["ecommerce_payment_page"] = ecommerce_service.payment_page_url()
context["sku"] = verified_mode.sku
context["bulk_sku"] = verified_mode.bulk_sku
return render_to_response("course_modes/choose.html", context)
......
......@@ -52,7 +52,8 @@ def get_enrollments(user_id):
"currency": "usd",
"expiration_datetime": null,
"description": null,
"sku": null
"sku": null,
"bulk_sku": null
}
],
"invite_only": False
......@@ -78,7 +79,8 @@ def get_enrollments(user_id):
"currency": "usd",
"expiration_datetime": null,
"description": null,
"sku": null
"sku": null,
"bulk_sku": null
}
],
"invite_only": True
......@@ -124,7 +126,8 @@ def get_enrollment(user_id, course_id):
"currency": "usd",
"expiration_datetime": null,
"description": null,
"sku": null
"sku": null,
"bulk_sku": null
}
],
"invite_only": False
......@@ -175,7 +178,8 @@ def add_enrollment(user_id, course_id, mode=None, is_active=True):
"currency": "usd",
"expiration_datetime": null,
"description": null,
"sku": null
"sku": null,
"bulk_sku": null
}
],
"invite_only": False
......@@ -227,7 +231,8 @@ def update_enrollment(user_id, course_id, mode=None, is_active=None, enrollment_
"currency": "usd",
"expiration_datetime": null,
"description": null,
"sku": null
"sku": null,
"bulk_sku": null
}
],
"invite_only": False
......@@ -280,7 +285,8 @@ def get_course_enrollment_details(course_id, include_expired=False):
"currency": "usd",
"expiration_datetime": null,
"description": null,
"sku": null
"sku": null,
"bulk_sku": null
}
],
"invite_only": False
......
......@@ -98,3 +98,4 @@ class ModeSerializer(serializers.Serializer):
expiration_datetime = serializers.DateTimeField()
description = serializers.CharField()
sku = serializers.CharField()
bulk_sku = serializers.CharField()
......@@ -402,6 +402,7 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
mode_slug=CourseMode.HONOR,
mode_display_name=CourseMode.HONOR,
sku='123',
bulk_sku="BULK123"
)
resp = self.client.get(
reverse('courseenrollmentdetails', kwargs={"course_id": unicode(self.course.id)})
......@@ -413,6 +414,7 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
mode = data['course_modes'][0]
self.assertEqual(mode['slug'], CourseMode.HONOR)
self.assertEqual(mode['sku'], '123')
self.assertEqual(mode['bulk_sku'], 'BULK123')
self.assertEqual(mode['name'], CourseMode.HONOR)
def test_get_course_details_with_credit_course(self):
......
......@@ -504,6 +504,7 @@ def complete_course_mode_info(course_id, enrollment, modes=None):
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
mode_info['verified_bulk_sku'] = modes['verified'].bulk_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()
......
......@@ -84,11 +84,13 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase)
# TODO Verify this is the best method to create CourseMode objects.
# TODO Find/create constants for the modes.
for mode in [CourseMode.HONOR, CourseMode.VERIFIED, CourseMode.AUDIT]:
sku_string = uuid4().hex.decode('ascii')
CourseModeFactory.create(
course_id=self.course.id,
mode_slug=mode,
mode_display_name=mode,
sku=uuid4().hex.decode('ascii')
sku=sku_string,
bulk_sku='BULK-{}'.format(sku_string)
)
# Ignore events fired from UserFactory creation
......@@ -268,8 +270,9 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase)
CourseMode.objects.filter(course_id=self.course.id).delete()
mode = CourseMode.NO_ID_PROFESSIONAL_MODE
sku_string = uuid4().hex.decode('ascii')
CourseModeFactory.create(course_id=self.course.id, mode_slug=mode, mode_display_name=mode,
sku=uuid4().hex.decode('ascii'))
sku=sku_string, bulk_sku='BULK-{}'.format(sku_string))
with mock_create_basket(expect_called=False):
response = self._post_to_view()
......
......@@ -87,6 +87,7 @@ class Course(object):
merged_mode.min_price = posted_mode.min_price
merged_mode.currency = posted_mode.currency
merged_mode.sku = posted_mode.sku
merged_mode.bulk_sku = posted_mode.bulk_sku
merged_mode.expiration_datetime = posted_mode.expiration_datetime
merged_mode.save()
......
......@@ -33,7 +33,7 @@ class CourseModeSerializer(serializers.ModelSerializer):
class Meta(object):
model = CourseMode
fields = ('name', 'currency', 'price', 'sku', 'expires')
fields = ('name', 'currency', 'price', 'sku', 'bulk_sku', 'expires')
def validate_course_id(course_id):
......
......@@ -37,8 +37,14 @@ class CourseApiViewTestMixin(object):
def setUp(self):
super(CourseApiViewTestMixin, self).setUp()
self.course = CourseFactory.create()
self.course_mode = CourseMode.objects.create(course_id=self.course.id, mode_slug=u'verified', min_price=100,
currency=u'USD', sku=u'ABC123')
self.course_mode = CourseMode.objects.create(
course_id=self.course.id,
mode_slug=u'verified',
min_price=100,
currency=u'USD',
sku=u'ABC123',
bulk_sku=u'BULK-ABC123'
)
@classmethod
def _serialize_datetime(cls, dt): # pylint: disable=invalid-name
......@@ -58,6 +64,7 @@ class CourseApiViewTestMixin(object):
u'currency': course_mode.currency.lower(),
u'price': course_mode.min_price,
u'sku': course_mode.sku,
u'bulk_sku': course_mode.bulk_sku,
u'expires': cls._serialize_datetime(course_mode.expiration_datetime),
}
......@@ -147,6 +154,7 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
min_price=200,
currency=u'USD',
sku=u'ABC123',
bulk_sku=u'BULK-ABC123',
expiration_datetime=mode_expiration
)
expected = self._serialize_course(self.course, [expected_course_mode], verification_deadline)
......@@ -217,6 +225,7 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
min_price=200,
currency=u'USD',
sku=u'ABC123',
bulk_sku=u'BULK-ABC123',
expiration_datetime=None
)
updated_data = self._serialize_course(self.course, [verified_mode], None)
......@@ -251,7 +260,13 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
""" Verify that data submitted via PUT overwrites/deletes modes that are
not included in the body of the request. """
course_id = unicode(self.course.id)
expected_course_mode = CourseMode(mode_slug=u'credit', min_price=500, currency=u'USD', sku=u'ABC123')
expected_course_mode = CourseMode(
mode_slug=u'credit',
min_price=500,
currency=u'USD',
sku=u'ABC123',
bulk_sku=u'BULK-ABC123'
)
expected = self._serialize_course(self.course, [expected_course_mode])
path = reverse('commerce_api:v1:courses:retrieve_update', args=[course_id])
response = self.client.put(path, json.dumps(expected), content_type=JSON_CONTENT_TYPE)
......@@ -276,6 +291,7 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
min_price=500,
currency=u'USD',
sku=u'ABC123',
bulk_sku=u'BULK-ABC123',
expiration_datetime=expiration_datetime
)
)
......@@ -290,8 +306,22 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
def assert_can_create_course(self, **request_kwargs):
""" Verify a course can be created by the view. """
course = CourseFactory.create()
expected_modes = [CourseMode(mode_slug=u'verified', min_price=150, currency=u'USD', sku=u'ABC123'),
CourseMode(mode_slug=u'honor', min_price=0, currency=u'USD', sku=u'DEADBEEF')]
expected_modes = [
CourseMode(
mode_slug=u'verified',
min_price=150,
currency=u'USD',
sku=u'ABC123',
bulk_sku=u'BULK-ABC123'
),
CourseMode(
mode_slug=u'honor',
min_price=0,
currency=u'USD',
sku=u'DEADBEEF',
bulk_sku=u'BULK-DEADBEEF'
)
]
expected = self._serialize_course(course, expected_modes)
path = reverse('commerce_api:v1:courses:retrieve_update', args=[unicode(course.id)])
......@@ -331,7 +361,8 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
CourseMode(
mode_slug=CourseMode.DEFAULT_MODE_SLUG,
min_price=150, currency=u'USD',
sku=u'ABC123'
sku=u'ABC123',
bulk_sku=u'BULK-ABC123'
)
]
......
......@@ -54,10 +54,10 @@ class EcommerceServiceTests(TestCase):
@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."""
"""Verify that is_enabled() returns True if used for a microsite."""
is_microsite.return_value = True
is_not_enabled = EcommerceService().is_enabled(self.user)
self.assertFalse(is_not_enabled)
is_enabled = EcommerceService().is_enabled(self.user)
self.assertTrue(is_enabled)
@override_settings(ECOMMERCE_PUBLIC_URL_ROOT='http://ecommerce_url')
def test_payment_page_url(self):
......
......@@ -45,9 +45,10 @@ class EcommerceService(object):
self.config = CommerceConfiguration.current()
def is_enabled(self, user):
""" Check if the user is activated, if the service is enabled and that the site is not a microsite. """
return (user.is_active and self.config.checkout_on_ecommerce_service and not
helpers.is_request_in_themed_site())
"""
Determines the availability of the Ecommerce service based on user activation and service configuration.
"""
return user.is_active and self.config.checkout_on_ecommerce_service
def payment_page_url(self):
""" Return the URL for the checkout page.
......
......@@ -10,6 +10,7 @@ from microsite_configuration import microsite
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from shoppingcart.processors.CyberSource2 import is_user_payment_error
from django.utils.translation import ugettext as _
from openedx.core.djangoapps.theming.helpers import is_request_in_themed_site
log = logging.getLogger(__name__)
......@@ -75,5 +76,6 @@ def checkout_receipt(request):
'payment_support_email': payment_support_email,
'username': request.user.username,
'nav_hidden': True,
'is_request_in_themed_site': is_request_in_themed_site()
}
return render_to_response('commerce/checkout_receipt.html', context)
......@@ -512,7 +512,8 @@ def course_about(request, course_id):
# 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 = ''
ecommerce_bulk_checkout_link = ''
professional_mode = None
ecomm_service = EcommerceService()
if ecomm_service.is_enabled(request.user) and (
CourseMode.PROFESSIONAL in modes or CourseMode.NO_ID_PROFESSIONAL_MODE in modes
......@@ -520,7 +521,8 @@ def course_about(request, course_id):
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)
if professional_mode.bulk_sku:
ecommerce_bulk_checkout_link = ecomm_service.checkout_page_url(professional_mode.bulk_sku)
course_price = get_cosmetic_display_price(course, registration_price)
can_add_course_to_cart = _is_shopping_cart_enabled and registration_price
......@@ -555,6 +557,7 @@ def course_about(request, course_id):
'in_cart': in_cart,
'ecommerce_checkout': ecomm_service.is_enabled(request.user),
'ecommerce_checkout_link': ecommerce_checkout_link,
'ecommerce_bulk_checkout_link': ecommerce_bulk_checkout_link,
'professional_mode': professional_mode,
'reg_then_add_to_cart_link': reg_then_add_to_cart_link,
'show_courseware_link': show_courseware_link,
......
......@@ -31,7 +31,8 @@ var edx = edx || {};
var templateHtml = $("#receipt-tpl").html(),
context = {
platformName: this.$el.data('platform-name'),
verified: this.$el.data('verified').toLowerCase() === 'true'
verified: this.$el.data('verified').toLowerCase() === 'true',
is_request_in_themed_site: this.$el.data('is-request-in-themed-site').toLowerCase() === 'true'
},
providerId;
......
......@@ -52,7 +52,8 @@ from django.utils.translation import ugettext as _
<div class="container">
<section class="wrapper carousel">
<div id="receipt-container" class="pay-and-verify hidden" data-is-payment-complete='${is_payment_complete}'
data-platform-name='${platform_name}' data-verified='${verified}' data-username='${username}'>
data-platform-name='${platform_name}' data-verified='${verified}' data-username='${username}'
data-is-request-in-themed-site='${is_request_in_themed_site}'>
<h1>${_("Loading Order Data...")}</h1>
<span>${ _("Please wait while we retrieve your order details.") }</span>
</div>
......
......@@ -86,7 +86,7 @@
<% } %>
<nav class="nav-wizard is-ready">
<% if ( verified ) { %>
<% if ( verified || is_request_in_themed_site) { %>
<a class="next action-primary right" href="/dashboard"><%- gettext( "Go to Dashboard" ) %></a>
<% } else { %>
<a id="verify_later_button" class="next action-secondary verify-later nav-link" href="/dashboard" data-tooltip="<%- _.sprintf( gettext( "If you don't verify your identity now, you can still explore your course from your dashboard. You will receive periodic reminders from %(platformName)s to verify your identity." ), { platformName: platformName } ) %>">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment