Skip to content

Commit

Permalink
Otto checkout flow
Browse files Browse the repository at this point in the history
  • Loading branch information
vkaracic committed Feb 17, 2016
1 parent eb914a0 commit 86a4710
Show file tree
Hide file tree
Showing 15 changed files with 322 additions and 61 deletions.
52 changes: 34 additions & 18 deletions common/djangoapps/course_modes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion common/djangoapps/student/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 9 additions & 1 deletion common/djangoapps/student/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)


Expand Down
8 changes: 8 additions & 0 deletions lms/djangoapps/commerce/admin.py
Original file line number Diff line number Diff line change
@@ -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)
32 changes: 32 additions & 0 deletions lms/djangoapps/commerce/migrations/0002_commerceconfiguration.py
Original file line number Diff line number Diff line change
@@ -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,
},
),
]
24 changes: 23 additions & 1 deletion lms/djangoapps/commerce/models.py
Original file line number Diff line number Diff line change
@@ -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"
47 changes: 46 additions & 1 deletion lms/djangoapps/commerce/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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)
31 changes: 31 additions & 0 deletions lms/djangoapps/commerce/utils.py
Original file line number Diff line number Diff line change
@@ -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__)

Expand Down Expand Up @@ -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)
Loading

0 comments on commit 86a4710

Please sign in to comment.