Commit 33330966 by Brandon DeRosier

ENT-134 Add consent gate when redeeming coupons from enterprise customers

parent c1b5a22b
......@@ -283,6 +283,15 @@ class SiteConfiguration(models.Model):
"""
return urljoin(self.lms_url_root, path)
def build_enterprise_service_url(self, path=''):
"""
Returns path joined with the appropriate Enterprise service URL root for the current site.
Returns:
str
"""
return urljoin(settings.ENTERPRISE_SERVICE_URL, path)
@property
def commerce_api_url(self):
""" Returns the URL for the root of the Commerce API (hosted by LMS). """
......@@ -311,7 +320,12 @@ class SiteConfiguration(models.Model):
@property
def enterprise_api_url(self):
""" Returns the URL for the Enterprise service. """
return self.build_lms_url('/enterprise/api/v1')
return settings.ENTERPRISE_API_URL
@property
def enterprise_grant_data_sharing_url(self):
""" Returns the URL for the Enterprise data sharing permission view. """
return self.build_enterprise_service_url('grant_data_sharing_permissions')
@property
def access_token(self):
......@@ -365,7 +379,7 @@ class SiteConfiguration(models.Model):
EdxRestApiClient: The client to access the Enterprise service.
"""
return EdxRestApiClient(settings.ENTERPRISE_API_URL, jwt=self.access_token)
return EdxRestApiClient(self.enterprise_api_url, jwt=self.access_token)
@cached_property
def user_api_client(self):
......
......@@ -18,6 +18,11 @@ from ecommerce.core.url_utils import get_lms_url
from ecommerce.coupons.tests.mixins import CouponMixin
from ecommerce.coupons.views import voucher_is_valid
from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.enterprise.tests.mixins import EnterpriseServiceMockMixin
from ecommerce.enterprise.utils import (
get_enterprise_course_consent_url,
get_enterprise_customer_data_sharing_consent_token,
)
from ecommerce.extensions.api import exceptions
from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin
from ecommerce.extensions.test.factories import prepare_voucher
......@@ -25,6 +30,7 @@ from ecommerce.extensions.voucher.utils import get_voucher_and_products_from_cod
from ecommerce.tests.mixins import ApiMockMixin, LmsApiMockMixin
from ecommerce.tests.testcases import TestCase
Applicator = get_class('offer.utils', 'Applicator')
Basket = get_model('basket', 'Basket')
Benefit = get_model('offer', 'Benefit')
......@@ -38,6 +44,7 @@ VoucherApplication = get_model('voucher', 'VoucherApplication')
CONTENT_TYPE = 'application/json'
COUPON_CODE = 'COUPONTEST'
ENTERPRISE_CUSTOMER = 'cf246b88-d5f6-4908-a522-fc307e0b0c59'
class CouponAppViewTests(TestCase):
......@@ -264,7 +271,8 @@ class CouponOfferViewTests(ApiMockMixin, CouponMixin, CourseCatalogTestMixin, Lm
self.assertEqual(response.status_code, 200)
class CouponRedeemViewTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockMixin, TestCase):
class CouponRedeemViewTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockMixin, EnterpriseServiceMockMixin,
TestCase):
redeem_url = reverse('coupons:redeem')
def setUp(self):
......@@ -283,27 +291,40 @@ class CouponRedeemViewTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockMixin
self.catalog.stock_records.add(StockRecord.objects.get(product=self.seat))
self.student_dashboard_url = get_lms_url(self.site.siteconfiguration.student_dashboard_url)
def redeem_url_with_params(self, code=COUPON_CODE):
def redeem_url_with_params(self, code=COUPON_CODE, consent_token=None):
""" Constructs the coupon redemption URL with the proper string query parameters. """
return self.redeem_url + '?code={}&sku={}'.format(code, self.stock_record.partner_sku)
def create_and_test_coupon_and_return_code(self, benefit_value=90, code=COUPON_CODE, email_domains=None):
params = {
'code': code,
'sku': self.stock_record.partner_sku,
}
if consent_token is not None:
params['consent_token'] = consent_token
return '{base}?{params}'.format(base=self.redeem_url, params=urllib.urlencode(params))
def create_and_test_coupon_and_return_code(
self,
benefit_value=90,
code=COUPON_CODE,
email_domains=None,
enterprise_customer=None
):
""" Creates coupon and returns code. """
coupon = self.create_coupon(
benefit_value=benefit_value,
catalog=self.catalog,
code=code,
email_domains=email_domains
email_domains=email_domains,
enterprise_customer=enterprise_customer
)
coupon_code = coupon.attr.coupon_vouchers.vouchers.first().code
self.assertEqual(Voucher.objects.filter(code=coupon_code).count(), 1)
return coupon_code
def assert_redemption_page_redirects(self, expected_url, target=200, code=COUPON_CODE):
def assert_redemption_page_redirects(self, expected_url, target=200, code=COUPON_CODE, consent_token=None):
""" Verify redirect from redeem page to expected page. """
self.request.user = self.user
self.mock_enrollment_api(self.request, self.user, self.course.id, is_active=False, mode=self.course_mode)
response = self.client.get(self.redeem_url_with_params(code=code))
response = self.client.get(self.redeem_url_with_params(code=code, consent_token=consent_token))
self.assertRedirects(response, expected_url, status_code=302, target_status_code=target)
def test_login_required(self):
......@@ -376,6 +397,107 @@ class CouponRedeemViewTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockMixin
)
@httpretty.activate
def test_enterprise_customer_redirect_no_consent(self):
""" Verify the view redirects to LMS when an enrollment code is provided. """
code = self.create_and_test_coupon_and_return_code(
benefit_value=100,
code='',
enterprise_customer=ENTERPRISE_CUSTOMER
)
self.request.user = self.user
self.mock_enrollment_api(self.request, self.user, self.course.id, is_active=False, mode=self.course_mode)
self.mock_account_api(self.request, self.user.username, data={'is_active': True})
self.mock_access_token_response()
self.mock_specific_enterprise_customer_api(ENTERPRISE_CUSTOMER)
consent_token = get_enterprise_customer_data_sharing_consent_token(
self.request.user.access_token,
self.course.id,
ENTERPRISE_CUSTOMER
)
expected_url = get_enterprise_course_consent_url(
self.site,
code,
self.stock_record.partner_sku,
consent_token,
self.course.id,
ENTERPRISE_CUSTOMER
)
response = self.client.get(self.redeem_url_with_params(code=code))
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
self.assertEqual(response.url, expected_url)
@httpretty.activate
def test_enterprise_customer_invalid_consent_token(self):
""" Verify that the view renders an error when the consent token doesn't match. """
code = self.create_and_test_coupon_and_return_code(
benefit_value=100,
code='',
enterprise_customer=ENTERPRISE_CUSTOMER
)
self.request.user = self.user
self.mock_enrollment_api(self.request, self.user, self.course.id, is_active=False, mode=self.course_mode)
self.mock_account_api(self.request, self.user.username, data={'is_active': True})
self.mock_access_token_response()
self.mock_specific_enterprise_customer_api(ENTERPRISE_CUSTOMER)
response = self.client.get(self.redeem_url_with_params(code=code, consent_token='invalid_consent_token'))
self.assertEqual(response.context['error'], 'Invalid data sharing consent token provided.')
@httpretty.activate
def test_enterprise_customer_does_not_exist(self):
"""
Verify that a generic error is rendered when the corresponding EnterpriseCustomer doesn't exist
on the Enterprise service.
"""
code = self.create_and_test_coupon_and_return_code(
benefit_value=100,
code='',
enterprise_customer=ENTERPRISE_CUSTOMER
)
self.request.user = self.user
self.mock_enrollment_api(self.request, self.user, self.course.id, is_active=False, mode=self.course_mode)
self.mock_account_api(self.request, self.user.username, data={'is_active': True})
self.mock_access_token_response()
self.mock_enterprise_customer_api_not_found(ENTERPRISE_CUSTOMER)
response = self.client.get(self.redeem_url_with_params(code=code))
self.assertEqual(response.context['error'], 'Couldn\'t find a matching Enterprise Customer for this coupon.')
@httpretty.activate
def test_enterprise_customer_successful_redemption(self):
""" Verify the view redirects to LMS when valid consent is provided. """
code = self.create_and_test_coupon_and_return_code(
benefit_value=100,
code='',
enterprise_customer=ENTERPRISE_CUSTOMER
)
self.request.user = self.user
self.mock_enrollment_api(self.request, self.user, self.course.id, is_active=False, mode=self.course_mode)
self.mock_account_api(self.request, self.user.username, data={'is_active': True})
self.mock_access_token_response()
self.mock_specific_enterprise_customer_api(ENTERPRISE_CUSTOMER)
self.mock_enterprise_learner_api_for_learner_with_no_enterprise()
self.mock_enterprise_learner_post_api()
consent_token = get_enterprise_customer_data_sharing_consent_token(
self.request.user.access_token,
self.course.id,
ENTERPRISE_CUSTOMER
)
self.assert_redemption_page_redirects(
self.student_dashboard_url,
target=status.HTTP_301_MOVED_PERMANENTLY,
code=code,
consent_token=consent_token,
)
last_request = httpretty.last_request()
self.assertEqual(last_request.path, '/api/enrollment/v1/enrollment')
self.assertEqual(last_request.method, 'POST')
@httpretty.activate
def test_multiple_vouchers(self):
""" Verify a redirect to LMS happens when a basket with already existing vouchers is used. """
code = self.create_and_test_coupon_and_return_code(benefit_value=100, code='')
......
......@@ -19,6 +19,12 @@ from ecommerce.core.url_utils import get_ecommerce_url
from ecommerce.core.views import StaffOnlyMixin
from ecommerce.coupons.decorators import login_required_for_credit
from ecommerce.extensions.api import exceptions
from ecommerce.enterprise.exceptions import EnterpriseDoesNotExist
from ecommerce.enterprise.utils import (
get_enterprise_course_consent_url,
get_enterprise_customer_data_sharing_consent_token,
get_enterprise_customer_from_voucher,
)
from ecommerce.extensions.basket.utils import prepare_basket
from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin
from ecommerce.extensions.voucher.utils import get_voucher_and_products_from_code
......@@ -26,6 +32,7 @@ from ecommerce.extensions.voucher.utils import get_voucher_and_products_from_cod
Applicator = get_class('offer.utils', 'Applicator')
Basket = get_model('basket', 'Basket')
Benefit = get_model('offer', 'Benefit')
ConditionalOffer = get_model('offer', 'ConditionalOffer')
logger = logging.getLogger(__name__)
OrderLineVouchers = get_model('voucher', 'OrderLineVouchers')
Order = get_model('order', 'Order')
......@@ -149,7 +156,7 @@ class CouponRedeemView(EdxOrderPlacementMixin, View):
if not voucher.offers.first().is_email_valid(request.user.email):
return render(request, template_name, {'error': _('You are not eligible to use this coupon.')})
if not request.user.account_details(request)['is_active']:
if not request.user.account_details(request).get('is_active'):
return render(
request,
template_name,
......@@ -159,6 +166,47 @@ class CouponRedeemView(EdxOrderPlacementMixin, View):
if request.user.is_user_already_enrolled(request, product):
return render(request, template_name, {'error': _('You are already enrolled in the course.')})
try:
enterprise_customer = get_enterprise_customer_from_voucher(request.site, request.user.access_token, voucher)
except EnterpriseDoesNotExist as e:
# If an EnterpriseException is caught while pulling the EnterpriseCustomer, that means there's no
# corresponding EnterpriseCustomer in the Enterprise service (which should never happen).
logger.exception(e.message)
return render(
request,
template_name,
{'error': _('Couldn\'t find a matching Enterprise Customer for this coupon.')}
)
if enterprise_customer is not None and enterprise_customer.get('enable_data_sharing_consent'):
consent_token = get_enterprise_customer_data_sharing_consent_token(
request.user.access_token,
product.course.id,
enterprise_customer['id']
)
received_consent_token = request.GET.get('consent_token')
if received_consent_token:
# If the consent token is set, then the user is returning from the consent view. Render out an error
# if the computed token doesn't match the one received from the redirect URL.
if received_consent_token != consent_token:
return render(
request,
template_name,
{'error': _('Invalid data sharing consent token provided.')}
)
else:
# The user hasn't been redirected to the interstitial consent view to collect consent, so
# redirect them now.
redirect_url = get_enterprise_course_consent_url(
request.site,
code,
sku,
consent_token,
product.course.id,
enterprise_customer['id']
)
return HttpResponseRedirect(redirect_url)
basket = prepare_basket(request, product, voucher)
if basket.total_excl_tax == 0:
self.place_free_order(basket)
......
"""
Exceptions used by the Enterprise app.
"""
class EnterpriseDoesNotExist(Exception):
"""
Exception for errors related to Enterprise service data.
"""
pass
......@@ -9,6 +9,9 @@ class EnterpriseServiceMockMixin(object):
"""
Mocks for the Open edX service 'Enterprise Service' responses.
"""
ENTERPRISE_CUSTOMER_URL = '{}enterprise-customer/'.format(
settings.ENTERPRISE_API_URL,
)
ENTERPRISE_LEARNER_URL = '{}enterprise-learner/'.format(
settings.ENTERPRISE_API_URL,
)
......@@ -17,7 +20,68 @@ class EnterpriseServiceMockMixin(object):
super(EnterpriseServiceMockMixin, self).setUp()
cache.clear()
def mock_enterprise_learner_api(self, catalog_id=1, entitlement_id=1, learner_id=1):
def mock_specific_enterprise_customer_api(self, uuid):
"""
Helper function to register the enterprise customer API endpoint.
"""
enterprise_customer_api_response = {
'uuid': uuid,
'name': 'TestShib',
'catalog': 0,
'active': True,
'site': {
'domain': 'example.com',
'name': 'example.com'
},
'enable_data_sharing_consent': True,
'enforce_data_sharing_consent': 'at_login',
'enterprise_customer_users': [
1
],
'branding_configuration': {
'enterprise_customer': 'cf246b88-d5f6-4908-a522-fc307e0b0c59',
'logo': 'https://open.edx.org/sites/all/themes/edx_open/logo.png'
},
'enterprise_customer_entitlements': [
{
'enterprise_customer': 'cf246b88-d5f6-4908-a522-fc307e0b0c59',
'entitlement_id': 0
}
]
}
enterprise_customer_api_response_json = json.dumps(enterprise_customer_api_response)
httpretty.register_uri(
method=httpretty.GET,
uri='{}{}/'.format(self.ENTERPRISE_CUSTOMER_URL, uuid),
body=enterprise_customer_api_response_json,
content_type='application/json'
)
def mock_enterprise_customer_api_not_found(self, uuid):
"""
Helper function to register the enterprise customer API endpoint.
"""
enterprise_customer_api_response = {
'detail': 'Not found.'
}
enterprise_customer_api_response_json = json.dumps(enterprise_customer_api_response)
httpretty.register_uri(
method=httpretty.GET,
uri='{}{}/'.format(self.ENTERPRISE_CUSTOMER_URL, uuid),
body=enterprise_customer_api_response_json,
content_type='application/json',
status=404,
)
def mock_enterprise_learner_api(
self,
catalog_id=1,
entitlement_id=1,
learner_id=1,
enterprise_customer_uuid='cf246b88-d5f6-4908-a522-fc307e0b0c59'
):
"""
Helper function to register enterprise learner API endpoint.
"""
......@@ -29,7 +93,7 @@ class EnterpriseServiceMockMixin(object):
{
'id': learner_id,
'enterprise_customer': {
'uuid': 'cf246b88-d5f6-4908-a522-fc307e0b0c59',
'uuid': enterprise_customer_uuid,
'name': 'TestShib',
'catalog': catalog_id,
'active': True,
......@@ -43,12 +107,12 @@ class EnterpriseServiceMockMixin(object):
1
],
'branding_configuration': {
'enterprise_customer': 'cf246b88-d5f6-4908-a522-fc307e0b0c59',
'enterprise_customer': enterprise_customer_uuid,
'logo': 'https://open.edx.org/sites/all/themes/edx_open/logo.png'
},
'enterprise_customer_entitlements': [
{
'enterprise_customer': 'cf246b88-d5f6-4908-a522-fc307e0b0c59',
'enterprise_customer': enterprise_customer_uuid,
'entitlement_id': entitlement_id
}
]
......@@ -85,6 +149,23 @@ class EnterpriseServiceMockMixin(object):
content_type='application/json'
)
def mock_enterprise_learner_post_api(self):
"""
Helper function to register the enterprise learner POST API endpoint.
"""
enterprise_learner_api_response = {
'enterprise_customer': 'cf246b88-d5f6-4908-a522-fc307e0b0c59',
'username': 'the_j_meister',
}
enterprise_learner_api_response_json = json.dumps(enterprise_learner_api_response)
httpretty.register_uri(
method=httpretty.POST,
uri=self.ENTERPRISE_LEARNER_URL,
body=enterprise_learner_api_response_json,
content_type='application/json'
)
def mock_enterprise_learner_api_for_learner_with_no_enterprise(self):
"""
Helper function to register enterprise learner API endpoint for a
......
from __future__ import unicode_literals
import ddt
import httpretty
from ecommerce.core.tests.decorators import mock_enterprise_api_client
from ecommerce.enterprise.utils import get_enterprise_customer, get_or_create_enterprise_customer_user
from ecommerce.enterprise.tests.mixins import EnterpriseServiceMockMixin
from ecommerce.tests.testcases import TestCase
TEST_ENTERPRISE_CUSTOMER_UUID = 'cf246b88-d5f6-4908-a522-fc307e0b0c59'
@ddt.ddt
@httpretty.activate
class EnterpriseUtilsTests(EnterpriseServiceMockMixin, TestCase):
def setUp(self):
super(EnterpriseUtilsTests, self).setUp()
self.learner = self.create_user(is_staff=True)
self.client.login(username=self.learner.username, password=self.password)
def test_get_enterprise_customer(self):
"""
Verify that "get_enterprise_customer" returns an appropriate response from the
"enterprise-customer" Enterprise service API endpoint.
"""
self.mock_specific_enterprise_customer_api(TEST_ENTERPRISE_CUSTOMER_UUID)
response = get_enterprise_customer(self.site, self.learner.access_token, TEST_ENTERPRISE_CUSTOMER_UUID)
self.assertEqual(TEST_ENTERPRISE_CUSTOMER_UUID, response.get('id'))
@mock_enterprise_api_client
@ddt.data(
(
['mock_enterprise_learner_api'],
{'user_id': 5},
),
(
[
'mock_enterprise_learner_api_for_learner_with_no_enterprise',
'mock_enterprise_learner_post_api',
],
{
'enterprise_customer': 'cf246b88-d5f6-4908-a522-fc307e0b0c59',
'username': 'the_j_meister',
},
)
)
@ddt.unpack
def test_post_enterprise_customer_user(self, mock_helpers, expected_return):
"""
Verify that "get_enterprise_customer" returns an appropriate response from the
"enterprise-customer" Enterprise service API endpoint.
"""
for mock in mock_helpers:
getattr(self, mock)()
response = get_or_create_enterprise_customer_user(
self.site,
TEST_ENTERPRISE_CUSTOMER_UUID,
self.learner.username
)
self.assertDictContainsSubset(expected_return, response)
"""
Helper methods for enterprise app.
"""
import hashlib
import hmac
from urllib import urlencode
from django.conf import settings
from django.core.urlresolvers import reverse
from edx_rest_api_client.client import EdxRestApiClient
from oscar.core.loading import get_model
import waffle
from slumber.exceptions import HttpNotFoundError
from ecommerce.courses.utils import traverse_pagination
from ecommerce.enterprise.exceptions import EnterpriseDoesNotExist
ConditionalOffer = get_model('offer', 'ConditionalOffer')
def is_enterprise_feature_enabled():
......@@ -20,3 +33,134 @@ def is_enterprise_feature_enabled():
"""
is_enterprise_enabled = waffle.switch_is_active(settings.ENABLE_ENTERPRISE_ON_RUNTIME_SWITCH)
return is_enterprise_enabled
def get_enterprise_customer(site, token, uuid):
"""
Return a single enterprise customer
"""
client = EdxRestApiClient(
site.siteconfiguration.enterprise_api_url,
oauth_access_token=token
)
path = ['enterprise-customer', str(uuid)]
client = reduce(getattr, path, client)
try:
response = client.get()
except HttpNotFoundError:
return None
return {
'name': response['name'],
'id': response['uuid'],
'enable_data_sharing_consent': response['enable_data_sharing_consent'],
}
def get_enterprise_customers(site, token):
resource = 'enterprise-customer'
client = EdxRestApiClient(
site.siteconfiguration.enterprise_api_url,
oauth_access_token=token
)
endpoint = getattr(client, resource)
response = endpoint.get()
return [
{
'name': each['name'],
'id': each['uuid'],
}
for each in traverse_pagination(response, endpoint)
]
def get_or_create_enterprise_customer_user(site, enterprise_customer_uuid, username):
"""
Create a new EnterpriseCustomerUser on the enterprise service if one doesn't already exist.
Return the EnterpriseCustomerUser data.
"""
data = {
'enterprise_customer': str(enterprise_customer_uuid),
'username': username,
}
api_resource_name = 'enterprise-learner'
api = site.siteconfiguration.enterprise_api_client
endpoint = getattr(api, api_resource_name)
get_response = endpoint.get(**data)
if get_response.get('count') == 1:
result = get_response['results'][0]
return result
response = endpoint.post(data)
return response
def get_enterprise_customer_from_voucher(site, token, voucher):
"""
Given a Voucher, find the associated Enterprise Customer and retrieve data about
that customer from the Enterprise service. If there is no Enterprise Customer
associated with the Voucher, `None` is returned.
"""
try:
offer = voucher.offers.get(benefit__range__enterprise_customer__isnull=False)
except ConditionalOffer.DoesNotExist:
# There's no Enterprise Customer associated with this voucher.
return None
# Get information about the enterprise customer from the Enterprise service.
enterprise_customer_uuid = offer.benefit.range.enterprise_customer
enterprise_customer = get_enterprise_customer(site, token, enterprise_customer_uuid)
if enterprise_customer is None:
raise EnterpriseDoesNotExist(
'Enterprise customer with UUID {uuid} does not exist in the Enterprise service.'.format(
uuid=enterprise_customer_uuid
)
)
return enterprise_customer
def get_enterprise_course_consent_url(site, code, sku, consent_token, course_id, enterprise_customer_uuid):
"""
Construct the URL that should be used for redirecting the user to the Enterprise service for
collecting consent. The URL contains a specially crafted "next" parameter that will result
in the user being redirected back to the coupon redemption view with the verified consent token.
"""
callback_url = '{protocol}://{domain}{resource}?{params}'.format(
protocol=settings.PROTOCOL,
domain=site.domain,
resource=reverse('coupons:redeem'),
params=urlencode({
'code': code,
'sku': sku,
'consent_token': consent_token,
})
)
request_params = {
'course_id': course_id,
'enterprise_id': enterprise_customer_uuid,
'enrollment_deferred': True,
'next': callback_url,
}
redirect_url = '{base}?{params}'.format(
base=site.siteconfiguration.enterprise_grant_data_sharing_url,
params=urlencode(request_params)
)
return redirect_url
def get_enterprise_customer_data_sharing_consent_token(access_token, course_id, enterprise_customer_uuid):
"""
Generate a sha256 hmac token unique to an end-user Access Token, Course, and
Enterprise Customer combination.
"""
consent_token_hmac = hmac.new(
str(access_token),
'{course_id}_{enterprise_uuid}'.format(
course_id=course_id,
enterprise_uuid=enterprise_customer_uuid,
),
digestmod=hashlib.sha256,
)
return consent_token_hmac.hexdigest()
......@@ -23,7 +23,7 @@ class TestEnterpriseCustomerView(TestCase):
]
}
@mock.patch('ecommerce.extensions.api.v2.views.enterprise.EdxRestApiClient')
@mock.patch('ecommerce.enterprise.utils.EdxRestApiClient')
def test_get_customers(self, mock_client):
instance = mock_client.return_value
setattr(
......
from edx_rest_api_client.client import EdxRestApiClient
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated, IsAdminUser
from rest_framework.response import Response
from ecommerce.courses.utils import traverse_pagination
from ecommerce.enterprise.utils import get_enterprise_customers
class EnterpriseCustomerViewSet(generics.GenericAPIView):
......@@ -13,20 +12,3 @@ class EnterpriseCustomerViewSet(generics.GenericAPIView):
def get(self, request):
site = request.site
return Response(data={'results': get_enterprise_customers(site, token=request.user.access_token)})
def get_enterprise_customers(site, token):
resource = 'enterprise-customer'
client = EdxRestApiClient(
site.siteconfiguration.enterprise_api_url,
oauth_access_token=token
)
endpoint = getattr(client, resource)
response = endpoint.get()
return [
{
'name': each['name'],
'id': each['uuid'],
}
for each in traverse_pagination(response, endpoint)
]
......@@ -19,6 +19,7 @@ from ecommerce.core.constants import ENROLLMENT_CODE_PRODUCT_CLASS_NAME, SEAT_PR
from ecommerce.core.url_utils import get_lms_enrollment_api_url
from ecommerce.courses.models import Course
from ecommerce.courses.utils import mode_for_seat
from ecommerce.enterprise.utils import get_or_create_enterprise_customer_user
from ecommerce.extensions.analytics.utils import audit_log, parse_tracking_context
from ecommerce.extensions.checkout.utils import get_receipt_page_url
from ecommerce.extensions.fulfillment.status import LINE
......@@ -209,6 +210,25 @@ class EnrollmentFulfillmentModule(BaseFulfillmentModule):
}
)
try:
# Collect the EnterpriseCustomer UUID from the coupon, if any.
enterprise_customer_uuid = None
for discount in order.discounts.all():
enterprise_customer_uuid = discount.voucher.benefit.range.enterprise_customer
if enterprise_customer_uuid is not None:
data['enterprise_course_consent'] = True
break
# If an EnterpriseCustomer UUID is associated with the coupon, create an EnterpriseCustomerUser
# on the Enterprise service if one doesn't already exist.
if enterprise_customer_uuid is not None:
get_or_create_enterprise_customer_user(
order.site,
enterprise_customer_uuid,
order.user.username
)
# Post to the Enrollment API. The LMS will take care of posting a new EnterpriseCourseEnrollment to
# the Enterprise service if the user+course has a corresponding EnterpriseCustomerUser.
response = self._post_to_enrollment_api(data, user=order.user)
if response.status_code == status.HTTP_200_OK:
......
......@@ -576,8 +576,8 @@ AFFILIATE_COOKIE_KEY = 'affiliate_id'
CRISPY_TEMPLATE_PACK = 'bootstrap3'
# ENTERPRISE APP CONFIGURATION
# URL for Enterprise service API
ENTERPRISE_API_URL = 'http://localhost:8000/enterprise/api/v1/'
# URL for Enterprise service
ENTERPRISE_SERVICE_URL = 'http://localhost:8000/enterprise/'
# Cache enterprise response from Enterprise API.
ENTERPRISE_API_CACHE_TIMEOUT = 3600 # Value is in seconds
......
......@@ -3,6 +3,7 @@ from ecommerce.settings.production import *
DEBUG = True
ENABLE_AUTO_AUTH = True
PROTOCOL = 'http'
# Docker does not support the syslog socket at /dev/log. Rely on the console.
LOGGING['handlers']['local'] = {
......
"""Development settings and globals."""
from __future__ import absolute_import
from urlparse import urljoin
from ecommerce.settings.base import *
......@@ -120,3 +121,5 @@ ENABLE_AUTO_AUTH = True
# Lastly, see if the developer has any local overrides.
if os.path.isfile(join(dirname(abspath(__file__)), 'private.py')):
from .private import * # pylint: disable=import-error
ENTERPRISE_API_URL = urljoin(ENTERPRISE_SERVICE_URL, 'api/v1/')
"""Production settings and globals."""
from os import environ
from urlparse import urljoin
# Normally you should not import ANYTHING from Django directly
# into your settings, but ImproperlyConfigured is an exception.
......@@ -10,6 +11,9 @@ import yaml
from ecommerce.settings.base import *
# Protocol used for construcing absolute callback URLs
PROTOCOL = 'https'
# Enable offline compression of CSS/JS
COMPRESS_ENABLED = True
COMPRESS_OFFLINE = True
......@@ -79,3 +83,5 @@ for __, configs in PAYMENT_PROCESSOR_CONFIG.iteritems():
'error_path': PAYMENT_PROCESSOR_ERROR_PATH,
})
# END PAYMENT PROCESSOR OVERRIDES
ENTERPRISE_API_URL = urljoin(ENTERPRISE_SERVICE_URL, 'api/v1/')
from __future__ import absolute_import
from path import Path
from urlparse import urljoin
from ecommerce.settings.base import *
SITE_ID = 1
PROTOCOL = 'http'
# TEST SETTINGS
INSTALLED_APPS += (
......@@ -142,3 +144,5 @@ COMPREHENSIVE_THEME_DIRS = [
]
DEFAULT_SITE_THEME = "test-theme"
ENTERPRISE_API_URL = urljoin(ENTERPRISE_SERVICE_URL, 'api/v1/')
......@@ -20,7 +20,7 @@ edx-django-sites-extensions==1.0.0
edx-drf-extensions==1.2.2
edx-ecommerce-worker==0.6.0
edx-opaque-keys==0.3.1
edx-rest-api-client==1.6.0
edx-rest-api-client==1.7.1
jsonfield==1.0.3
libsass==0.9.2
ndg-httpsclient==0.4.0
......
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