Merge pull request #20258 from edx/emma-green/REVEM-282/A/Add-discount-applicability-endpoint
Add applicability endpoint
This commit is contained in:
@@ -2279,6 +2279,7 @@ INSTALLED_APPS = [
|
||||
'openedx.features.learner_profile',
|
||||
'openedx.features.course_duration_limits',
|
||||
'openedx.features.content_type_gating',
|
||||
'openedx.features.discounts',
|
||||
|
||||
'experiments',
|
||||
|
||||
|
||||
@@ -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'):
|
||||
|
||||
5
openedx/features/discounts/__init__.py
Normal file
5
openedx/features/discounts/__init__.py
Normal 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.
|
||||
"""
|
||||
45
openedx/features/discounts/applicability.py
Normal file
45
openedx/features/discounts/applicability.py
Normal 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
|
||||
11
openedx/features/discounts/apps.py
Normal file
11
openedx/features/discounts/apps.py
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
"""
|
||||
Discounts application configuration
|
||||
"""
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DiscountsConfig(AppConfig):
|
||||
name = 'openedx.features.discounts'
|
||||
0
openedx/features/discounts/tests/__init__.py
Normal file
0
openedx/features/discounts/tests/__init__.py
Normal file
25
openedx/features/discounts/tests/test_applicability.py
Normal file
25
openedx/features/discounts/tests/test_applicability.py
Normal 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)
|
||||
55
openedx/features/discounts/tests/test_views.py
Normal file
55
openedx/features/discounts/tests/test_views.py
Normal 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
|
||||
11
openedx/features/discounts/urls.py
Normal file
11
openedx/features/discounts/urls.py
Normal 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'),
|
||||
]
|
||||
73
openedx/features/discounts/views.py
Normal file
73
openedx/features/discounts/views.py
Normal 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)})
|
||||
Reference in New Issue
Block a user