Merge pull request #20884 from cpennington/discount-no-previous-purchase
REVEM-289: Discount no previous purchase
This commit is contained in:
@@ -516,7 +516,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
course_overview = CourseOverviewFactory(
|
||||
start=self.TOMORROW, self_paced=True, enrollment_end=self.TOMORROW
|
||||
)
|
||||
course_enrollment = CourseEnrollmentFactory(user=self.user)
|
||||
course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=course_overview.id)
|
||||
entitlement = CourseEntitlementFactory(user=self.user, enrollment_course_run=course_enrollment)
|
||||
course_runs = [{
|
||||
'key': six.text_type(course_overview.id),
|
||||
|
||||
@@ -14,9 +14,10 @@ from django.utils.timezone import now
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from course_modes.models import format_course_price, get_cosmetic_verified_display_price
|
||||
from course_modes.models import format_course_price, get_cosmetic_verified_display_price, CourseMode
|
||||
from courseware.access import has_staff_access_to_preview_mode
|
||||
from courseware.date_summary import verified_upgrade_deadline_link, verified_upgrade_link_is_valid
|
||||
from entitlements.models import CourseEntitlement
|
||||
from lms.djangoapps.commerce.utils import EcommerceService
|
||||
from openedx.core.djangoapps.catalog.utils import get_programs
|
||||
from openedx.core.djangoapps.django_comment_common.models import Role
|
||||
@@ -232,14 +233,12 @@ def get_dashboard_course_info(user, dashboard_enrollments):
|
||||
if DASHBOARD_INFO_FLAG.is_enabled():
|
||||
# Get the enrollments here since the dashboard filters out those with completed entitlements
|
||||
user_enrollments = CourseEnrollment.objects.select_related('course').filter(user_id=user.id)
|
||||
audit_enrollments = user_enrollments.filter(mode='audit')
|
||||
|
||||
course_info = {
|
||||
str(dashboard_enrollment.course): get_base_experiment_metadata_context(dashboard_enrollment.course,
|
||||
user,
|
||||
dashboard_enrollment,
|
||||
user_enrollments,
|
||||
audit_enrollments)
|
||||
user_enrollments)
|
||||
for dashboard_enrollment in dashboard_enrollments
|
||||
}
|
||||
return course_info
|
||||
@@ -257,8 +256,7 @@ def get_experiment_user_metadata_context(course, user):
|
||||
has_non_audit_enrollments = False
|
||||
try:
|
||||
user_enrollments = CourseEnrollment.objects.select_related('course').filter(user_id=user.id)
|
||||
audit_enrollments = user_enrollments.filter(mode='audit')
|
||||
has_non_audit_enrollments = (len(audit_enrollments) != len(user_enrollments))
|
||||
has_non_audit_enrollments = user_enrollments.exclude(mode__in=CourseMode.UPSELL_TO_VERIFIED_MODES).exists()
|
||||
# TODO: clean up as part of REVO-28 (END)
|
||||
enrollment = CourseEnrollment.objects.select_related(
|
||||
'course'
|
||||
@@ -266,7 +264,11 @@ def get_experiment_user_metadata_context(course, user):
|
||||
except CourseEnrollment.DoesNotExist:
|
||||
pass # Not enrolled, use the default values
|
||||
|
||||
context = get_base_experiment_metadata_context(course, user, enrollment, user_enrollments, audit_enrollments)
|
||||
has_entitlements = False
|
||||
if user.is_authenticated():
|
||||
has_entitlements = CourseEntitlement.objects.filter(user=user).exists()
|
||||
|
||||
context = get_base_experiment_metadata_context(course, user, enrollment, user_enrollments)
|
||||
has_staff_access = has_staff_access_to_preview_mode(user, course.id)
|
||||
forum_roles = []
|
||||
if user.is_authenticated:
|
||||
@@ -280,7 +282,7 @@ def get_experiment_user_metadata_context(course, user):
|
||||
user_partitions = {}
|
||||
|
||||
# TODO: clean up as part of REVO-28 (START)
|
||||
context['has_non_audit_enrollments'] = has_non_audit_enrollments
|
||||
context['has_non_audit_enrollments'] = has_non_audit_enrollments or has_entitlements
|
||||
# TODO: clean up as part of REVO-28 (END)
|
||||
context['has_staff_access'] = has_staff_access
|
||||
context['forum_roles'] = forum_roles
|
||||
@@ -288,14 +290,14 @@ def get_experiment_user_metadata_context(course, user):
|
||||
return context
|
||||
|
||||
|
||||
def get_base_experiment_metadata_context(course, user, enrollment, user_enrollments, audit_enrollments):
|
||||
def get_base_experiment_metadata_context(course, user, enrollment, user_enrollments):
|
||||
"""
|
||||
Return a context dictionary with the keys used by dashboard_metadata.html and user_metadata.html
|
||||
"""
|
||||
enrollment_mode = None
|
||||
enrollment_time = None
|
||||
# TODO: clean up as part of REVEM-199 (START)
|
||||
program_key = get_program_context(course, user_enrollments, audit_enrollments)
|
||||
program_key = get_program_context(course, user_enrollments)
|
||||
# TODO: clean up as part of REVEM-199 (END)
|
||||
if enrollment and enrollment.is_active:
|
||||
enrollment_mode = enrollment.mode
|
||||
@@ -332,11 +334,13 @@ def get_audit_access_expiration(user, course):
|
||||
|
||||
|
||||
# TODO: clean up as part of REVEM-199 (START)
|
||||
def get_program_context(course, user_enrollments, audit_enrollments):
|
||||
def get_program_context(course, user_enrollments):
|
||||
"""
|
||||
Return a context dictionary with program information.
|
||||
"""
|
||||
program_key = None
|
||||
non_audit_enrollments = user_enrollments.exclude(mode__in=CourseMode.UPSELL_TO_VERIFIED_MODES)
|
||||
|
||||
if PROGRAM_INFO_FLAG.is_enabled():
|
||||
programs = get_programs(course=course.id)
|
||||
if programs:
|
||||
@@ -358,7 +362,6 @@ def get_program_context(course, user_enrollments, audit_enrollments):
|
||||
# program has 3 courses (A, B and C), and the user previously purchased a certificate for A.
|
||||
# The user is enrolled in audit mode for B. The "left to purchase price" should be the price of
|
||||
# B+C.
|
||||
non_audit_enrollments = [en for en in user_enrollments if en not in audit_enrollments]
|
||||
courses_left_to_purchase = get_unenrolled_courses(courses, non_audit_enrollments)
|
||||
if courses_left_to_purchase:
|
||||
has_courses_left_to_purchase = True
|
||||
|
||||
@@ -13,10 +13,12 @@ class CourseOverviewFactory(DjangoModelFactory):
|
||||
class Meta(object):
|
||||
model = CourseOverview
|
||||
django_get_or_create = ('id', )
|
||||
exclude = ('run', )
|
||||
|
||||
version = CourseOverview.VERSION
|
||||
pre_requisite_courses = []
|
||||
org = 'edX'
|
||||
run = factory.Sequence('2012_Fall_{}'.format)
|
||||
|
||||
@factory.lazy_attribute
|
||||
def _pre_requisite_courses_json(self):
|
||||
@@ -28,7 +30,7 @@ class CourseOverviewFactory(DjangoModelFactory):
|
||||
|
||||
@factory.lazy_attribute
|
||||
def id(self):
|
||||
return CourseLocator(self.org, 'toy', '2012_Fall')
|
||||
return CourseLocator(self.org, 'toy', self.run)
|
||||
|
||||
@factory.lazy_attribute
|
||||
def display_name(self):
|
||||
|
||||
@@ -139,6 +139,8 @@ class ScheduleSendEmailTestMixin(FilteredQueryCountMixin):
|
||||
factory_kwargs.setdefault('start', target_day)
|
||||
factory_kwargs.setdefault('upgrade_deadline', upgrade_deadline)
|
||||
factory_kwargs.setdefault('enrollment__course__self_paced', True)
|
||||
# Make all schedules in the same course
|
||||
factory_kwargs.setdefault('enrollment__course__run', '2012_Fall')
|
||||
if hasattr(self, 'experience_type'):
|
||||
factory_kwargs.setdefault('experience__experience_type', self.experience_type)
|
||||
schedule = ScheduleFactory(**factory_kwargs)
|
||||
|
||||
@@ -9,8 +9,10 @@ not other discounts like coupons or enterprise/program offers configured in ecom
|
||||
|
||||
"""
|
||||
from course_modes.models import CourseMode
|
||||
from entitlements.models import CourseEntitlement
|
||||
from openedx.core.djangoapps.waffle_utils import WaffleFlag, WaffleFlagNamespace
|
||||
from openedx.features.discounts.models import DiscountRestrictionConfig
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
# .. feature_toggle_name: discounts.enable_discounting
|
||||
# .. feature_toggle_type: flag
|
||||
@@ -55,6 +57,15 @@ def can_receive_discount(user, course): # pylint: disable=unused-argument
|
||||
if DiscountRestrictionConfig.disabled_for_course_stacked_config(course):
|
||||
return False
|
||||
|
||||
# Don't allow users who have enrolled in any courses in non-upsellable
|
||||
# modes
|
||||
if CourseEnrollment.objects.filter(user=user).exclude(mode__in=CourseMode.UPSELL_TO_VERIFIED_MODES).exists():
|
||||
return False
|
||||
|
||||
# Don't allow any users who have entitlements (past or present)
|
||||
if CourseEntitlement.objects.filter(user=user).exists():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -2,19 +2,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import timedelta
|
||||
import ddt
|
||||
from django.utils.timezone import now
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from entitlements.tests.factories import CourseEntitlementFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
|
||||
from openedx.features.discounts.models import DiscountRestrictionConfig
|
||||
from student.tests.factories import UserFactory
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from ..applicability import can_receive_discount, DISCOUNT_APPLICABILITY_FLAG
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestApplicability(ModuleStoreTestCase):
|
||||
"""
|
||||
Applicability determines if this combination of user and course can receive a discount. Make
|
||||
@@ -54,3 +58,40 @@ class TestApplicability(ModuleStoreTestCase):
|
||||
DiscountRestrictionConfig.objects.create(disabled=True, course=disabled_course_overview)
|
||||
applicability = can_receive_discount(user=self.user, course=disabled_course)
|
||||
self.assertEqual(applicability, False)
|
||||
|
||||
@ddt.data(*(
|
||||
[[]] +
|
||||
[[mode] for mode in CourseMode.ALL_MODES] +
|
||||
[
|
||||
[mode1, mode2]
|
||||
for mode1 in CourseMode.ALL_MODES
|
||||
for mode2 in CourseMode.ALL_MODES
|
||||
if mode1 != mode2
|
||||
]
|
||||
))
|
||||
@override_waffle_flag(DISCOUNT_APPLICABILITY_FLAG, active=True)
|
||||
def test_can_receive_discount_previous_verified_enrollment(self, existing_enrollments):
|
||||
"""
|
||||
Ensure that only users who have not already purchased courses receive the discount.
|
||||
"""
|
||||
for mode in existing_enrollments:
|
||||
CourseEnrollmentFactory.create(mode=mode, user=self.user)
|
||||
|
||||
applicability = can_receive_discount(user=self.user, course=self.course)
|
||||
assert applicability == all(mode in CourseMode.UPSELL_TO_VERIFIED_MODES for mode in existing_enrollments)
|
||||
|
||||
@ddt.data(
|
||||
None,
|
||||
CourseMode.VERIFIED,
|
||||
CourseMode.PROFESSIONAL,
|
||||
)
|
||||
@override_waffle_flag(DISCOUNT_APPLICABILITY_FLAG, active=True)
|
||||
def test_can_receive_discount_entitlement(self, entitlement_mode):
|
||||
"""
|
||||
Ensure that only users who have not already purchased courses receive the discount.
|
||||
"""
|
||||
if entitlement_mode is not None:
|
||||
CourseEntitlementFactory.create(mode=entitlement_mode, user=self.user)
|
||||
|
||||
applicability = can_receive_discount(user=self.user, course=self.course)
|
||||
assert applicability == (entitlement_mode is None)
|
||||
|
||||
@@ -94,6 +94,7 @@ INSTALLED_APPS = (
|
||||
|
||||
# Django 1.11 demands to have imported models supported by installed apps.
|
||||
'completion',
|
||||
'entitlements',
|
||||
)
|
||||
|
||||
LMS_ROOT_URL = "http://localhost:8000"
|
||||
|
||||
Reference in New Issue
Block a user