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
384f22ff
Commit
384f22ff
authored
Jul 28, 2017
by
Calen Pennington
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add per-user metadata to course pages to make experimentation easier
parent
cc6f8be8
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
266 additions
and
156 deletions
+266
-156
common/djangoapps/course_modes/models.py
+54
-0
common/djangoapps/course_modes/tests/test_models.py
+30
-2
lms/djangoapps/ccx/tests/test_field_override_performance.py
+27
-27
lms/djangoapps/courseware/tests/test_course_info.py
+2
-2
lms/djangoapps/courseware/tests/test_views.py
+5
-25
lms/djangoapps/courseware/views/index.py
+10
-6
lms/djangoapps/courseware/views/views.py
+26
-84
lms/djangoapps/discussion/views.py
+8
-5
lms/djangoapps/experiments/utils.py
+48
-0
lms/templates/main.html
+1
-0
lms/templates/user_metadata.html
+51
-0
openedx/features/course_experience/tests/views/test_course_home.py
+1
-1
openedx/features/course_experience/tests/views/test_course_updates.py
+1
-1
openedx/features/course_experience/views/course_sock.py
+1
-2
scripts/xss_linter.py
+1
-1
No files found.
common/djangoapps/course_modes/models.py
View file @
384f22ff
...
@@ -12,6 +12,7 @@ from django.db import models
...
@@ -12,6 +12,7 @@ from django.db import models
from
django.db.models
import
Q
from
django.db.models
import
Q
from
django.dispatch
import
receiver
from
django.dispatch
import
receiver
from
django.utils.translation
import
ugettext_lazy
as
_
from
django.utils.translation
import
ugettext_lazy
as
_
from
django.utils.encoding
import
force_text
from
openedx.core.djangoapps.xmodule_django.models
import
CourseKeyField
from
openedx.core.djangoapps.xmodule_django.models
import
CourseKeyField
from
request_cache.middleware
import
RequestCache
,
ns_request_cached
from
request_cache.middleware
import
RequestCache
,
ns_request_cached
...
@@ -693,6 +694,59 @@ def invalidate_course_mode_cache(sender, **kwargs): # pylint: disable=unused-a
...
@@ -693,6 +694,59 @@ def invalidate_course_mode_cache(sender, **kwargs): # pylint: disable=unused-a
RequestCache
.
clear_request_cache
(
name
=
CourseMode
.
CACHE_NAMESPACE
)
RequestCache
.
clear_request_cache
(
name
=
CourseMode
.
CACHE_NAMESPACE
)
def
get_cosmetic_verified_display_price
(
course
):
"""
Returns the minimum verified cert course price as a string preceded by correct currency, or 'Free'.
"""
return
get_course_prices
(
course
,
verified_only
=
True
)[
1
]
def
get_cosmetic_display_price
(
course
):
"""
Returns the course price as a string preceded by correct currency, or 'Free'.
"""
return
get_course_prices
(
course
)[
1
]
def
get_course_prices
(
course
,
verified_only
=
False
):
"""
Return registration_price and cosmetic_display_prices.
registration_price is the minimum price for the course across all course modes.
cosmetic_display_prices is the course price as a string preceded by correct currency, or 'Free'.
"""
# Find the
if
verified_only
:
registration_price
=
CourseMode
.
min_course_price_for_verified_for_currency
(
course
.
id
,
settings
.
PAID_COURSE_REGISTRATION_CURRENCY
[
0
]
)
else
:
registration_price
=
CourseMode
.
min_course_price_for_currency
(
course
.
id
,
settings
.
PAID_COURSE_REGISTRATION_CURRENCY
[
0
]
)
currency_symbol
=
settings
.
PAID_COURSE_REGISTRATION_CURRENCY
[
1
]
if
registration_price
>
0
:
price
=
registration_price
# Handle course overview objects which have no cosmetic_display_price
elif
hasattr
(
course
,
'cosmetic_display_price'
):
price
=
course
.
cosmetic_display_price
else
:
price
=
None
if
price
:
# Translators: This will look like '$50', where {currency_symbol} is a symbol such as '$' and {price} is a
# numerical amount in that currency. Adjust this display as needed for your language.
cosmetic_display_price
=
_
(
"{currency_symbol}{price}"
)
.
format
(
currency_symbol
=
currency_symbol
,
price
=
price
)
else
:
# Translators: This refers to the cost of the course. In this case, the course costs nothing so it is free.
cosmetic_display_price
=
_
(
'Free'
)
return
registration_price
,
force_text
(
cosmetic_display_price
)
class
CourseModesArchive
(
models
.
Model
):
class
CourseModesArchive
(
models
.
Model
):
"""
"""
Store the past values of course_mode that a course had in the past. We decided on having
Store the past values of course_mode that a course had in the past. We decided on having
...
...
common/djangoapps/course_modes/tests/test_models.py
View file @
384f22ff
...
@@ -11,13 +11,18 @@ from datetime import datetime, timedelta
...
@@ -11,13 +11,18 @@ from datetime import datetime, timedelta
import
ddt
import
ddt
import
pytz
import
pytz
from
django.core.exceptions
import
ValidationError
from
django.core.exceptions
import
ValidationError
from
django.test
import
TestCase
from
django.test
import
TestCase
,
override_settings
from
mock
import
patch
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
opaque_keys.edx.locator
import
CourseLocator
from
opaque_keys.edx.locator
import
CourseLocator
from
course_modes.helpers
import
enrollment_mode_display
from
course_modes.helpers
import
enrollment_mode_display
from
course_modes.models
import
CourseMode
,
Mode
,
invalidate_course_mode_cache
from
course_modes.models
import
CourseMode
,
Mode
,
invalidate_course_mode_cache
,
get_cosmetic_display_price
from
course_modes.tests.factories
import
CourseModeFactory
from
course_modes.tests.factories
import
CourseModeFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.django_utils
import
(
ModuleStoreTestCase
,
)
@ddt.ddt
@ddt.ddt
...
@@ -474,3 +479,26 @@ class CourseModeModelTest(TestCase):
...
@@ -474,3 +479,26 @@ class CourseModeModelTest(TestCase):
self
.
assertTrue
(
is_error_expected
,
"Did not expect a ValidationError to be thrown."
)
self
.
assertTrue
(
is_error_expected
,
"Did not expect a ValidationError to be thrown."
)
else
:
else
:
self
.
assertFalse
(
is_error_expected
,
"Expected a ValidationError to be thrown."
)
self
.
assertFalse
(
is_error_expected
,
"Expected a ValidationError to be thrown."
)
class
TestDisplayPrices
(
ModuleStoreTestCase
):
@override_settings
(
PAID_COURSE_REGISTRATION_CURRENCY
=
[
"USD"
,
"$"
])
def
test_get_cosmetic_display_price
(
self
):
"""
Check that get_cosmetic_display_price() returns the correct price given its inputs.
"""
course
=
CourseFactory
.
create
()
registration_price
=
99
course
.
cosmetic_display_price
=
10
with
patch
(
'course_modes.models.CourseMode.min_course_price_for_currency'
,
return_value
=
registration_price
):
# Since registration_price is set, it overrides the cosmetic_display_price and should be returned
self
.
assertEqual
(
get_cosmetic_display_price
(
course
),
"$99"
)
registration_price
=
0
with
patch
(
'course_modes.models.CourseMode.min_course_price_for_currency'
,
return_value
=
registration_price
):
# Since registration_price is not set, cosmetic_display_price should be returned
self
.
assertEqual
(
get_cosmetic_display_price
(
course
),
"$10"
)
course
.
cosmetic_display_price
=
0
# Since both prices are not set, there is no price, thus "Free"
self
.
assertEqual
(
get_cosmetic_display_price
(
course
),
"Free"
)
lms/djangoapps/ccx/tests/test_field_override_performance.py
View file @
384f22ff
...
@@ -237,18 +237,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
...
@@ -237,18 +237,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
# # of sql queries to default,
# # of sql queries to default,
# # of mongo queries,
# # of mongo queries,
# )
# )
(
'no_overrides'
,
1
,
True
,
False
):
(
2
3
,
1
),
(
'no_overrides'
,
1
,
True
,
False
):
(
2
4
,
1
),
(
'no_overrides'
,
2
,
True
,
False
):
(
2
3
,
1
),
(
'no_overrides'
,
2
,
True
,
False
):
(
2
4
,
1
),
(
'no_overrides'
,
3
,
True
,
False
):
(
2
3
,
1
),
(
'no_overrides'
,
3
,
True
,
False
):
(
2
4
,
1
),
(
'ccx'
,
1
,
True
,
False
):
(
2
3
,
1
),
(
'ccx'
,
1
,
True
,
False
):
(
2
4
,
1
),
(
'ccx'
,
2
,
True
,
False
):
(
2
3
,
1
),
(
'ccx'
,
2
,
True
,
False
):
(
2
4
,
1
),
(
'ccx'
,
3
,
True
,
False
):
(
2
3
,
1
),
(
'ccx'
,
3
,
True
,
False
):
(
2
4
,
1
),
(
'no_overrides'
,
1
,
False
,
False
):
(
2
3
,
1
),
(
'no_overrides'
,
1
,
False
,
False
):
(
2
4
,
1
),
(
'no_overrides'
,
2
,
False
,
False
):
(
2
3
,
1
),
(
'no_overrides'
,
2
,
False
,
False
):
(
2
4
,
1
),
(
'no_overrides'
,
3
,
False
,
False
):
(
2
3
,
1
),
(
'no_overrides'
,
3
,
False
,
False
):
(
2
4
,
1
),
(
'ccx'
,
1
,
False
,
False
):
(
2
3
,
1
),
(
'ccx'
,
1
,
False
,
False
):
(
2
4
,
1
),
(
'ccx'
,
2
,
False
,
False
):
(
2
3
,
1
),
(
'ccx'
,
2
,
False
,
False
):
(
2
4
,
1
),
(
'ccx'
,
3
,
False
,
False
):
(
2
3
,
1
),
(
'ccx'
,
3
,
False
,
False
):
(
2
4
,
1
),
}
}
...
@@ -260,19 +260,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
...
@@ -260,19 +260,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
__test__
=
True
__test__
=
True
TEST_DATA
=
{
TEST_DATA
=
{
(
'no_overrides'
,
1
,
True
,
False
):
(
2
3
,
3
),
(
'no_overrides'
,
1
,
True
,
False
):
(
2
4
,
3
),
(
'no_overrides'
,
2
,
True
,
False
):
(
2
3
,
3
),
(
'no_overrides'
,
2
,
True
,
False
):
(
2
4
,
3
),
(
'no_overrides'
,
3
,
True
,
False
):
(
2
3
,
3
),
(
'no_overrides'
,
3
,
True
,
False
):
(
2
4
,
3
),
(
'ccx'
,
1
,
True
,
False
):
(
2
3
,
3
),
(
'ccx'
,
1
,
True
,
False
):
(
2
4
,
3
),
(
'ccx'
,
2
,
True
,
False
):
(
2
3
,
3
),
(
'ccx'
,
2
,
True
,
False
):
(
2
4
,
3
),
(
'ccx'
,
3
,
True
,
False
):
(
2
3
,
3
),
(
'ccx'
,
3
,
True
,
False
):
(
2
4
,
3
),
(
'ccx'
,
1
,
True
,
True
):
(
2
4
,
3
),
(
'ccx'
,
1
,
True
,
True
):
(
2
5
,
3
),
(
'ccx'
,
2
,
True
,
True
):
(
2
4
,
3
),
(
'ccx'
,
2
,
True
,
True
):
(
2
5
,
3
),
(
'ccx'
,
3
,
True
,
True
):
(
2
4
,
3
),
(
'ccx'
,
3
,
True
,
True
):
(
2
5
,
3
),
(
'no_overrides'
,
1
,
False
,
False
):
(
2
3
,
3
),
(
'no_overrides'
,
1
,
False
,
False
):
(
2
4
,
3
),
(
'no_overrides'
,
2
,
False
,
False
):
(
2
3
,
3
),
(
'no_overrides'
,
2
,
False
,
False
):
(
2
4
,
3
),
(
'no_overrides'
,
3
,
False
,
False
):
(
2
3
,
3
),
(
'no_overrides'
,
3
,
False
,
False
):
(
2
4
,
3
),
(
'ccx'
,
1
,
False
,
False
):
(
2
3
,
3
),
(
'ccx'
,
1
,
False
,
False
):
(
2
4
,
3
),
(
'ccx'
,
2
,
False
,
False
):
(
2
3
,
3
),
(
'ccx'
,
2
,
False
,
False
):
(
2
4
,
3
),
(
'ccx'
,
3
,
False
,
False
):
(
2
3
,
3
),
(
'ccx'
,
3
,
False
,
False
):
(
2
4
,
3
),
}
}
lms/djangoapps/courseware/tests/test_course_info.py
View file @
384f22ff
...
@@ -388,7 +388,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
...
@@ -388,7 +388,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
self
.
assertEqual
(
resp
.
status_code
,
200
)
self
.
assertEqual
(
resp
.
status_code
,
200
)
def
test_num_queries_instructor_paced
(
self
):
def
test_num_queries_instructor_paced
(
self
):
self
.
fetch_course_info_with_queries
(
self
.
instructor_paced_course
,
2
5
,
3
)
self
.
fetch_course_info_with_queries
(
self
.
instructor_paced_course
,
2
6
,
3
)
def
test_num_queries_self_paced
(
self
):
def
test_num_queries_self_paced
(
self
):
self
.
fetch_course_info_with_queries
(
self
.
self_paced_course
,
2
5
,
3
)
self
.
fetch_course_info_with_queries
(
self
.
self_paced_course
,
2
6
,
3
)
lms/djangoapps/courseware/tests/test_views.py
View file @
384f22ff
...
@@ -211,8 +211,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
...
@@ -211,8 +211,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
NUM_PROBLEMS
=
20
NUM_PROBLEMS
=
20
@ddt.data
(
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
10
,
14
4
),
(
ModuleStoreEnum
.
Type
.
mongo
,
10
,
14
5
),
(
ModuleStoreEnum
.
Type
.
split
,
4
,
14
4
),
(
ModuleStoreEnum
.
Type
.
split
,
4
,
14
5
),
)
)
@ddt.unpack
@ddt.unpack
def
test_index_query_counts
(
self
,
store_type
,
expected_mongo_query_count
,
expected_mysql_query_count
):
def
test_index_query_counts
(
self
,
store_type
,
expected_mongo_query_count
,
expected_mysql_query_count
):
...
@@ -577,26 +577,6 @@ class ViewsTestCase(ModuleStoreTestCase):
...
@@ -577,26 +577,6 @@ class ViewsTestCase(ModuleStoreTestCase):
response
=
self
.
client
.
get
(
request_url
)
response
=
self
.
client
.
get
(
request_url
)
self
.
assertEqual
(
response
.
status_code
,
404
)
self
.
assertEqual
(
response
.
status_code
,
404
)
@override_settings
(
PAID_COURSE_REGISTRATION_CURRENCY
=
[
"USD"
,
"$"
])
def
test_get_cosmetic_display_price
(
self
):
"""
Check that get_cosmetic_display_price() returns the correct price given its inputs.
"""
registration_price
=
99
self
.
course
.
cosmetic_display_price
=
10
with
patch
(
'course_modes.models.CourseMode.min_course_price_for_currency'
,
return_value
=
registration_price
):
# Since registration_price is set, it overrides the cosmetic_display_price and should be returned
self
.
assertEqual
(
views
.
get_cosmetic_display_price
(
self
.
course
),
"$99"
)
registration_price
=
0
with
patch
(
'course_modes.models.CourseMode.min_course_price_for_currency'
,
return_value
=
registration_price
):
# Since registration_price is not set, cosmetic_display_price should be returned
self
.
assertEqual
(
views
.
get_cosmetic_display_price
(
self
.
course
),
"$10"
)
self
.
course
.
cosmetic_display_price
=
0
# Since both prices are not set, there is no price, thus "Free"
self
.
assertEqual
(
views
.
get_cosmetic_display_price
(
self
.
course
),
"Free"
)
def
test_jump_to_invalid
(
self
):
def
test_jump_to_invalid
(
self
):
# TODO add a test for invalid location
# TODO add a test for invalid location
# TODO add a test for no data *
# TODO add a test for no data *
...
@@ -1464,12 +1444,12 @@ class ProgressPageTests(ProgressPageBaseTests):
...
@@ -1464,12 +1444,12 @@ class ProgressPageTests(ProgressPageBaseTests):
"""Test that query counts remain the same for self-paced and instructor-paced courses."""
"""Test that query counts remain the same for self-paced and instructor-paced courses."""
SelfPacedConfiguration
(
enabled
=
self_paced_enabled
)
.
save
()
SelfPacedConfiguration
(
enabled
=
self_paced_enabled
)
.
save
()
self
.
setup_course
(
self_paced
=
self_paced
)
self
.
setup_course
(
self_paced
=
self_paced
)
with
self
.
assertNumQueries
(
4
0
,
table_blacklist
=
QUERY_COUNT_TABLE_BLACKLIST
),
check_mongo_calls
(
1
):
with
self
.
assertNumQueries
(
4
1
,
table_blacklist
=
QUERY_COUNT_TABLE_BLACKLIST
),
check_mongo_calls
(
1
):
self
.
_get_progress_page
()
self
.
_get_progress_page
()
@ddt.data
(
@ddt.data
(
(
False
,
4
0
,
26
),
(
False
,
4
1
,
27
),
(
True
,
3
3
,
22
)
(
True
,
3
4
,
23
)
)
)
@ddt.unpack
@ddt.unpack
def
test_progress_queries
(
self
,
enable_waffle
,
initial
,
subsequent
):
def
test_progress_queries
(
self
,
enable_waffle
,
initial
,
subsequent
):
...
...
lms/djangoapps/courseware/views/index.py
View file @
384f22ff
...
@@ -22,6 +22,7 @@ from web_fragments.fragment import Fragment
...
@@ -22,6 +22,7 @@ from web_fragments.fragment import Fragment
from
edxmako.shortcuts
import
render_to_response
,
render_to_string
from
edxmako.shortcuts
import
render_to_response
,
render_to_string
from
lms.djangoapps.courseware.exceptions
import
CourseAccessRedirect
from
lms.djangoapps.courseware.exceptions
import
CourseAccessRedirect
from
lms.djangoapps.experiments.utils
import
get_experiment_user_metadata_context
from
lms.djangoapps.gating.api
import
get_entrance_exam_score_ratio
,
get_entrance_exam_usage_key
from
lms.djangoapps.gating.api
import
get_entrance_exam_score_ratio
,
get_entrance_exam_usage_key
from
lms.djangoapps.grades.new.course_grade_factory
import
CourseGradeFactory
from
lms.djangoapps.grades.new.course_grade_factory
import
CourseGradeFactory
from
openedx.core.djangoapps.crawlers.models
import
CrawlersConfig
from
openedx.core.djangoapps.crawlers.models
import
CrawlersConfig
...
@@ -34,6 +35,7 @@ from openedx.features.course_experience.views.course_sock import CourseSockFragm
...
@@ -34,6 +35,7 @@ from openedx.features.course_experience.views.course_sock import CourseSockFragm
from
openedx.features.enterprise_support.api
import
data_sharing_consent_required
from
openedx.features.enterprise_support.api
import
data_sharing_consent_required
from
shoppingcart.models
import
CourseRegistrationCode
from
shoppingcart.models
import
CourseRegistrationCode
from
student.views
import
is_course_blocked
from
student.views
import
is_course_blocked
from
student.models
import
CourseEnrollment
from
util.views
import
ensure_valid_course_key
from
util.views
import
ensure_valid_course_key
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
xmodule.x_module
import
STUDENT_VIEW
from
xmodule.x_module
import
STUDENT_VIEW
...
@@ -52,8 +54,6 @@ from ..model_data import FieldDataCache
...
@@ -52,8 +54,6 @@ from ..model_data import FieldDataCache
from
..module_render
import
get_module_for_descriptor
,
toc_for_course
from
..module_render
import
get_module_for_descriptor
,
toc_for_course
from
.views
import
(
from
.views
import
(
CourseTabView
,
CourseTabView
,
check_and_get_upgrade_link
,
get_cosmetic_verified_display_price
)
)
log
=
logging
.
getLogger
(
"edx.courseware.views.index"
)
log
=
logging
.
getLogger
(
"edx.courseware.views.index"
)
...
@@ -325,6 +325,7 @@ class CoursewareIndex(View):
...
@@ -325,6 +325,7 @@ class CoursewareIndex(View):
"""
"""
course_url_name
=
default_course_url_name
(
self
.
course
.
id
)
course_url_name
=
default_course_url_name
(
self
.
course
.
id
)
course_url
=
reverse
(
course_url_name
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)})
course_url
=
reverse
(
course_url_name
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)})
courseware_context
=
{
courseware_context
=
{
'csrf'
:
csrf
(
self
.
request
)[
'csrf_token'
],
'csrf'
:
csrf
(
self
.
request
)[
'csrf_token'
],
'course'
:
self
.
course
,
'course'
:
self
.
course
,
...
@@ -344,11 +345,14 @@ class CoursewareIndex(View):
...
@@ -344,11 +345,14 @@ class CoursewareIndex(View):
'section_title'
:
None
,
'section_title'
:
None
,
'sequence_title'
:
None
,
'sequence_title'
:
None
,
'disable_accordion'
:
COURSE_OUTLINE_PAGE_FLAG
.
is_enabled
(
self
.
course
.
id
),
'disable_accordion'
:
COURSE_OUTLINE_PAGE_FLAG
.
is_enabled
(
self
.
course
.
id
),
# TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts
'upgrade_link'
:
check_and_get_upgrade_link
(
request
,
self
.
effective_user
,
self
.
course
.
id
),
'upgrade_price'
:
get_cosmetic_verified_display_price
(
self
.
course
),
# ENDTODO
}
}
courseware_context
.
update
(
get_experiment_user_metadata_context
(
request
,
self
.
course
,
self
.
effective_user
,
)
)
table_of_contents
=
toc_for_course
(
table_of_contents
=
toc_for_course
(
self
.
effective_user
,
self
.
effective_user
,
self
.
request
,
self
.
request
,
...
...
lms/djangoapps/courseware/views/views.py
View file @
384f22ff
...
@@ -14,7 +14,7 @@ import waffle
...
@@ -14,7 +14,7 @@ import waffle
from
certificates
import
api
as
certs_api
from
certificates
import
api
as
certs_api
from
certificates.models
import
CertificateStatuses
from
certificates.models
import
CertificateStatuses
from
commerce.utils
import
EcommerceService
from
commerce.utils
import
EcommerceService
from
course_modes.models
import
CourseMode
from
course_modes.models
import
(
CourseMode
,
get_course_prices
)
from
courseware.access
import
has_access
,
has_ccx_coach_role
from
courseware.access
import
has_access
,
has_ccx_coach_role
from
courseware.access_utils
import
check_course_open_for_learner
from
courseware.access_utils
import
check_course_open_for_learner
from
courseware.courses
import
(
from
courseware.courses
import
(
...
@@ -61,6 +61,7 @@ from ipware.ip import get_ip
...
@@ -61,6 +61,7 @@ from ipware.ip import get_ip
from
lms.djangoapps.ccx.custom_exception
import
CCXLocatorValidationException
from
lms.djangoapps.ccx.custom_exception
import
CCXLocatorValidationException
from
lms.djangoapps.ccx.utils
import
prep_course_for_grading
from
lms.djangoapps.ccx.utils
import
prep_course_for_grading
from
lms.djangoapps.courseware.exceptions
import
CourseAccessRedirect
,
Redirect
from
lms.djangoapps.courseware.exceptions
import
CourseAccessRedirect
,
Redirect
from
lms.djangoapps.experiments.utils
import
get_experiment_user_metadata_context
from
lms.djangoapps.grades.new.course_grade_factory
import
CourseGradeFactory
from
lms.djangoapps.grades.new.course_grade_factory
import
CourseGradeFactory
from
lms.djangoapps.instructor.enrollment
import
uses_shib
from
lms.djangoapps.instructor.enrollment
import
uses_shib
from
lms.djangoapps.instructor.views.api
import
require_global_staff
from
lms.djangoapps.instructor.views.api
import
require_global_staff
...
@@ -322,13 +323,14 @@ def course_info(request, course_id):
...
@@ -322,13 +323,14 @@ def course_info(request, course_id):
'dates_fragment'
:
dates_fragment
,
'dates_fragment'
:
dates_fragment
,
'url_to_enroll'
:
CourseTabView
.
url_to_enroll
(
course_key
),
'url_to_enroll'
:
CourseTabView
.
url_to_enroll
(
course_key
),
'course_tools'
:
course_tools
,
'course_tools'
:
course_tools
,
# TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts
'upgrade_link'
:
check_and_get_upgrade_link
(
request
,
user
,
course
.
id
),
'upgrade_price'
:
get_cosmetic_verified_display_price
(
course
),
'course_tools'
:
course_tools
,
# ENDTODO
}
}
context
.
update
(
get_experiment_user_metadata_context
(
request
,
course
,
user
,
)
)
# Get the URL of the user's last position in order to display the 'where you were last' message
# Get the URL of the user's last position in order to display the 'where you were last' message
context
[
'resume_course_url'
]
=
None
context
[
'resume_course_url'
]
=
None
...
@@ -348,20 +350,6 @@ def course_info(request, course_id):
...
@@ -348,20 +350,6 @@ def course_info(request, course_id):
UPGRADE_COOKIE_NAME
=
'show_upgrade_notification'
UPGRADE_COOKIE_NAME
=
'show_upgrade_notification'
# TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts
def
check_and_get_upgrade_link
(
request
,
user
,
course_id
):
upgrade_link
=
None
if
request
.
user
.
is_authenticated
():
upgrade_data
=
VerifiedUpgradeDeadlineDate
(
None
,
user
,
course_id
=
course_id
)
if
upgrade_data
.
is_enabled
:
upgrade_link
=
upgrade_data
.
link
request
.
need_to_set_upgrade_cookie
=
True
return
upgrade_link
# ENDTODO
class
StaticCourseTabView
(
EdxFragmentView
):
class
StaticCourseTabView
(
EdxFragmentView
):
"""
"""
View that displays a static course tab with a given name.
View that displays a static course tab with a given name.
...
@@ -521,7 +509,8 @@ class CourseTabView(EdxFragmentView):
...
@@ -521,7 +509,8 @@ class CourseTabView(EdxFragmentView):
# Disable student view button if user is staff and
# Disable student view button if user is staff and
# course is not yet visible to students.
# course is not yet visible to students.
supports_preview_menu
=
False
supports_preview_menu
=
False
return
{
context
=
{
'course'
:
course
,
'course'
:
course
,
'tab'
:
tab
,
'tab'
:
tab
,
'active_page'
:
tab
.
get
(
'type'
,
None
),
'active_page'
:
tab
.
get
(
'type'
,
None
),
...
@@ -530,11 +519,15 @@ class CourseTabView(EdxFragmentView):
...
@@ -530,11 +519,15 @@ class CourseTabView(EdxFragmentView):
'supports_preview_menu'
:
supports_preview_menu
,
'supports_preview_menu'
:
supports_preview_menu
,
'uses_pattern_library'
:
True
,
'uses_pattern_library'
:
True
,
'disable_courseware_js'
:
True
,
'disable_courseware_js'
:
True
,
# TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts
'upgrade_link'
:
check_and_get_upgrade_link
(
request
,
request
.
user
,
course
.
id
),
'upgrade_price'
:
get_cosmetic_verified_display_price
(
course
),
# ENDTODO
}
}
context
.
update
(
get_experiment_user_metadata_context
(
request
,
course
,
request
.
user
,
)
)
return
context
def
render_to_fragment
(
self
,
request
,
course
=
None
,
page_context
=
None
,
**
kwargs
):
def
render_to_fragment
(
self
,
request
,
course
=
None
,
page_context
=
None
,
**
kwargs
):
"""
"""
...
@@ -585,59 +578,6 @@ def registered_for_course(course, user):
...
@@ -585,59 +578,6 @@ def registered_for_course(course, user):
return
False
return
False
def
get_cosmetic_verified_display_price
(
course
):
"""
Returns the minimum verified cert course price as a string preceded by correct currency, or 'Free'.
"""
return
get_course_prices
(
course
,
verified_only
=
True
)[
1
]
def
get_cosmetic_display_price
(
course
):
"""
Returns the course price as a string preceded by correct currency, or 'Free'.
"""
return
get_course_prices
(
course
)[
1
]
def
get_course_prices
(
course
,
verified_only
=
False
):
"""
Return registration_price and cosmetic_display_prices.
registration_price is the minimum price for the course across all course modes.
cosmetic_display_prices is the course price as a string preceded by correct currency, or 'Free'.
"""
# Find the
if
verified_only
:
registration_price
=
CourseMode
.
min_course_price_for_verified_for_currency
(
course
.
id
,
settings
.
PAID_COURSE_REGISTRATION_CURRENCY
[
0
]
)
else
:
registration_price
=
CourseMode
.
min_course_price_for_currency
(
course
.
id
,
settings
.
PAID_COURSE_REGISTRATION_CURRENCY
[
0
]
)
currency_symbol
=
settings
.
PAID_COURSE_REGISTRATION_CURRENCY
[
1
]
if
registration_price
>
0
:
price
=
registration_price
# Handle course overview objects which have no cosmetic_display_price
elif
hasattr
(
course
,
'cosmetic_display_price'
):
price
=
course
.
cosmetic_display_price
else
:
price
=
None
if
price
:
# Translators: This will look like '$50', where {currency_symbol} is a symbol such as '$' and {price} is a
# numerical amount in that currency. Adjust this display as needed for your language.
cosmetic_display_price
=
_
(
"{currency_symbol}{price}"
)
.
format
(
currency_symbol
=
currency_symbol
,
price
=
price
)
else
:
# Translators: This refers to the cost of the course. In this case, the course costs nothing so it is free.
cosmetic_display_price
=
_
(
'Free'
)
return
registration_price
,
cosmetic_display_price
class
EnrollStaffView
(
View
):
class
EnrollStaffView
(
View
):
"""
"""
Displays view for registering in the course to a global staff user.
Displays view for registering in the course to a global staff user.
...
@@ -927,7 +867,6 @@ def _progress(request, course_key, student_id):
...
@@ -927,7 +867,6 @@ def _progress(request, course_key, student_id):
grade_summary
=
course_grade
.
summary
grade_summary
=
course_grade
.
summary
studio_url
=
get_studio_url
(
course
,
'settings/grading'
)
studio_url
=
get_studio_url
(
course
,
'settings/grading'
)
# checking certificate generation configuration
# checking certificate generation configuration
enrollment_mode
,
is_active
=
CourseEnrollment
.
enrollment_mode_for_user
(
student
,
course_key
)
enrollment_mode
,
is_active
=
CourseEnrollment
.
enrollment_mode_for_user
(
student
,
course_key
)
...
@@ -943,11 +882,14 @@ def _progress(request, course_key, student_id):
...
@@ -943,11 +882,14 @@ def _progress(request, course_key, student_id):
'passed'
:
is_course_passed
(
course
,
grade_summary
),
'passed'
:
is_course_passed
(
course
,
grade_summary
),
'credit_course_requirements'
:
_credit_course_requirements
(
course_key
,
student
),
'credit_course_requirements'
:
_credit_course_requirements
(
course_key
,
student
),
'certificate_data'
:
_get_cert_data
(
student
,
course
,
course_key
,
is_active
,
enrollment_mode
),
'certificate_data'
:
_get_cert_data
(
student
,
course
,
course_key
,
is_active
,
enrollment_mode
),
# TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts
'upgrade_link'
:
check_and_get_upgrade_link
(
request
,
student
,
course
.
id
),
'upgrade_price'
:
get_cosmetic_verified_display_price
(
course
),
# ENDTODO
}
}
context
.
update
(
get_experiment_user_metadata_context
(
request
,
course
,
student
,
)
)
with
outer_atomic
():
with
outer_atomic
():
response
=
render_to_response
(
'courseware/progress.html'
,
context
)
response
=
render_to_response
(
'courseware/progress.html'
,
context
)
...
...
lms/djangoapps/discussion/views.py
View file @
384f22ff
...
@@ -24,6 +24,7 @@ from rest_framework import status
...
@@ -24,6 +24,7 @@ from rest_framework import status
from
web_fragments.fragment
import
Fragment
from
web_fragments.fragment
import
Fragment
import
django_comment_client.utils
as
utils
import
django_comment_client.utils
as
utils
from
lms.djangoapps.experiments.utils
import
get_experiment_user_metadata_context
import
lms.lib.comment_client
as
cc
import
lms.lib.comment_client
as
cc
from
courseware.access
import
has_access
from
courseware.access
import
has_access
from
courseware.courses
import
get_course_with_access
from
courseware.courses
import
get_course_with_access
...
@@ -44,7 +45,6 @@ from django_comment_client.utils import (
...
@@ -44,7 +45,6 @@ from django_comment_client.utils import (
strip_none
strip_none
)
)
from
django_comment_common.utils
import
ThreadContext
,
get_course_discussion_settings
,
set_course_discussion_settings
from
django_comment_common.utils
import
ThreadContext
,
get_course_discussion_settings
,
set_course_discussion_settings
from
lms.djangoapps.courseware.views.views
import
check_and_get_upgrade_link
,
get_cosmetic_verified_display_price
from
openedx.core.djangoapps.plugin_api.views
import
EdxFragmentView
from
openedx.core.djangoapps.plugin_api.views
import
EdxFragmentView
from
student.models
import
CourseEnrollment
from
student.models
import
CourseEnrollment
from
util.json_request
import
JsonResponse
,
expect_json
from
util.json_request
import
JsonResponse
,
expect_json
...
@@ -481,13 +481,16 @@ def _create_discussion_board_context(request, base_context, thread=None):
...
@@ -481,13 +481,16 @@ def _create_discussion_board_context(request, base_context, thread=None):
'category_map'
:
course_settings
[
"category_map"
],
'category_map'
:
course_settings
[
"category_map"
],
'course_settings'
:
course_settings
,
'course_settings'
:
course_settings
,
'is_commentable_divided'
:
is_commentable_divided
(
course_key
,
discussion_id
,
course_discussion_settings
),
'is_commentable_divided'
:
is_commentable_divided
(
course_key
,
discussion_id
,
course_discussion_settings
),
# TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts
'upgrade_link'
:
check_and_get_upgrade_link
(
request
,
user
,
course
.
id
),
'upgrade_price'
:
get_cosmetic_verified_display_price
(
course
),
# ENDTODO
# If the default topic id is None the front-end code will look for a topic that contains "General"
# If the default topic id is None the front-end code will look for a topic that contains "General"
'discussion_default_topic_id'
:
_get_discussion_default_topic_id
(
course
),
'discussion_default_topic_id'
:
_get_discussion_default_topic_id
(
course
),
})
})
context
.
update
(
get_experiment_user_metadata_context
(
request
,
course
,
user
,
)
)
return
context
return
context
...
...
lms/djangoapps/experiments/utils.py
0 → 100644
View file @
384f22ff
from
student.models
import
CourseEnrollment
from
course_modes.models
import
(
get_cosmetic_verified_display_price
)
from
courseware.date_summary
import
(
VerifiedUpgradeDeadlineDate
)
def
check_and_get_upgrade_link
(
request
,
user
,
course_id
):
"""
For an authenticated user, return a link to allow them to upgrade
in the specified course.
"""
if
request
.
user
.
is_authenticated
():
upgrade_data
=
VerifiedUpgradeDeadlineDate
(
None
,
user
,
course_id
=
course_id
)
if
upgrade_data
.
is_enabled
:
request
.
need_to_set_upgrade_cookie
=
True
return
upgrade_data
return
None
def
get_experiment_user_metadata_context
(
request
,
course
,
user
):
"""
Return a context dictionary with the keys used by the user_metadata.html.
"""
enrollment_mode
=
None
enrollment_time
=
None
try
:
enrollment
=
CourseEnrollment
.
objects
.
get
(
user_id
=
user
.
id
,
course_id
=
course
.
id
)
if
enrollment
.
is_active
:
enrollment_mode
=
enrollment
.
mode
enrollment_time
=
enrollment
.
created
except
CourseEnrollment
.
DoesNotExist
:
pass
# Not enrolled, used the default None values
upgrade_data
=
check_and_get_upgrade_link
(
request
,
user
,
course
.
id
)
return
{
'upgrade_link'
:
upgrade_data
and
upgrade_data
.
link
,
'upgrade_price'
:
get_cosmetic_verified_display_price
(
course
),
'enrollment_mode'
:
enrollment_mode
,
'enrollment_time'
:
enrollment_time
,
'pacing_type'
:
'self_paced'
if
course
.
self_paced
else
'instructor_paced'
,
'upgrade_deadline'
:
upgrade_data
and
upgrade_data
.
date
,
'course_key'
:
course
.
id
,
}
lms/templates/main.html
View file @
384f22ff
...
@@ -104,6 +104,7 @@ from pipeline_mako import render_require_js_path_overrides
...
@@ -104,6 +104,7 @@ from pipeline_mako import render_require_js_path_overrides
<
%
block
name=
"head_extra"
/>
<
%
block
name=
"head_extra"
/>
<
%
include
file=
"/courseware/experiments.html"
/>
<
%
include
file=
"/courseware/experiments.html"
/>
<
%
include
file=
"user_metadata.html"
/>
<
%
static:optional_include_mako
file=
"head-extra.html"
is_theming_enabled=
"True"
/>
<
%
static:optional_include_mako
file=
"head-extra.html"
is_theming_enabled=
"True"
/>
<
%
include
file=
"widgets/optimizely.html"
/>
<
%
include
file=
"widgets/optimizely.html"
/>
...
...
lms/templates/user_metadata.html
0 → 100644
View file @
384f22ff
<
%
page
expression_filter=
"h"
/>
<
%!
from
openedx
.
core
.
djangolib
.
js_utils
import
dump_js_escaped_json
from
eventtracking
import
tracker
from
opaque_keys
.
edx
.
keys
import
CourseKey
%
>
<
%
user_metadata =
{
key:
context
.
get
(
key
)
for
key
in
(
'
username
',
'
user_id
',
'
course_id
',
'
enrollment_mode
',
'
upgrade_link
',
'
upgrade_deadline
',
'
upgrade_price
',
'
pacing_type
',
)
}
if
user:
user_metadata
['
username
']
=
user
.
username
user_metadata
['
user_id
']
=
user
.
id
for
datekey
in
('
schedule_start
',
'
enrollment_time
')
:
user_metadata
[
datekey
]
=
(
context
.
get
(
datekey
).
isoformat
()
if
context
.
get
(
datekey
)
else
None
)
course_key =
context.get('course_key')
if
course
and
not
course_key:
course_key =
course.id
if
course_key:
if
isinstance
(
course_key
,
CourseKey
)
:
user_metadata
['
course_key_fields
']
=
{
'
org
'
:
course_key
.
org
,
'
course
'
:
course_key
.
course
,
'
run
'
:
course_key
.
run
,
}
if
not
course_id:
user_metadata
['
course_id
']
=
unicode
(
course_key
)
elif
isinstance
(
course_key
,
basestring
)
:
user_metadata
['
course_id
']
=
course_key
%
>
<script
type=
"application/json"
id=
"user-metadata"
>
$
{
user_metadata
|
n
,
dump_js_escaped_json
}
</script>
openedx/features/course_experience/tests/views/test_course_home.py
View file @
384f22ff
...
@@ -160,7 +160,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
...
@@ -160,7 +160,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
course_home_url
(
self
.
course
)
course_home_url
(
self
.
course
)
# Fetch the view and verify the query counts
# Fetch the view and verify the query counts
with
self
.
assertNumQueries
(
4
0
,
table_blacklist
=
QUERY_COUNT_TABLE_BLACKLIST
):
with
self
.
assertNumQueries
(
4
1
,
table_blacklist
=
QUERY_COUNT_TABLE_BLACKLIST
):
with
check_mongo_calls
(
4
):
with
check_mongo_calls
(
4
):
url
=
course_home_url
(
self
.
course
)
url
=
course_home_url
(
self
.
course
)
self
.
client
.
get
(
url
)
self
.
client
.
get
(
url
)
...
...
openedx/features/course_experience/tests/views/test_course_updates.py
View file @
384f22ff
...
@@ -127,7 +127,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase):
...
@@ -127,7 +127,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase):
course_updates_url
(
self
.
course
)
course_updates_url
(
self
.
course
)
# Fetch the view and verify that the query counts haven't changed
# Fetch the view and verify that the query counts haven't changed
with
self
.
assertNumQueries
(
3
1
,
table_blacklist
=
QUERY_COUNT_TABLE_BLACKLIST
):
with
self
.
assertNumQueries
(
3
2
,
table_blacklist
=
QUERY_COUNT_TABLE_BLACKLIST
):
with
check_mongo_calls
(
4
):
with
check_mongo_calls
(
4
):
url
=
course_updates_url
(
self
.
course
)
url
=
course_updates_url
(
self
.
course
)
self
.
client
.
get
(
url
)
self
.
client
.
get
(
url
)
openedx/features/course_experience/views/course_sock.py
View file @
384f22ff
...
@@ -6,9 +6,8 @@ from opaque_keys.edx.keys import CourseKey
...
@@ -6,9 +6,8 @@ from opaque_keys.edx.keys import CourseKey
from
web_fragments.fragment
import
Fragment
from
web_fragments.fragment
import
Fragment
from
student.models
import
CourseEnrollment
from
student.models
import
CourseEnrollment
from
course_modes.models
import
CourseMode
from
course_modes.models
import
CourseMode
,
get_cosmetic_verified_display_price
from
courseware.date_summary
import
VerifiedUpgradeDeadlineDate
from
courseware.date_summary
import
VerifiedUpgradeDeadlineDate
from
courseware.views.views
import
get_cosmetic_verified_display_price
from
openedx.core.djangoapps.plugin_api.views
import
EdxFragmentView
from
openedx.core.djangoapps.plugin_api.views
import
EdxFragmentView
...
...
scripts/xss_linter.py
View file @
384f22ff
...
@@ -2392,7 +2392,7 @@ class MakoTemplateLinter(BaseLinter):
...
@@ -2392,7 +2392,7 @@ class MakoTemplateLinter(BaseLinter):
contexts
=
[{
'index'
:
0
,
'type'
:
'html'
}]
contexts
=
[{
'index'
:
0
,
'type'
:
'html'
}]
javascript_types
=
[
javascript_types
=
[
'text/javascript'
,
'text/ecmascript'
,
'application/ecmascript'
,
'application/javascript'
,
'text/javascript'
,
'text/ecmascript'
,
'application/ecmascript'
,
'application/javascript'
,
'text/x-mathjax-config'
,
'json/xblock-args'
'text/x-mathjax-config'
,
'json/xblock-args'
,
'application/json'
,
]
]
html_types
=
[
'text/template'
]
html_types
=
[
'text/template'
]
for
context
in
contexts_re
.
finditer
(
mako_template
):
for
context
in
contexts_re
.
finditer
(
mako_template
):
...
...
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