Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Checkout on Otto #11503

Merged
merged 1 commit into from
Feb 17, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use unicode(course_key).

),
"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(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like here :)

})

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extend this and update the list view to display the current values. Our current list views are not useful. Bonus points if you can update ConfigurationModelAdmin to make this happen for all configuration models.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please be more specific which values should be displayed in the admin panel. This is what I currently see: http://i.imgur.com/exUJHyr.png

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, you're all set.

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 """

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know I've asked about this before, but I just want to ask again if this is the right location/time to add a foreign key back to the Site.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And if so, maybe the name of this model should be SiteCommerceConfiguration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is not my code, I'd like to include @clintonb to weigh in.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may have initially wrote this code as a prototype; but, if you are submitting the PR @vkaracic, it's your code.

That being said, yes, this would be the place to add multi-tenancy support. That should be tested extensively (obviously).

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