Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
f72cf800
Commit
f72cf800
authored
Nov 17, 2017
by
Anthony Mangano
Committed by
McKenzie Welter
Nov 30, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Consider user entitlements and use entitlement products in bundle one-click purchase
parent
c25e4ba0
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
276 additions
and
66 deletions
+276
-66
common/djangoapps/course_modes/models.py
+4
-4
common/djangoapps/entitlements/models.py
+7
-0
openedx/core/djangoapps/catalog/tests/factories.py
+12
-2
openedx/core/djangoapps/programs/tests/test_utils.py
+177
-26
openedx/core/djangoapps/programs/utils.py
+76
-34
No files found.
common/djangoapps/course_modes/models.py
View file @
f72cf800
...
...
@@ -136,10 +136,10 @@ class CourseMode(models.Model):
HONOR
=
'honor'
PROFESSIONAL
=
'professional'
VERIFIED
=
"verified"
AUDIT
=
"audit"
NO_ID_PROFESSIONAL_MODE
=
"no-id-professional"
CREDIT_MODE
=
"credit"
VERIFIED
=
'verified'
AUDIT
=
'audit'
NO_ID_PROFESSIONAL_MODE
=
'no-id-professional'
CREDIT_MODE
=
'credit'
DEFAULT_MODE
=
Mode
(
settings
.
COURSE_MODE_DEFAULTS
[
'slug'
],
...
...
common/djangoapps/entitlements/models.py
View file @
f72cf800
...
...
@@ -24,3 +24,10 @@ class CourseEntitlement(TimeStampedModel):
help_text
=
'The current Course enrollment for this entitlement. If NULL the Learner has not enrolled.'
)
order_number
=
models
.
CharField
(
max_length
=
128
,
null
=
True
)
@property
def
expired_at_datetime
(
self
):
"""
Getter to be used instead of expired_at because of the conditional check and update
"""
return
self
.
expired_at
openedx/core/djangoapps/catalog/tests/factories.py
View file @
f72cf800
...
...
@@ -8,6 +8,7 @@ from faker import Faker
fake
=
Faker
()
VERIFIED_MODE
=
'verified'
def
generate_instances
(
factory_class
,
count
=
3
):
...
...
@@ -103,10 +104,18 @@ class SeatFactory(DictFactoryBase):
currency
=
'USD'
price
=
factory
.
Faker
(
'random_int'
)
sku
=
factory
.
LazyFunction
(
generate_seat_sku
)
type
=
'verified'
type
=
VERIFIED_MODE
upgrade_deadline
=
factory
.
LazyFunction
(
generate_zulu_datetime
)
class
EntitlementFactory
(
DictFactoryBase
):
currency
=
'USD'
price
=
factory
.
Faker
(
'random_int'
)
sku
=
factory
.
LazyFunction
(
generate_seat_sku
)
mode
=
VERIFIED_MODE
expires
=
None
class
CourseRunFactory
(
DictFactoryBase
):
eligible_for_financial_aid
=
True
end
=
factory
.
LazyFunction
(
generate_zulu_datetime
)
...
...
@@ -121,7 +130,7 @@ class CourseRunFactory(DictFactoryBase):
start
=
factory
.
LazyFunction
(
generate_zulu_datetime
)
status
=
'published'
title
=
factory
.
Faker
(
'catch_phrase'
)
type
=
'verified'
type
=
VERIFIED_MODE
uuid
=
factory
.
Faker
(
'uuid4'
)
content_language
=
'en'
max_effort
=
4
...
...
@@ -130,6 +139,7 @@ class CourseRunFactory(DictFactoryBase):
class
CourseFactory
(
DictFactoryBase
):
course_runs
=
factory
.
LazyFunction
(
partial
(
generate_instances
,
CourseRunFactory
))
entitlements
=
factory
.
LazyFunction
(
partial
(
generate_instances
,
EntitlementFactory
))
image
=
ImageFactory
()
key
=
factory
.
LazyFunction
(
generate_course_key
)
owners
=
factory
.
LazyFunction
(
partial
(
generate_instances
,
OrganizationFactory
,
count
=
1
))
...
...
openedx/core/djangoapps/programs/tests/test_utils.py
View file @
f72cf800
...
...
@@ -16,6 +16,7 @@ from nose.plugins.attrib import attr
from
pytz
import
utc
from
course_modes.models
import
CourseMode
from
entitlements.tests.factories
import
CourseEntitlementFactory
from
lms.djangoapps.certificates.api
import
MODES
from
lms.djangoapps.commerce.tests.test_utils
import
update_commerce_config
from
lms.djangoapps.commerce.utils
import
EcommerceService
...
...
@@ -23,6 +24,7 @@ from lms.djangoapps.grades.tests.utils import mock_passing_grade
from
openedx.core.djangoapps.catalog.tests.factories
import
(
CourseFactory
,
CourseRunFactory
,
EntitlementFactory
,
ProgramFactory
,
SeatFactory
,
generate_course_run_key
...
...
@@ -63,7 +65,7 @@ class TestProgramProgressMeter(TestCase):
def
_create_enrollments
(
self
,
*
course_run_ids
):
"""Variadic helper used to create course run enrollments."""
for
course_run_id
in
course_run_ids
:
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course_run_id
,
mode
=
'verified'
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course_run_id
,
mode
=
CourseMode
.
VERIFIED
)
def
_assert_progress
(
self
,
meter
,
*
progresses
):
"""Variadic helper used to verify progress calculations."""
...
...
@@ -225,22 +227,22 @@ class TestProgramProgressMeter(TestCase):
course_run_key
=
generate_course_run_key
()
now
=
datetime
.
datetime
.
now
(
utc
)
upgrade_deadline
=
None
if
not
offset
else
str
(
now
+
datetime
.
timedelta
(
days
=
offset
))
required_seat
=
SeatFactory
(
type
=
'verified'
,
upgrade_deadline
=
upgrade_deadline
)
enrolled_seat
=
SeatFactory
(
type
=
'audit'
)
required_seat
=
SeatFactory
(
type
=
CourseMode
.
VERIFIED
,
upgrade_deadline
=
upgrade_deadline
)
enrolled_seat
=
SeatFactory
(
type
=
CourseMode
.
AUDIT
)
seats
=
[
required_seat
,
enrolled_seat
]
data
=
[
ProgramFactory
(
courses
=
[
CourseFactory
(
course_runs
=
[
CourseRunFactory
(
key
=
course_run_key
,
type
=
'verified'
,
seats
=
seats
),
CourseRunFactory
(
key
=
course_run_key
,
type
=
CourseMode
.
VERIFIED
,
seats
=
seats
),
]),
]
)
]
mock_get_programs
.
return_value
=
data
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course_run_key
,
mode
=
'audit'
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course_run_key
,
mode
=
CourseMode
.
AUDIT
)
meter
=
ProgramProgressMeter
(
self
.
site
,
self
.
user
)
...
...
@@ -537,7 +539,9 @@ class TestProgramProgressMeter(TestCase):
Verify that the method can find course run certificates when not mocked out.
"""
mock_get_certificates_for_user
.
return_value
=
[
self
.
_make_certificate_result
(
status
=
'downloadable'
,
type
=
'verified'
,
course_key
=
'downloadable-course'
),
self
.
_make_certificate_result
(
status
=
'downloadable'
,
type
=
CourseMode
.
VERIFIED
,
course_key
=
'downloadable-course'
),
self
.
_make_certificate_result
(
status
=
'generating'
,
type
=
'honor'
,
course_key
=
'generating-course'
),
self
.
_make_certificate_result
(
status
=
'unknown'
,
course_key
=
'unknown-course'
),
]
...
...
@@ -546,7 +550,7 @@ class TestProgramProgressMeter(TestCase):
self
.
assertEqual
(
meter
.
completed_course_runs
,
[
{
'course_run_id'
:
'downloadable-course'
,
'type'
:
'verified'
},
{
'course_run_id'
:
'downloadable-course'
,
'type'
:
CourseMode
.
VERIFIED
},
{
'course_run_id'
:
'generating-course'
,
'type'
:
'honor'
},
]
)
...
...
@@ -558,9 +562,10 @@ class TestProgramProgressMeter(TestCase):
Verify that 'no-id-professional' certificates are treated as if they were
'professional' certificates when determining program completion.
"""
# Create serialized course runs like the ones we expect to receive from
# the discovery service's API. These runs are of type 'professional'.
course_runs
=
CourseRunFactory
.
create_batch
(
2
,
type
=
'professional'
)
# Create serialized course runs like the ones we expect to receive from the discovery service's API.
# These runs are of type 'professional' because there is no seat type for no-id-professional;
# it uses professional as the seat type instead.
course_runs
=
CourseRunFactory
.
create_batch
(
2
,
type
=
CourseMode
.
PROFESSIONAL
)
program
=
ProgramFactory
(
courses
=
[
CourseFactory
(
course_runs
=
course_runs
)])
mock_get_programs
.
return_value
=
[
program
]
...
...
@@ -571,7 +576,9 @@ class TestProgramProgressMeter(TestCase):
# Grant a 'no-id-professional' certificate for one of the course runs,
# thereby completing the program.
mock_get_certificates_for_user
.
return_value
=
[
self
.
_make_certificate_result
(
status
=
'downloadable'
,
type
=
'no-id-professional'
,
course_key
=
course_runs
[
0
][
'key'
])
self
.
_make_certificate_result
(
status
=
'downloadable'
,
type
=
CourseMode
.
NO_ID_PROFESSIONAL_MODE
,
course_key
=
course_runs
[
0
][
'key'
]
)
]
# Verify that the program is complete.
...
...
@@ -592,7 +599,7 @@ class TestProgramProgressMeter(TestCase):
mock_get_programs
.
return_value
=
[
program
]
self
.
_create_enrollments
(
course_run_key
)
meter
=
ProgramProgressMeter
(
self
.
site
,
self
.
user
)
mock_completed_course_runs
.
return_value
=
[{
'course_run_id'
:
course_run_key
,
'type'
:
'verified'
}]
mock_completed_course_runs
.
return_value
=
[{
'course_run_id'
:
course_run_key
,
'type'
:
CourseMode
.
VERIFIED
}]
self
.
assertEqual
(
meter
.
_is_course_complete
(
course
),
True
)
def
test_course_grade_results
(
self
,
mock_get_programs
):
...
...
@@ -628,7 +635,7 @@ class TestProgramProgressMeter(TestCase):
self
.
assertEqual
(
meter
.
progress
(
count_only
=
False
),
expected
)
def
_create_course
(
self
,
course_price
,
course_run_count
=
1
):
def
_create_course
(
self
,
course_price
,
course_run_count
=
1
,
make_entitlement
=
False
):
"""
Creates the course in mongo and update it with the instructor data.
Also creates catalog course with respect to course run.
...
...
@@ -646,8 +653,9 @@ def _create_course(self, course_price, course_run_count=1):
run
=
CourseRunFactory
(
key
=
unicode
(
course
.
id
),
seats
=
[
SeatFactory
(
price
=
course_price
)])
course_runs
.
append
(
run
)
entitlements
=
[
EntitlementFactory
()]
if
make_entitlement
else
[]
return
CourseFactory
(
course_runs
=
course_runs
)
return
CourseFactory
(
course_runs
=
course_runs
,
entitlements
=
entitlements
)
@ddt.ddt
...
...
@@ -879,12 +887,12 @@ class TestProgramDataExtender(ModuleStoreTestCase):
course1
=
_create_course
(
self
,
self
.
course_price
)
course2
=
_create_course
(
self
,
self
.
course_price
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course1
[
'course_runs'
][
0
][
'key'
],
mode
=
'verified'
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course2
[
'course_runs'
][
0
][
'key'
],
mode
=
'audit'
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course1
[
'course_runs'
][
0
][
'key'
],
mode
=
CourseMode
.
VERIFIED
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course2
[
'course_runs'
][
0
][
'key'
],
mode
=
CourseMode
.
AUDIT
)
program2
=
ProgramFactory
(
courses
=
[
course1
,
course2
],
is_program_eligible_for_one_click_purchase
=
True
,
applicable_seat_types
=
[
'verified'
],
applicable_seat_types
=
[
CourseMode
.
VERIFIED
],
)
data
=
ProgramDataExtender
(
program2
,
self
.
user
)
.
extend
()
self
.
assertTrue
(
data
[
'is_learner_eligible_for_one_click_purchase'
])
...
...
@@ -897,12 +905,12 @@ class TestProgramDataExtender(ModuleStoreTestCase):
"""
course1
=
_create_course
(
self
,
self
.
course_price
,
course_run_count
=
2
)
course2
=
_create_course
(
self
,
self
.
course_price
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course1
[
'course_runs'
][
0
][
'key'
],
mode
=
'verified'
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course1
[
'course_runs'
][
0
][
'key'
],
mode
=
CourseMode
.
VERIFIED
)
course1
[
'course_runs'
][
0
][
'status'
]
=
'unpublished'
program2
=
ProgramFactory
(
courses
=
[
course1
,
course2
],
is_program_eligible_for_one_click_purchase
=
True
,
applicable_seat_types
=
[
'verified'
],
applicable_seat_types
=
[
CourseMode
.
VERIFIED
],
)
data
=
ProgramDataExtender
(
program2
,
self
.
user
)
.
extend
()
self
.
assertEqual
(
len
(
data
[
'skus'
]),
1
)
...
...
@@ -915,12 +923,13 @@ class TestProgramDataExtender(ModuleStoreTestCase):
This test is primarily for the case of no-id-professional enrollment modes
"""
course1
=
_create_course
(
self
,
self
.
course_price
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course1
[
'course_runs'
][
0
][
'key'
],
mode
=
'no-id-professional'
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course1
[
'course_runs'
][
0
][
'key'
],
mode
=
CourseMode
.
NO_ID_PROFESSIONAL_MODE
)
program2
=
ProgramFactory
(
courses
=
[
course1
],
is_program_eligible_for_one_click_purchase
=
True
,
applicable_seat_types
=
[
'professional'
],
# There is no seat type for no-id-professional, it
# instead uses professional
applicable_seat_types
=
[
CourseMode
.
PROFESSIONAL
]
)
data
=
ProgramDataExtender
(
program2
,
self
.
user
)
.
extend
()
self
.
assertFalse
(
data
[
'is_learner_eligible_for_one_click_purchase'
])
...
...
@@ -938,7 +947,7 @@ class TestProgramDataExtender(ModuleStoreTestCase):
key
=
str
(
ModuleStoreCourseFactory
()
.
id
),
status
=
'published'
)
course
=
CourseFactory
(
course_runs
=
[
course_run_1
,
course_run_2
])
course
=
CourseFactory
(
course_runs
=
[
course_run_1
,
course_run_2
]
,
entitlements
=
[]
)
program
=
ProgramFactory
(
courses
=
[
CourseFactory
(
course_runs
=
[
...
...
@@ -956,7 +965,7 @@ class TestProgramDataExtender(ModuleStoreTestCase):
])
],
is_program_eligible_for_one_click_purchase
=
True
,
applicable_seat_types
=
[
'verified'
]
applicable_seat_types
=
[
CourseMode
.
VERIFIED
]
)
data
=
ProgramDataExtender
(
program
,
self
.
user
)
.
extend
()
...
...
@@ -967,6 +976,147 @@ class TestProgramDataExtender(ModuleStoreTestCase):
self
.
assertTrue
(
data
[
'is_learner_eligible_for_one_click_purchase'
])
def
test_learner_eligibility_for_one_click_purchase_entitlement_products
(
self
):
"""
Learner should be eligible for one click purchase if:
- program is eligible for one click purchase
- There are remaining unpurchased courses with entitlement products
"""
course1
=
_create_course
(
self
,
self
.
course_price
,
course_run_count
=
2
,
make_entitlement
=
True
)
course2
=
_create_course
(
self
,
self
.
course_price
,
course_run_count
=
2
,
make_entitlement
=
True
)
expected_skus
=
set
([
course1
[
'entitlements'
][
0
][
'sku'
],
course2
[
'entitlements'
][
0
][
'sku'
]])
program
=
ProgramFactory
(
courses
=
[
course1
,
course2
],
is_program_eligible_for_one_click_purchase
=
True
,
applicable_seat_types
=
[
CourseMode
.
VERIFIED
],
)
data
=
ProgramDataExtender
(
program
,
self
.
user
)
.
extend
()
self
.
assertTrue
(
data
[
'is_learner_eligible_for_one_click_purchase'
])
self
.
assertEqual
(
set
(
data
[
'skus'
]),
expected_skus
)
def
test_learner_eligibility_for_one_click_purchase_ineligible_program
(
self
):
"""
Learner should not be eligible for one click purchase if the program is not eligible for one click purchase
"""
course1
=
_create_course
(
self
,
self
.
course_price
,
course_run_count
=
2
,
make_entitlement
=
True
)
course2
=
_create_course
(
self
,
self
.
course_price
,
course_run_count
=
2
,
make_entitlement
=
True
)
program
=
ProgramFactory
(
courses
=
[
course1
,
course2
],
is_program_eligible_for_one_click_purchase
=
False
,
applicable_seat_types
=
[
CourseMode
.
VERIFIED
],
)
data
=
ProgramDataExtender
(
program
,
self
.
user
)
.
extend
()
self
.
assertFalse
(
data
[
'is_learner_eligible_for_one_click_purchase'
])
self
.
assertEqual
(
data
[
'skus'
],
[])
def
test_learner_eligibility_for_one_click_purchase_user_entitlements
(
self
):
"""
Learner should be eligibile for one click purchase if they hold an entitlement in one or more courses
in the program and there are remaining unpurchased courses in the program with entitlement products.
"""
course1
=
_create_course
(
self
,
self
.
course_price
,
course_run_count
=
2
,
make_entitlement
=
True
)
course2
=
_create_course
(
self
,
self
.
course_price
,
course_run_count
=
2
,
make_entitlement
=
True
)
CourseEntitlementFactory
(
user
=
self
.
user
,
course_uuid
=
course1
[
'uuid'
],
mode
=
CourseMode
.
VERIFIED
)
expected_skus
=
set
([
course2
[
'entitlements'
][
0
][
'sku'
]])
program
=
ProgramFactory
(
courses
=
[
course1
,
course2
],
is_program_eligible_for_one_click_purchase
=
True
,
applicable_seat_types
=
[
CourseMode
.
VERIFIED
],
)
data
=
ProgramDataExtender
(
program
,
self
.
user
)
.
extend
()
self
.
assertTrue
(
data
[
'is_learner_eligible_for_one_click_purchase'
])
self
.
assertEqual
(
set
(
data
[
'skus'
]),
expected_skus
)
def
test_all_courses_owned
(
self
):
"""
Learner should not be eligible for one click purchase if they hold entitlements in all courses in the program.
"""
course1
=
_create_course
(
self
,
self
.
course_price
,
make_entitlement
=
True
)
course2
=
_create_course
(
self
,
self
.
course_price
)
CourseEntitlementFactory
(
user
=
self
.
user
,
course_uuid
=
course1
[
'uuid'
],
mode
=
CourseMode
.
VERIFIED
)
CourseEntitlementFactory
(
user
=
self
.
user
,
course_uuid
=
course2
[
'uuid'
],
mode
=
CourseMode
.
VERIFIED
)
program
=
ProgramFactory
(
courses
=
[
course1
,
course2
],
is_program_eligible_for_one_click_purchase
=
True
,
applicable_seat_types
=
[
CourseMode
.
VERIFIED
],
)
data
=
ProgramDataExtender
(
program
,
self
.
user
)
.
extend
()
self
.
assertFalse
(
data
[
'is_learner_eligible_for_one_click_purchase'
])
self
.
assertEqual
(
data
[
'skus'
],
[])
def
test_entitlement_product_wrong_mode
(
self
):
"""
Learner should not be eligible for one click purchase if the only entitlement product
for a course in the program is not in an applicable mode, and that course has multiple course runs.
"""
course1
=
_create_course
(
self
,
self
.
course_price
)
course2
=
_create_course
(
self
,
self
.
course_price
,
course_run_count
=
2
)
course2
[
'entitlements'
]
.
append
(
EntitlementFactory
(
mode
=
CourseMode
.
PROFESSIONAL
))
program
=
ProgramFactory
(
courses
=
[
course1
,
course2
],
is_program_eligible_for_one_click_purchase
=
True
,
applicable_seat_types
=
[
CourseMode
.
VERIFIED
],
)
data
=
ProgramDataExtender
(
program
,
self
.
user
)
.
extend
()
self
.
assertFalse
(
data
[
'is_learner_eligible_for_one_click_purchase'
])
self
.
assertEqual
(
data
[
'skus'
],
[])
def
test_second_entitlement_product_wrong_mode
(
self
):
"""
Learner should be eligible for one click purchase if a course has multiple entitlement products
and at least one of them is in an applicable mode, even if one is not in an applicable mode.
"""
course1
=
_create_course
(
self
,
self
.
course_price
)
course2
=
_create_course
(
self
,
self
.
course_price
,
course_run_count
=
2
,
make_entitlement
=
True
)
# The above statement makes a verfied entitlement for the course, which is an applicable seat type
# and the statement below makes a professional entitlement for the same course, which is not applicable
course2
[
'entitlements'
]
.
append
(
EntitlementFactory
(
mode
=
CourseMode
.
PROFESSIONAL
))
expected_skus
=
set
([
course1
[
'course_runs'
][
0
][
'seats'
][
0
][
'sku'
],
course2
[
'entitlements'
][
0
][
'sku'
]])
program
=
ProgramFactory
(
courses
=
[
course1
,
course2
],
is_program_eligible_for_one_click_purchase
=
True
,
applicable_seat_types
=
[
CourseMode
.
VERIFIED
],
)
data
=
ProgramDataExtender
(
program
,
self
.
user
)
.
extend
()
self
.
assertTrue
(
data
[
'is_learner_eligible_for_one_click_purchase'
])
self
.
assertEqual
(
set
(
data
[
'skus'
]),
expected_skus
)
def
test_entitlement_product_and_user_enrollment
(
self
):
"""
Learner should be eligible for one click purchase if they hold an enrollment
but not an entitlement in a course for which there exists an entitlement product.
"""
course1
=
_create_course
(
self
,
self
.
course_price
,
make_entitlement
=
True
)
course2
=
_create_course
(
self
,
self
.
course_price
)
expected_skus
=
set
([
course2
[
'course_runs'
][
0
][
'seats'
][
0
][
'sku'
]])
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course1
[
'course_runs'
][
0
][
'key'
],
mode
=
CourseMode
.
VERIFIED
)
program
=
ProgramFactory
(
courses
=
[
course1
,
course2
],
is_program_eligible_for_one_click_purchase
=
True
,
applicable_seat_types
=
[
CourseMode
.
VERIFIED
],
)
data
=
ProgramDataExtender
(
program
,
self
.
user
)
.
extend
()
self
.
assertTrue
(
data
[
'is_learner_eligible_for_one_click_purchase'
])
self
.
assertEqual
(
set
(
data
[
'skus'
]),
expected_skus
)
def
test_user_enrollment_with_other_course_entitlement_product
(
self
):
"""
Learner should be eligible for one click purchase if they hold an enrollment in one course of the program
and there is an entitlement product for another course in the program.
"""
course1
=
_create_course
(
self
,
self
.
course_price
,
course_run_count
=
2
)
course2
=
_create_course
(
self
,
self
.
course_price
,
course_run_count
=
2
,
make_entitlement
=
True
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course1
[
'course_runs'
][
0
][
'key'
],
mode
=
CourseMode
.
VERIFIED
)
expected_skus
=
set
([
course2
[
'entitlements'
][
0
][
'sku'
]])
program
=
ProgramFactory
(
courses
=
[
course1
,
course2
],
is_program_eligible_for_one_click_purchase
=
True
,
applicable_seat_types
=
[
CourseMode
.
VERIFIED
,
CourseMode
.
PROFESSIONAL
],
)
data
=
ProgramDataExtender
(
program
,
self
.
user
)
.
extend
()
self
.
assertTrue
(
data
[
'is_learner_eligible_for_one_click_purchase'
])
self
.
assertEqual
(
set
(
data
[
'skus'
]),
expected_skus
)
@skip_unless_lms
@mock.patch
(
UTILS_MODULE
+
'.get_credentials'
)
...
...
@@ -1095,7 +1245,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
self
.
number_of_courses
=
2
self
.
program
=
ProgramFactory
(
courses
=
[
_create_course
(
self
,
self
.
course_price
)
for
__
in
range
(
self
.
number_of_courses
)],
applicable_seat_types
=
[
'verified'
]
applicable_seat_types
=
[
CourseMode
.
VERIFIED
]
)
def
_prepare_program_for_discounted_price_calculation_endpoint
(
self
):
...
...
@@ -1212,8 +1362,9 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
body
=
json
.
dumps
(
mock_discount_data
),
content_type
=
'application/json'
)
user
=
AnonymousUserFactory
()
data
=
ProgramMarketingDataExtender
(
self
.
program
,
AnonymousUserFactory
()
)
.
extend
()
data
=
ProgramMarketingDataExtender
(
self
.
program
,
user
)
.
extend
()
self
.
_update_discount_data
(
mock_discount_data
)
self
.
assertEqual
(
...
...
openedx/core/djangoapps/programs/utils.py
View file @
f72cf800
...
...
@@ -460,57 +460,99 @@ class ProgramDataExtender(object):
def
_attach_course_run_may_certify
(
self
,
run_mode
):
run_mode
[
'may_certify'
]
=
self
.
course_overview
.
may_certify
()
def
_check_enrollment_for_user
(
self
,
course_run
):
applicable_seat_types
=
self
.
data
[
'applicable_seat_types'
]
def
_filter_out_courses_with_entitlements
(
self
,
courses
):
"""
Removes courses for which the current user already holds an applicable entitlement.
TODO:
Add a NULL value of enrollment_course_run to filter, as courses with entitlements spent on applicable
enrollments will already have been filtered out by _filter_out_courses_with_enrollments.
(
enrollment_mode
,
active
)
=
CourseEnrollment
.
enrollment_mode_for_user
(
self
.
user
,
CourseKey
.
from_string
(
course_run
[
'key'
])
Arguments:
courses (list): Containing dicts representing courses in a program
Returns:
A subset of the given list of course dicts
"""
course_uuids
=
set
(
course
[
'uuid'
]
for
course
in
courses
)
# Filter the entitlements' modes with a case-insensitive match against applicable seat_types
entitlements
=
self
.
user
.
courseentitlement_set
.
filter
(
mode__in
=
self
.
data
[
'applicable_seat_types'
],
course_uuid__in
=
course_uuids
,
)
# Here we check the entitlements' expired_at_datetime property rather than filter by the expired_at attribute
# to ensure that the expiration status is as up to date as possible
entitlements
=
[
e
for
e
in
entitlements
if
not
e
.
expired_at_datetime
]
courses_with_entitlements
=
set
(
unicode
(
entitlement
.
course_uuid
)
for
entitlement
in
entitlements
)
return
[
course
for
course
in
courses
if
course
[
'uuid'
]
not
in
courses_with_entitlements
]
is_paid_seat
=
False
if
enrollment_mode
is
not
None
and
active
is
not
None
and
active
is
True
:
# Check all the applicable seat types
# this will also check for no-id-professional as professional
is_paid_seat
=
any
(
seat_type
in
enrollment_mode
for
seat_type
in
applicable_seat_types
)
def
_filter_out_courses_with_enrollments
(
self
,
courses
):
"""
Removes courses for which the current user already holds an active and applicable enrollment
for one of that course's runs.
return
is_paid_seat
Arguments:
courses (list): Containing dicts representing courses in a program
Returns:
A subset of the given list of course dicts
"""
enrollments
=
self
.
user
.
courseenrollment_set
.
filter
(
is_active
=
True
,
mode__in
=
self
.
data
[
'applicable_seat_types'
]
)
course_runs_with_enrollments
=
set
(
unicode
(
enrollment
.
course_id
)
for
enrollment
in
enrollments
)
courses_without_enrollments
=
[]
for
course
in
courses
:
if
all
(
unicode
(
run
[
'key'
])
not
in
course_runs_with_enrollments
for
run
in
course
[
'course_runs'
]):
courses_without_enrollments
.
append
(
course
)
return
courses_without_enrollments
def
_collect_one_click_purchase_eligibility_data
(
self
):
"""
Extend the program data with data about learner's eligibility for one click purchase,
discount data of the program and SKUs of seats that should be added to basket.
"""
applicable_seat_types
=
self
.
data
[
'applicable_seat_types'
]
if
'professional'
in
self
.
data
[
'applicable_seat_types'
]:
self
.
data
[
'applicable_seat_types'
]
.
append
(
'no-id-professional'
)
applicable_seat_types
=
set
(
seat
for
seat
in
self
.
data
[
'applicable_seat_types'
]
if
seat
!=
'credit'
)
is_learner_eligible_for_one_click_purchase
=
self
.
data
[
'is_program_eligible_for_one_click_purchase'
]
skus
=
[]
bundle_variant
=
'full'
if
is_learner_eligible_for_one_click_purchase
:
for
course
in
self
.
data
[
'courses'
]:
add_course_sku
=
True
course_runs
=
course
.
get
(
'course_runs'
,
[])
published_course_runs
=
filter
(
lambda
run
:
run
[
'status'
]
==
'published'
,
course_runs
)
if
len
(
published_course_runs
)
==
1
:
for
course_run
in
course_runs
:
is_paid_seat
=
self
.
_check_enrollment_for_user
(
course_run
)
if
is_paid_seat
:
add_course_sku
=
False
break
if
add_course_sku
:
if
is_learner_eligible_for_one_click_purchase
:
courses
=
self
.
data
[
'courses'
]
if
not
self
.
user
.
is_anonymous
():
courses
=
self
.
_filter_out_courses_with_enrollments
(
courses
)
courses
=
self
.
_filter_out_courses_with_entitlements
(
courses
)
if
len
(
courses
)
<
len
(
self
.
data
[
'courses'
]):
bundle_variant
=
'partial'
for
course
in
courses
:
entitlement_product
=
False
for
entitlement
in
course
.
get
(
'entitlements'
,
[]):
# We add the first entitlement product found with an applicable seat type because, at this time,
# we are assuming that, for any given course, there is at most one paid entitlement available.
if
entitlement
[
'mode'
]
in
applicable_seat_types
:
skus
.
append
(
entitlement
[
'sku'
])
entitlement_product
=
True
break
if
not
entitlement_product
:
course_runs
=
course
.
get
(
'course_runs'
,
[])
published_course_runs
=
[
run
for
run
in
course_runs
if
run
[
'status'
]
==
'published'
]
if
len
(
published_course_runs
)
==
1
:
for
seat
in
published_course_runs
[
0
][
'seats'
]:
if
seat
[
'type'
]
in
applicable_seat_types
and
seat
[
'sku'
]:
skus
.
append
(
seat
[
'sku'
])
break
else
:
bundle_variant
=
'partial'
else
:
# If a course in the program has more than 1 published course run
# learner won't be eligible for a one click purchase.
is_learner_eligible_for_one_click_purchase
=
False
skus
=
[]
break
# If a course in the program has more than 1 published course run
# learner won't be eligible for a one click purchase.
skus
=
[]
break
if
skus
:
try
:
...
...
@@ -604,7 +646,7 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
def
__init__
(
self
,
program_data
,
user
):
super
(
ProgramMarketingDataExtender
,
self
)
.
__init__
(
program_data
,
user
)
# Aggregate list of instructors for the program
# Aggregate list of instructors for the program
keyed by name
self
.
instructors
=
[]
# Values for programs' price calculation.
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment