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
bf164ae9
Commit
bf164ae9
authored
Jun 08, 2017
by
Marko Jevtic
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[LEARNER-1183] Prepare program data to be presented on program marketing page
[LEARNER-1393] Filter program course runs by status
parent
9c4869c1
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
281 additions
and
21 deletions
+281
-21
lms/djangoapps/courseware/views/views.py
+6
-1
openedx/core/djangoapps/catalog/tests/factories.py
+58
-4
openedx/core/djangoapps/programs/tests/test_utils.py
+153
-7
openedx/core/djangoapps/programs/utils.py
+64
-9
No files found.
lms/djangoapps/courseware/views/views.py
View file @
bf164ae9
...
@@ -801,8 +801,13 @@ def program_marketing(request, program_uuid):
...
@@ -801,8 +801,13 @@ def program_marketing(request, program_uuid):
if
not
program_data
:
if
not
program_data
:
raise
Http404
raise
Http404
program
=
ProgramMarketingDataExtender
(
program_data
,
request
.
user
)
.
extend
()
skus
=
program
.
get
(
'skus'
)
ecommerce_service
=
EcommerceService
()
return
render_to_response
(
'courseware/program_marketing.html'
,
{
return
render_to_response
(
'courseware/program_marketing.html'
,
{
'program'
:
ProgramMarketingDataExtender
(
program_data
,
request
.
user
)
.
extend
()
'buy_button_href'
:
ecommerce_service
.
get_checkout_page_url
(
*
skus
)
if
skus
else
'#courses'
,
'program'
:
program
,
})
})
...
...
openedx/core/djangoapps/catalog/tests/factories.py
View file @
bf164ae9
...
@@ -3,6 +3,7 @@
...
@@ -3,6 +3,7 @@
from
functools
import
partial
from
functools
import
partial
import
factory
import
factory
import
uuid
from
faker
import
Faker
from
faker
import
Faker
...
@@ -34,6 +35,19 @@ def generate_zulu_datetime():
...
@@ -34,6 +35,19 @@ def generate_zulu_datetime():
return
fake
.
date_time
()
.
isoformat
()
+
'Z'
return
fake
.
date_time
()
.
isoformat
()
+
'Z'
def
generate_price_ranges
():
return
[{
'currency'
:
'USD'
,
'max'
:
1000
,
'min'
:
100
,
'total'
:
500
}]
def
generate_seat_sku
():
return
uuid
.
uuid4
()
.
hex
[:
7
]
.
upper
()
class
DictFactoryBase
(
factory
.
Factory
):
class
DictFactoryBase
(
factory
.
Factory
):
"""
"""
Subclass this to make factories that can be used to produce fake API response
Subclass this to make factories that can be used to produce fake API response
...
@@ -77,12 +91,15 @@ class OrganizationFactory(DictFactoryBase):
...
@@ -77,12 +91,15 @@ class OrganizationFactory(DictFactoryBase):
key
=
factory
.
Faker
(
'word'
)
key
=
factory
.
Faker
(
'word'
)
name
=
factory
.
Faker
(
'company'
)
name
=
factory
.
Faker
(
'company'
)
uuid
=
factory
.
Faker
(
'uuid4'
)
uuid
=
factory
.
Faker
(
'uuid4'
)
logo_image_url
=
factory
.
Faker
(
'image_url'
)
class
SeatFactory
(
DictFactoryBase
):
class
SeatFactory
(
DictFactoryBase
):
type
=
factory
.
Faker
(
'word'
)
price
=
factory
.
Faker
(
'random_int'
)
currency
=
'USD'
currency
=
'USD'
price
=
factory
.
Faker
(
'random_int'
)
sku
=
factory
.
LazyFunction
(
generate_seat_sku
)
type
=
'verified'
upgrade_deadline
=
factory
.
LazyFunction
(
generate_zulu_datetime
)
class
CourseRunFactory
(
DictFactoryBase
):
class
CourseRunFactory
(
DictFactoryBase
):
...
@@ -91,13 +108,13 @@ class CourseRunFactory(DictFactoryBase):
...
@@ -91,13 +108,13 @@ class CourseRunFactory(DictFactoryBase):
enrollment_end
=
factory
.
LazyFunction
(
generate_zulu_datetime
)
enrollment_end
=
factory
.
LazyFunction
(
generate_zulu_datetime
)
enrollment_start
=
factory
.
LazyFunction
(
generate_zulu_datetime
)
enrollment_start
=
factory
.
LazyFunction
(
generate_zulu_datetime
)
image
=
ImageFactory
()
image
=
ImageFactory
()
is_enrolled
=
False
key
=
factory
.
LazyFunction
(
generate_course_run_key
)
key
=
factory
.
LazyFunction
(
generate_course_run_key
)
marketing_url
=
factory
.
Faker
(
'url'
)
marketing_url
=
factory
.
Faker
(
'url'
)
pacing_type
=
'self_paced'
pacing_type
=
'self_paced'
seats
=
factory
.
LazyFunction
(
partial
(
generate_instances
,
SeatFactory
))
seats
=
factory
.
LazyFunction
(
partial
(
generate_instances
,
SeatFactory
))
short_description
=
factory
.
Faker
(
'sentence'
)
short_description
=
factory
.
Faker
(
'sentence'
)
start
=
factory
.
LazyFunction
(
generate_zulu_datetime
)
start
=
factory
.
LazyFunction
(
generate_zulu_datetime
)
status
=
'published'
title
=
factory
.
Faker
(
'catch_phrase'
)
title
=
factory
.
Faker
(
'catch_phrase'
)
type
=
'verified'
type
=
'verified'
uuid
=
factory
.
Faker
(
'uuid4'
)
uuid
=
factory
.
Faker
(
'uuid4'
)
...
@@ -112,20 +129,57 @@ class CourseFactory(DictFactoryBase):
...
@@ -112,20 +129,57 @@ class CourseFactory(DictFactoryBase):
uuid
=
factory
.
Faker
(
'uuid4'
)
uuid
=
factory
.
Faker
(
'uuid4'
)
class
JobOutlookItemFactory
(
DictFactoryBase
):
value
=
factory
.
Faker
(
'sentence'
)
class
PersonFactory
(
DictFactoryBase
):
bio
=
factory
.
Faker
(
'paragraphs'
)
given_name
=
factory
.
Faker
(
'first_name'
)
family_name
=
factory
.
Faker
(
'last_name'
)
profile_image_url
=
factory
.
Faker
(
'image_url'
)
uuid
=
factory
.
Faker
(
'uuid4'
)
class
EndorserFactory
(
DictFactoryBase
):
person
=
PersonFactory
()
quote
=
factory
.
Faker
(
'sentence'
)
class
ExpectedLearningItemFactory
(
DictFactoryBase
):
value
=
factory
.
Faker
(
'sentence'
)
class
FAQFactory
(
DictFactoryBase
):
answer
=
factory
.
Faker
(
'sentence'
)
question
=
factory
.
Faker
(
'sentence'
)
class
ProgramFactory
(
DictFactoryBase
):
class
ProgramFactory
(
DictFactoryBase
):
authoring_organizations
=
factory
.
LazyFunction
(
partial
(
generate_instances
,
OrganizationFactory
,
count
=
1
))
authoring_organizations
=
factory
.
LazyFunction
(
partial
(
generate_instances
,
OrganizationFactory
,
count
=
1
))
applicable_seat_types
=
[]
banner_image
=
factory
.
LazyFunction
(
generate_sized_stdimage
)
banner_image
=
factory
.
LazyFunction
(
generate_sized_stdimage
)
card_image_url
=
factory
.
Faker
(
'image_url'
)
card_image_url
=
factory
.
Faker
(
'image_url'
)
courses
=
factory
.
LazyFunction
(
partial
(
generate_instances
,
CourseFactory
))
courses
=
factory
.
LazyFunction
(
partial
(
generate_instances
,
CourseFactory
))
expected_learning_items
=
factory
.
LazyFunction
(
partial
(
generate_instances
,
CourseFactory
))
faq
=
factory
.
LazyFunction
(
partial
(
generate_instances
,
FAQFactory
))
hidden
=
False
individual_endorsements
=
factory
.
LazyFunction
(
partial
(
generate_instances
,
EndorserFactory
))
is_program_eligible_for_one_click_purchase
=
True
is_program_eligible_for_one_click_purchase
=
True
job_outlook_items
=
factory
.
LazyFunction
(
partial
(
generate_instances
,
JobOutlookItemFactory
))
marketing_slug
=
factory
.
Faker
(
'slug'
)
marketing_slug
=
factory
.
Faker
(
'slug'
)
marketing_url
=
factory
.
Faker
(
'url'
)
marketing_url
=
factory
.
Faker
(
'url'
)
max_hours_effort_per_week
=
fake
.
random_int
(
21
,
28
)
min_hours_effort_per_week
=
fake
.
random_int
(
7
,
14
)
overview
=
factory
.
Faker
(
'sentence'
)
price_ranges
=
factory
.
LazyFunction
(
generate_price_ranges
)
staff
=
factory
.
LazyFunction
(
partial
(
generate_instances
,
PersonFactory
))
status
=
'active'
status
=
'active'
subtitle
=
factory
.
Faker
(
'sentence'
)
subtitle
=
factory
.
Faker
(
'sentence'
)
title
=
factory
.
Faker
(
'catch_phrase'
)
title
=
factory
.
Faker
(
'catch_phrase'
)
type
=
factory
.
Faker
(
'word'
)
type
=
factory
.
Faker
(
'word'
)
uuid
=
factory
.
Faker
(
'uuid4'
)
uuid
=
factory
.
Faker
(
'uuid4'
)
hidden
=
False
weeks_to_complete
=
fake
.
random_int
(
1
,
45
)
class
ProgramTypeFactory
(
DictFactoryBase
):
class
ProgramTypeFactory
(
DictFactoryBase
):
...
...
openedx/core/djangoapps/programs/tests/test_utils.py
View file @
bf164ae9
"""Tests covering Programs utilities."""
"""Tests covering Programs utilities."""
# pylint: disable=no-member
# pylint: disable=no-member
import
datetime
import
datetime
import
json
import
uuid
import
uuid
import
ddt
import
ddt
import
httpretty
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
django.test
import
TestCase
from
django.test
import
TestCase
from
django.test.utils
import
override_settings
from
django.test.utils
import
override_settings
...
@@ -14,6 +17,7 @@ from pytz import utc
...
@@ -14,6 +17,7 @@ from pytz import utc
from
course_modes.models
import
CourseMode
from
course_modes.models
import
CourseMode
from
lms.djangoapps.certificates.api
import
MODES
from
lms.djangoapps.certificates.api
import
MODES
from
lms.djangoapps.commerce.tests.test_utils
import
update_commerce_config
from
lms.djangoapps.commerce.tests.test_utils
import
update_commerce_config
from
lms.djangoapps.commerce.utils
import
EcommerceService
from
openedx.core.djangoapps.catalog.tests.factories
import
(
from
openedx.core.djangoapps.catalog.tests.factories
import
(
generate_course_run_key
,
generate_course_run_key
,
ProgramFactory
,
ProgramFactory
,
...
@@ -30,15 +34,15 @@ from openedx.core.djangoapps.programs.utils import (
...
@@ -30,15 +34,15 @@ from openedx.core.djangoapps.programs.utils import (
get_certificates
,
get_certificates
,
)
)
from
openedx.core.djangolib.testing.utils
import
skip_unless_lms
from
openedx.core.djangolib.testing.utils
import
skip_unless_lms
from
student.tests.factories
import
UserFactory
,
CourseEnrollmentFactory
from
student.tests.factories
import
AnonymousUserFactory
,
UserFactory
,
CourseEnrollmentFactory
from
util.date_utils
import
strftime_localized
from
util.date_utils
import
strftime_localized
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
as
ModuleStoreCourseFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
as
ModuleStoreCourseFactory
UTILS_MODULE
=
'openedx.core.djangoapps.programs.utils'
CERTIFICATES_API_MODULE
=
'lms.djangoapps.certificates.api'
CERTIFICATES_API_MODULE
=
'lms.djangoapps.certificates.api'
ECOMMERCE_URL_ROOT
=
'https://example-ecommerce.com'
ECOMMERCE_URL_ROOT
=
'https://ecommerce.example.com'
UTILS_MODULE
=
'openedx.core.djangoapps.programs.utils'
@ddt.ddt
@ddt.ddt
...
@@ -809,6 +813,7 @@ class TestGetCertificates(TestCase):
...
@@ -809,6 +813,7 @@ class TestGetCertificates(TestCase):
@skip_unless_lms
@skip_unless_lms
class
TestProgramMarketingDataExtender
(
ModuleStoreTestCase
):
class
TestProgramMarketingDataExtender
(
ModuleStoreTestCase
):
"""Tests of the program data extender utility class."""
"""Tests of the program data extender utility class."""
ECOMMERCE_CALCULATE_DISCOUNT_ENDPOINT
=
'{root}/api/v2/baskets/calculate/'
.
format
(
root
=
ECOMMERCE_URL_ROOT
)
instructors
=
{
instructors
=
{
'instructors'
:
[
'instructors'
:
[
{
{
...
@@ -825,13 +830,16 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
...
@@ -825,13 +830,16 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
def
setUp
(
self
):
def
setUp
(
self
):
super
(
TestProgramMarketingDataExtender
,
self
)
.
setUp
()
super
(
TestProgramMarketingDataExtender
,
self
)
.
setUp
()
# Ensure the E-Commerce service user exists
UserFactory
(
username
=
settings
.
ECOMMERCE_SERVICE_WORKER_USERNAME
,
is_staff
=
True
)
self
.
course_price
=
100
self
.
course_price
=
100
self
.
number_of_courses
=
2
self
.
number_of_courses
=
2
self
.
program
=
ProgramFactory
(
self
.
program
=
ProgramFactory
(
courses
=
[
self
.
_create_course
(
self
.
course_price
)
for
__
in
range
(
self
.
number_of_courses
)]
courses
=
[
self
.
_create_course
(
self
.
course_price
)
for
__
in
range
(
self
.
number_of_courses
)]
)
)
def
_create_course
(
self
,
course_price
,
is_enrolled
=
False
):
def
_create_course
(
self
,
course_price
):
"""
"""
Creates the course in mongo and update it with the instructor data.
Creates the course in mongo and update it with the instructor data.
Also creates catalog course with respect to course run.
Also creates catalog course with respect to course run.
...
@@ -846,12 +854,24 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
...
@@ -846,12 +854,24 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
course
=
self
.
update_course
(
course
,
self
.
user
.
id
)
course
=
self
.
update_course
(
course
,
self
.
user
.
id
)
course_run
=
CourseRunFactory
(
course_run
=
CourseRunFactory
(
is_enrolled
=
is_enrolled
,
key
=
unicode
(
course
.
id
),
key
=
unicode
(
course
.
id
),
seats
=
[
SeatFactory
(
price
=
course_price
)]
seats
=
[
SeatFactory
(
price
=
course_price
)]
)
)
return
CourseFactory
(
course_runs
=
[
course_run
])
return
CourseFactory
(
course_runs
=
[
course_run
])
def
_prepare_program_for_discounted_price_calculation_endpoint
(
self
):
"""
Program's applicable seat types should match some or all seat types of the seats that are a part of the program.
Otherwise, ecommerce API endpoint for calculating the discounted price won't be called.
Returns:
seat: seat for which the discount is applicable
"""
self
.
ecommerce_service
=
EcommerceService
()
seat
=
self
.
program
[
'courses'
][
0
][
'course_runs'
][
0
][
'seats'
][
0
]
self
.
program
[
'applicable_seat_types'
]
=
[
seat
[
'type'
]]
return
seat
def
test_instructors
(
self
):
def
test_instructors
(
self
):
data
=
ProgramMarketingDataExtender
(
self
.
program
,
self
.
user
)
.
extend
()
data
=
ProgramMarketingDataExtender
(
self
.
program
,
self
.
user
)
.
extend
()
...
@@ -896,10 +916,136 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
...
@@ -896,10 +916,136 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
data
=
ProgramMarketingDataExtender
(
program
,
self
.
user
)
.
extend
()
data
=
ProgramMarketingDataExtender
(
program
,
self
.
user
)
.
extend
()
self
.
assertFalse
(
data
[
'is_learner_eligible_for_one_click_purchase'
])
self
.
assertFalse
(
data
[
'is_learner_eligible_for_one_click_purchase'
])
courses
.
append
(
self
.
_create_course
(
self
.
course_price
,
is_enrolled
=
True
))
course
=
self
.
_create_course
(
self
.
course_price
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course
[
'course_runs'
][
0
][
'key'
])
program2
=
ProgramFactory
(
program2
=
ProgramFactory
(
courses
=
courses
,
courses
=
[
course
]
,
is_program_eligible_for_one_click_purchase
=
True
is_program_eligible_for_one_click_purchase
=
True
)
)
data
=
ProgramMarketingDataExtender
(
program2
,
self
.
user
)
.
extend
()
data
=
ProgramMarketingDataExtender
(
program2
,
self
.
user
)
.
extend
()
self
.
assertFalse
(
data
[
'is_learner_eligible_for_one_click_purchase'
])
self
.
assertFalse
(
data
[
'is_learner_eligible_for_one_click_purchase'
])
def
test_multiple_published_course_runs
(
self
):
"""
Learner should not be eligible for one click purchase if:
- program has a course with more than one published course run
"""
course_run_1
=
CourseRunFactory
(
key
=
str
(
ModuleStoreCourseFactory
()
.
id
),
status
=
'published'
)
course_run_2
=
CourseRunFactory
(
key
=
str
(
ModuleStoreCourseFactory
()
.
id
),
status
=
'published'
)
course
=
CourseFactory
(
course_runs
=
[
course_run_1
,
course_run_2
])
program
=
ProgramFactory
(
courses
=
[
CourseFactory
(
course_runs
=
[
CourseRunFactory
(
key
=
str
(
ModuleStoreCourseFactory
()
.
id
),
status
=
'published'
)
]),
course
,
CourseFactory
(
course_runs
=
[
CourseRunFactory
(
key
=
str
(
ModuleStoreCourseFactory
()
.
id
),
status
=
'published'
)
])
],
is_program_eligible_for_one_click_purchase
=
True
)
data
=
ProgramMarketingDataExtender
(
program
,
self
.
user
)
.
extend
()
self
.
assertFalse
(
data
[
'is_learner_eligible_for_one_click_purchase'
])
course_run_2
[
'status'
]
=
'unpublished'
data
=
ProgramMarketingDataExtender
(
program
,
self
.
user
)
.
extend
()
self
.
assertTrue
(
data
[
'is_learner_eligible_for_one_click_purchase'
])
@httpretty.activate
def
test_fetching_program_discounted_price
(
self
):
"""
Authenticated users eligible for one click purchase should see the purchase button
- displaying program's discounted price if it exists.
- leading to ecommerce basket page
"""
self
.
_prepare_program_for_discounted_price_calculation_endpoint
()
mock_discount_data
=
{
'total_incl_tax_excl_discounts'
:
200.0
,
'currency'
:
"USD"
,
'total_incl_tax'
:
50.0
}
httpretty
.
register_uri
(
httpretty
.
GET
,
self
.
ECOMMERCE_CALCULATE_DISCOUNT_ENDPOINT
,
body
=
json
.
dumps
(
mock_discount_data
),
content_type
=
'application/json'
)
data
=
ProgramMarketingDataExtender
(
self
.
program
,
self
.
user
)
.
extend
()
self
.
assertEqual
(
data
[
'skus'
],
[
course
[
'course_runs'
][
0
][
'seats'
][
0
][
'sku'
]
for
course
in
self
.
program
[
'courses'
]]
)
self
.
assertEqual
(
data
[
'discount_data'
],
mock_discount_data
)
@httpretty.activate
def
test_fetching_program_discounted_price_as_anonymous_user
(
self
):
"""
Anonymous users should see the purchase button same way the authenticated users do
when the program is eligible for one click purchase.
"""
self
.
_prepare_program_for_discounted_price_calculation_endpoint
()
mock_discount_data
=
{
'total_incl_tax_excl_discounts'
:
200.0
,
'currency'
:
"USD"
,
'total_incl_tax'
:
50.0
}
httpretty
.
register_uri
(
httpretty
.
GET
,
self
.
ECOMMERCE_CALCULATE_DISCOUNT_ENDPOINT
,
body
=
json
.
dumps
(
mock_discount_data
),
content_type
=
'application/json'
)
data
=
ProgramMarketingDataExtender
(
self
.
program
,
AnonymousUserFactory
())
.
extend
()
self
.
assertEqual
(
data
[
'skus'
],
[
course
[
'course_runs'
][
0
][
'seats'
][
0
][
'sku'
]
for
course
in
self
.
program
[
'courses'
]]
)
self
.
assertEqual
(
data
[
'discount_data'
],
mock_discount_data
)
def
test_fetching_program_discounted_price_no_applicable_seats
(
self
):
"""
User shouldn't be able to do a one click purchase of a program if a program has no applicable seat types.
"""
data
=
ProgramMarketingDataExtender
(
self
.
program
,
self
.
user
)
.
extend
()
self
.
assertEqual
(
len
(
data
[
'skus'
]),
0
)
@httpretty.activate
def
test_fetching_program_discounted_price_api_exception_caught
(
self
):
"""
User should be able to do a one click purchase of a program even if the ecommerce API throws an exception
during the calculation of program discounted price.
"""
self
.
_prepare_program_for_discounted_price_calculation_endpoint
()
httpretty
.
register_uri
(
httpretty
.
GET
,
self
.
ECOMMERCE_CALCULATE_DISCOUNT_ENDPOINT
,
status
=
400
,
content_type
=
'application/json'
)
data
=
ProgramMarketingDataExtender
(
self
.
program
,
self
.
user
)
.
extend
()
self
.
assertEqual
(
data
[
'skus'
],
[
course
[
'course_runs'
][
0
][
'seats'
][
0
][
'sku'
]
for
course
in
self
.
program
[
'courses'
]]
)
openedx/core/djangoapps/programs/utils.py
View file @
bf164ae9
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
"""Helper functions for working with Programs."""
"""Helper functions for working with Programs."""
import
datetime
import
datetime
import
logging
from
collections
import
defaultdict
from
collections
import
defaultdict
from
copy
import
deepcopy
from
copy
import
deepcopy
from
itertools
import
chain
from
itertools
import
chain
...
@@ -8,17 +9,21 @@ from urlparse import urljoin
...
@@ -8,17 +9,21 @@ from urlparse import urljoin
from
dateutil.parser
import
parse
from
dateutil.parser
import
parse
from
django.conf
import
settings
from
django.conf
import
settings
from
django.contrib.auth
import
get_user_model
from
django.core.cache
import
cache
from
django.core.cache
import
cache
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
django.utils.functional
import
cached_property
from
django.utils.functional
import
cached_property
from
edx_rest_api_client.exceptions
import
SlumberBaseException
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
from
pytz
import
utc
from
pytz
import
utc
from
requests.exceptions
import
ConnectionError
,
Timeout
from
course_modes.models
import
CourseMode
from
course_modes.models
import
CourseMode
from
lms.djangoapps.certificates
import
api
as
certificate_api
from
lms.djangoapps.certificates
import
api
as
certificate_api
from
lms.djangoapps.commerce.utils
import
EcommerceService
from
lms.djangoapps.commerce.utils
import
EcommerceService
from
lms.djangoapps.courseware.access
import
has_access
from
lms.djangoapps.courseware.access
import
has_access
from
openedx.core.djangoapps.catalog.utils
import
get_programs
from
openedx.core.djangoapps.catalog.utils
import
get_programs
from
openedx.core.djangoapps.commerce.utils
import
ecommerce_api_client
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
openedx.core.djangoapps.credentials.utils
import
get_credentials
from
openedx.core.djangoapps.credentials.utils
import
get_credentials
from
student.models
import
CourseEnrollment
from
student.models
import
CourseEnrollment
...
@@ -28,6 +33,8 @@ from xmodule.modulestore.django import modulestore
...
@@ -28,6 +33,8 @@ from xmodule.modulestore.django import modulestore
# The datetime module's strftime() methods require a year >= 1900.
# The datetime module's strftime() methods require a year >= 1900.
DEFAULT_ENROLLMENT_START_DATE
=
datetime
.
datetime
(
1900
,
1
,
1
,
tzinfo
=
utc
)
DEFAULT_ENROLLMENT_START_DATE
=
datetime
.
datetime
(
1900
,
1
,
1
,
tzinfo
=
utc
)
log
=
logging
.
getLogger
(
__name__
)
def
get_program_marketing_url
(
programs_config
):
def
get_program_marketing_url
(
programs_config
):
"""Build a URL used to link to programs on the marketing site."""
"""Build a URL used to link to programs on the marketing site."""
...
@@ -507,27 +514,25 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
...
@@ -507,27 +514,25 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
uuid
=
self
.
data
[
'uuid'
]
uuid
=
self
.
data
[
'uuid'
]
)
)
program_instructors
=
cache
.
get
(
cache_key
)
program_instructors
=
cache
.
get
(
cache_key
)
is_learner_eligible_for_one_click_purchase
=
self
.
data
[
'is_program_eligible_for_one_click_purchase'
]
for
course
in
self
.
data
[
'courses'
]:
for
course
in
self
.
data
[
'courses'
]:
self
.
_execute
(
'_collect_course'
,
course
)
self
.
_execute
(
'_collect_course'
,
course
)
if
not
program_instructors
:
if
not
program_instructors
:
for
course_run
in
course
[
'course_runs'
]:
for
course_run
in
course
[
'course_runs'
]:
self
.
_execute
(
'_collect_instructors'
,
course_run
)
self
.
_execute
(
'_collect_instructors'
,
course_run
)
if
is_learner_eligible_for_one_click_purchase
:
is_learner_eligible_for_one_click_purchase
=
not
any
(
course_run
[
'is_enrolled'
]
for
course_run
in
course
[
'course_runs'
]
)
if
not
program_instructors
:
if
not
program_instructors
:
# We cache the program instructors list to avoid repeated modulestore queries
# We cache the program instructors list to avoid repeated modulestore queries
program_instructors
=
self
.
instructors
.
values
()
program_instructors
=
self
.
instructors
.
values
()
cache
.
set
(
cache_key
,
program_instructors
,
3600
)
cache
.
set
(
cache_key
,
program_instructors
,
3600
)
self
.
data
.
update
({
self
.
data
[
'instructors'
]
=
program_instructors
'instructors'
:
program_instructors
,
'is_learner_eligible_for_one_click_purchase'
:
is_learner_eligible_for_one_click_purchase
,
def
extend
(
self
):
})
"""Execute extension handlers, returning the extended data."""
self
.
data
.
update
(
super
(
ProgramMarketingDataExtender
,
self
)
.
extend
())
self
.
_collect_one_click_purchase_eligibility_data
()
return
self
.
data
@classmethod
@classmethod
def
_handlers
(
cls
,
prefix
):
def
_handlers
(
cls
,
prefix
):
...
@@ -582,3 +587,53 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
...
@@ -582,3 +587,53 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
self
.
instructors
.
update
(
self
.
instructors
.
update
(
{
instructor
.
get
(
'name'
):
instructor
for
instructor
in
course_instructors
.
get
(
'instructors'
,
[])}
{
instructor
.
get
(
'name'
):
instructor
for
instructor
in
course_instructors
.
get
(
'instructors'
,
[])}
)
)
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'
]
is_learner_eligible_for_one_click_purchase
=
self
.
data
[
'is_program_eligible_for_one_click_purchase'
]
skus
=
[]
if
is_learner_eligible_for_one_click_purchase
:
for
course
in
self
.
data
[
'courses'
]:
is_learner_eligible_for_one_click_purchase
=
not
any
(
course_run
[
'is_enrolled'
]
for
course_run
in
course
[
'course_runs'
]
)
if
is_learner_eligible_for_one_click_purchase
:
published_course_runs
=
filter
(
lambda
run
:
run
[
'status'
]
==
'published'
,
course
[
'course_runs'
])
if
len
(
published_course_runs
)
==
1
:
for
seat
in
published_course_runs
[
0
][
'seats'
]:
if
seat
[
'type'
]
in
applicable_seat_types
:
skus
.
append
(
seat
[
'sku'
])
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
else
:
skus
=
[]
break
if
skus
:
try
:
User
=
get_user_model
()
service_user
=
User
.
objects
.
get
(
username
=
settings
.
ECOMMERCE_SERVICE_WORKER_USERNAME
)
api
=
ecommerce_api_client
(
service_user
)
# Make an API call to calculate the discounted price
discount_data
=
api
.
baskets
.
calculate
.
get
(
sku
=
skus
)
self
.
data
.
update
({
'discount_data'
:
discount_data
,
'full_program_price'
:
discount_data
[
'total_incl_tax'
]
})
except
(
ConnectionError
,
SlumberBaseException
,
Timeout
):
log
.
exception
(
'Failed to get discount price for following product SKUs:
%
s '
,
', '
.
join
(
skus
))
self
.
data
.
update
({
'is_learner_eligible_for_one_click_purchase'
:
is_learner_eligible_for_one_click_purchase
,
'skus'
:
skus
,
})
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