Merge pull request #20258 from edx/emma-green/REVEM-282/A/Add-discount-applicability-endpoint

Add applicability endpoint
This commit is contained in:
emma-green
2019-05-10 11:23:57 -04:00
committed by GitHub
10 changed files with 227 additions and 0 deletions

View File

@@ -2279,6 +2279,7 @@ INSTALLED_APPS = [
'openedx.features.learner_profile',
'openedx.features.course_duration_limits',
'openedx.features.content_type_gating',
'openedx.features.discounts',
'experiments',

View File

@@ -139,6 +139,7 @@ urlpatterns = [
url(r'^dashboard/', include('learner_dashboard.urls')),
url(r'^api/experiments/', include('experiments.urls', namespace='api_experiments')),
url(r'^api/discounts/', include('openedx.features.discounts.urls', namespace='api_discounts')),
]
if settings.FEATURES.get('ENABLE_MOBILE_REST_API'):

View File

@@ -0,0 +1,5 @@
"""
Discounts are determined by a combination of user and course, and have a one to one relationship with the enrollment
(if already enrolled) or a join table of user and course. They are determined in LMS, because all of the data for
the business rules exists here. Discount rules are meant to be permanent.
"""

View File

@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
"""
Contains code related to computing discount percentage
and discount applicability.
"""
from openedx.core.djangoapps.waffle_utils import WaffleFlag, WaffleFlagNamespace
# .. feature_toggle_name: discounts.enable_discounting
# .. feature_toggle_type: flag
# .. feature_toggle_default: False
# .. feature_toggle_description: Toggle discounts always being disabled
# .. feature_toggle_category: discounts
# .. feature_toggle_use_cases: monitored_rollout
# .. feature_toggle_creation_date: 2019-4-16
# .. feature_toggle_expiration_date: None
# .. feature_toggle_warnings: None
# .. feature_toggle_tickets: REVEM-282
# .. feature_toggle_status: supported
DISCOUNT_APPLICABILITY_FLAG = WaffleFlag(
waffle_namespace=WaffleFlagNamespace(name=u'discounts'),
flag_name=u'enable_discounting',
flag_undefined_default=False
)
def can_recieve_discount(user, course_key_string): # pylint: disable=unused-argument
"""
Check all the business logic about whether this combination of user and course
can recieve a discount.
"""
# Always disable discounts until we are ready to enable this feature
if not DISCOUNT_APPLICABILITY_FLAG.is_enabled():
return False
# TODO: Add additional conditions to return False here
return True
def discount_percentage():
"""
Get the configured discount amount.
"""
# TODO: Add configuration information here
return 15

View File

@@ -0,0 +1,11 @@
"""
Discounts application configuration
"""
# -*- coding: utf-8 -*-
from django.apps import AppConfig
class DiscountsConfig(AppConfig):
name = 'openedx.features.discounts'

View File

@@ -0,0 +1,25 @@
"""Tests of openedx.features.discounts.applicability"""
# -*- coding: utf-8 -*-
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from ..applicability import can_recieve_discount
class TestApplicability(ModuleStoreTestCase):
"""
Applicability determines if this combination of user and course can receive a discount. Make
sure that all of the business conditions work.
"""
def setUp(self):
super(TestApplicability, self).setUp()
self.user = UserFactory.create()
self.course = CourseFactory.create(run='test', display_name='test')
def test_can_recieve_discount(self):
# Right now, no one should be able to recieve the discount
applicability = can_recieve_discount(user=self.user, course_key_string=self.course.id)
self.assertEqual(applicability, False)

View File

@@ -0,0 +1,55 @@
"""Tests of openedx.features.discounts.views"""
# -*- coding: utf-8 -*-
import jwt
from django.test.client import Client
from django.urls import reverse
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from student.tests.factories import UserFactory, TEST_PASSWORD
class TestCourseUserDiscount(ModuleStoreTestCase):
"""
CourseUserDiscount should return a jwt with the information if this combination of user and
course can receive a discount, and how much that discount should be.
"""
def setUp(self):
super(TestCourseUserDiscount, self).setUp()
self.user = UserFactory.create()
self.course = CourseFactory.create(run='test', display_name='test')
self.client = Client()
self.url = reverse('api_discounts:course_user_discount', kwargs={'course_key_string': unicode(self.course.id)})
def test_url(self):
"""
Test that the url hasn't changed
"""
assert self.url == ('/api/discounts/course/' + unicode(self.course.id))
def test_course_user_discount(self):
"""
Test that the api returns a jwt with the discount information
"""
self.client.login(username=self.user.username, password=TEST_PASSWORD)
# the endpoint should return a 200 if all goes well
response = self.client.get(self.url)
assert response.status_code == 200
# for now, it should always return false
expected_payload = {'discount_applicable': False, 'discount_percent': 15}
assert expected_payload['discount_applicable'] == response.data['discount_applicable']
# make sure that the response matches the expected response
response_payload = jwt.decode(response.data['jwt'], verify=False)
assert all(item in response_payload.items() for item in expected_payload.items())
def test_course_user_discount_no_user(self):
"""
Test that the endpoint returns a 401 if there is no user signed in
"""
# the endpoint should return a 401 because the user is not logged in
response = self.client.get(self.url)
assert response.status_code == 401

View File

@@ -0,0 +1,11 @@
"""
Discount API URLs
"""
from django.conf import settings
from django.conf.urls import url
from .views import CourseUserDiscount
urlpatterns = [
url(r'^course/{}'.format(settings.COURSE_KEY_PATTERN), CourseUserDiscount.as_view(), name='course_user_discount'),
]

View File

@@ -0,0 +1,73 @@
"""
The Discount API Views should return information about discounts that apply to the user and course.
"""
# -*- coding: utf-8 -*-
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
from openedx.core.djangoapps.cors_csrf.decorators import ensure_csrf_cookie_cross_domain
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
from openedx.core.lib.api.permissions import ApiKeyHeaderPermissionIsAuthenticated
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin
from rest_framework.response import Response
from rest_framework.views import APIView
from django.utils.decorators import method_decorator
from .applicability import can_recieve_discount, discount_percentage
class CourseUserDiscount(DeveloperErrorViewMixin, APIView):
"""
**Use Cases**
Request discount information for a user and course
**Example Requests**
GET /api/discounts/v1/course/{course_key_string}
**Response Values**
Body consists of the following fields:
discount_applicable:
whether the user can recieve a discount for this course
jwt:
the jwt with user information and discount information
**Parameters:**
course_key_string:
The course key for the which the discount should be applied
**Returns**
* 200 on success with above fields.
* 401 if there is no user signed in.
Example response:
{
"discount_applicable": false,
"jwt": xxxxxxxx.xxxxxxxx.xxxxxxx
}
"""
authentication_classes = (JwtAuthentication, OAuth2AuthenticationAllowInactiveUser,
SessionAuthenticationAllowInactiveUser,)
permission_classes = (ApiKeyHeaderPermissionIsAuthenticated,)
# Since the course about page on the marketing site uses this API to auto-enroll users,
# we need to support cross-domain CSRF.
@method_decorator(ensure_csrf_cookie_cross_domain)
def get(self, request, course_key_string):
"""
Return the discount percent, if the user has appropriate permissions.
"""
discount_applicable = can_recieve_discount(user=request.user, course_key_string=course_key_string)
discount_percent = discount_percentage()
payload = {'discount_applicable': discount_applicable, 'discount_percent': discount_percent}
return Response({
'discount_applicable': discount_applicable,
'jwt': create_jwt_for_user(request.user, additional_claims=payload)})