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
d9bfab5b
Commit
d9bfab5b
authored
Dec 01, 2017
by
Feanil Patel
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' into release-candidate
parents
39fdbd90
70dc3359
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
49 changed files
with
400 additions
and
88 deletions
+400
-88
common/djangoapps/course_modes/models.py
+4
-4
common/djangoapps/course_modes/tests/test_views.py
+2
-0
common/djangoapps/entitlements/models.py
+7
-0
common/test/templates/theme-footer.html
+1
-0
common/test/templates/theme-google-analytics.html
+1
-0
common/test/templates/theme-head-extra.html
+1
-0
common/test/templates/theme-header.html
+1
-0
common/test/test_sites/test_site/templates/courseware/syllabus.html
+1
-0
common/test/test_sites/test_site/templates/courseware/tabs.html
+1
-1
common/test/test_sites/test_site/templates/courseware/test_absolute_path.html
+2
-0
common/test/test_sites/test_site/templates/courseware/test_relative_path.html
+2
-0
common/test/test_sites/test_site/templates/footer.html
+1
-0
common/test/test_sites/test_site/templates/head-extra.html
+1
-0
common/test/test_sites/test_site/templates/login-sidebar.html
+2
-1
common/test/test_sites/test_site/templates/register-sidebar.html
+2
-1
common/test/test_sites/test_site/templates/static_templates/about.html
+1
-0
common/test/test_sites/test_site/templates/static_templates/contact.html
+1
-0
common/test/test_sites/test_site/templates/static_templates/copyright.html
+1
-0
common/test/test_sites/test_site/templates/static_templates/faq.html
+1
-0
common/test/test_sites/test_site/templates/static_templates/tos.html
+1
-0
lms/djangoapps/certificates/views/webview.py
+2
-20
lms/djangoapps/courseware/tests/test_submitting_problems.py
+1
-0
lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py
+3
-0
lms/djangoapps/instructor_task/tests/test_integration.py
+2
-0
lms/djangoapps/lti_provider/tests/test_views.py
+2
-0
lms/djangoapps/shoppingcart/tests/test_views.py
+14
-9
lms/djangoapps/student_account/test/test_views.py
+5
-0
lms/djangoapps/support/tests/test_views.py
+2
-0
lms/static/sass/views/_program-marketing-page.scss
+5
-0
lms/templates/courseware/program_marketing.html
+24
-1
lms/templates/shoppingcart/test/fake_payment_error.html
+1
-0
lms/templates/shoppingcart/test/fake_payment_page.html
+1
-0
openedx/core/djangoapps/catalog/tests/factories.py
+12
-2
openedx/core/djangoapps/content/course_overviews/models.py
+10
-0
openedx/core/djangoapps/content/course_overviews/signals.py
+2
-1
openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py
+17
-0
openedx/core/djangoapps/external_auth/tests/test_shib.py
+4
-2
openedx/core/djangoapps/lang_pref/api.py
+19
-0
openedx/core/djangoapps/programs/tests/test_utils.py
+0
-0
openedx/core/djangoapps/programs/utils.py
+76
-34
openedx/core/djangoapps/schedules/README.rst
+80
-1
openedx/core/djangoapps/schedules/admin.py
+59
-3
openedx/core/djangoapps/schedules/img/system_diagram.png
+0
-0
openedx/core/djangoapps/schedules/resolvers.py
+3
-3
openedx/core/djangoapps/schedules/templates/dropdown_filter.html
+15
-0
openedx/tests/xblock_integration/test_review_xblock.py
+0
-0
requirements/edx/base.txt
+1
-1
requirements/edx/github.txt
+2
-0
scripts/xsslint_thresholds.json
+4
-4
No files found.
common/djangoapps/course_modes/models.py
View file @
d9bfab5b
...
...
@@ -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/course_modes/tests/test_views.py
View file @
d9bfab5b
...
...
@@ -9,6 +9,7 @@ from datetime import datetime, timedelta
import
ddt
import
freezegun
import
httpretty
import
pytest
import
pytz
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
...
...
@@ -68,6 +69,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
(
False
,
None
,
False
,
False
),
)
@ddt.unpack
@pytest.mark.django111_expected_failure
def
test_redirect_to_dashboard
(
self
,
is_active
,
enrollment_mode
,
redirect
,
has_started
):
# Configure whether course has started
# If it has go to course home instead of dashboard
...
...
common/djangoapps/entitlements/models.py
View file @
d9bfab5b
...
...
@@ -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
common/test/templates/theme-footer.html
View file @
d9bfab5b
<
%
page
expression_filter=
"h"
/>
# intentionally left blank
common/test/templates/theme-google-analytics.html
View file @
d9bfab5b
<
%
page
expression_filter=
"h"
/>
# intentionally left blank
common/test/templates/theme-head-extra.html
View file @
d9bfab5b
<
%
page
expression_filter=
"h"
/>
# intentionally left blank
common/test/templates/theme-header.html
View file @
d9bfab5b
<
%
page
expression_filter=
"h"
/>
# intentionally left blank
common/test/test_sites/test_site/templates/courseware/syllabus.html
View file @
d9bfab5b
## mako
<
%
page
expression_filter=
"h"
/>
<
%
namespace
name=
'static'
file=
'/static_content.html'
/>
<
%
include
file=
"${static.get_template_path('courseware/test_relative_path.html')}"
/>
<
%
include
file=
"${static.get_template_path('/courseware/test_absolute_path.html')}"
/>
...
...
common/test/test_sites/test_site/templates/courseware/tabs.html
View file @
d9bfab5b
...
...
@@ -4,7 +4,7 @@
from
django
.
utils
.
translation
import
ugettext
as
_
from
django
.
core
.
urlresolvers
import
reverse
%
>
<
%
page
args=
"tab_list, active_page, default_tab, tab_image"
/>
<
%
page
args=
"tab_list, active_page, default_tab, tab_image"
expression_filter=
"h"
/>
<
%
def
url_class
(
is_active
)
:
...
...
common/test/test_sites/test_site/templates/courseware/test_absolute_path.html
View file @
d9bfab5b
## mako
<
%
page
expression_filter=
"h"
/>
<
%
namespace
name=
'static'
file=
'/static_content.html'
/>
<div>
Microsite absolute path template contents
</div>
\ No newline at end of file
common/test/test_sites/test_site/templates/courseware/test_relative_path.html
View file @
d9bfab5b
## mako
<
%
page
expression_filter=
"h"
/>
<
%
namespace
name=
'static'
file=
'/static_content.html'
/>
<div>
Microsite relative path template contents
</div>
\ No newline at end of file
common/test/test_sites/test_site/templates/footer.html
View file @
d9bfab5b
## mako
<
%
page
expression_filter=
"h"
/>
<
%
namespace
name=
'static'
file=
'static_content.html'
/>
<
%!
from
django
.
core
.
urlresolvers
import
reverse
...
...
common/test/test_sites/test_site/templates/head-extra.html
View file @
d9bfab5b
<
%
page
expression_filter=
"h"
/>
<
%
namespace
name=
'static'
file=
'../../static_content.html'
/>
<
%
style_overrides_file =
static.get_value('css_overrides_file')
%
>
...
...
common/test/test_sites/test_site/templates/login-sidebar.html
View file @
d9bfab5b
<
%!
<
%
page
expression_filter=
"h"
/>
<
%!
from
django
.
utils
.
translation
import
ugettext
as
_
from
django
.
core
.
urlresolvers
import
reverse
%
>
...
...
common/test/test_sites/test_site/templates/register-sidebar.html
View file @
d9bfab5b
<
%!
<
%
page
expression_filter=
"h"
/>
<
%!
from
django
.
utils
.
translation
import
ugettext
as
_
from
django
.
core
.
urlresolvers
import
reverse
%
>
...
...
common/test/test_sites/test_site/templates/static_templates/about.html
View file @
d9bfab5b
<
%
page
expression_filter=
"h"
/>
<
%!
from
django
.
core
.
urlresolvers
import
reverse
%
>
<
%
namespace
name=
'static'
file=
'../../../static_content.html'
/>
...
...
common/test/test_sites/test_site/templates/static_templates/contact.html
View file @
d9bfab5b
<
%
page
expression_filter=
"h"
/>
<
%
inherit
file=
"../main.html"
/>
<
%
namespace
name=
'static'
file=
'../static_content.html'
/>
<
%!
...
...
common/test/test_sites/test_site/templates/static_templates/copyright.html
View file @
d9bfab5b
<
%
page
expression_filter=
"h"
/>
This is a copyright page for an Open edX site.
common/test/test_sites/test_site/templates/static_templates/faq.html
View file @
d9bfab5b
<
%
page
expression_filter=
"h"
/>
<
%
inherit
file=
"../main.html"
/>
<
%
namespace
name=
'static'
file=
'../static_content.html'
/>
<
%!
...
...
common/test/test_sites/test_site/templates/static_templates/tos.html
View file @
d9bfab5b
<
%
page
expression_filter=
"h"
/>
<
%
inherit
file=
"../main.html"
/>
<
%
namespace
name=
'static'
file=
'../static_content.html'
/>
<
%!
...
...
lms/djangoapps/certificates/views/webview.py
View file @
d9bfab5b
...
...
@@ -40,7 +40,7 @@ from courseware.courses import get_course_by_id
from
edxmako.shortcuts
import
render_to_response
from
edxmako.template
import
Template
from
openedx.core.djangoapps.catalog.utils
import
get_course_run_details
from
openedx.core.djangoapps.lang_pref.api
import
released_languages
from
openedx.core.djangoapps.lang_pref.api
import
get_closest_released_language
from
openedx.core.djangoapps.site_configuration
import
helpers
as
configuration_helpers
from
openedx.core.lib.courses
import
course_image_url
from
openedx.core.djangoapps.certificates.api
import
display_date_for_certificate
,
certificates_viewable_for_course
...
...
@@ -656,7 +656,7 @@ def _get_custom_template_and_language(course_id, course_mode, course_language):
Return the custom certificate template, if any, that should be rendered for the provided course/mode/language
combination, along with the language that should be used to render that template.
"""
closest_released_language
=
_
get_closest_released_language
(
course_language
)
if
course_language
else
None
closest_released_language
=
get_closest_released_language
(
course_language
)
if
course_language
else
None
template
=
get_certificate_template
(
course_id
,
course_mode
,
closest_released_language
)
if
template
and
template
.
language
:
...
...
@@ -667,24 +667,6 @@ def _get_custom_template_and_language(course_id, course_mode, course_language):
return
(
None
,
None
)
def
_get_closest_released_language
(
target
):
"""
Return the language code that most closely matches the target and is fully supported by the LMS, or None
if there are no fully supported languages that match the target.
"""
match
=
None
languages
=
released_languages
()
for
language
in
languages
:
if
language
.
code
==
target
:
match
=
language
.
code
break
elif
(
match
is
None
)
and
(
language
.
code
[:
2
]
==
target
[:
2
]):
match
=
language
.
code
return
match
def
_render_invalid_certificate
(
course_id
,
platform_name
,
configuration
):
context
=
{}
_update_context_with_basic_info
(
context
,
course_id
,
platform_name
,
configuration
)
...
...
lms/djangoapps/courseware/tests/test_submitting_problems.py
View file @
d9bfab5b
...
...
@@ -337,6 +337,7 @@ class TestCourseGrades(TestSubmittingProblems):
@attr
(
shard
=
3
)
@ddt.ddt
@pytest.mark.django111_expected_failure
class
TestCourseGrader
(
TestSubmittingProblems
):
"""
Suite of tests for the course grader.
...
...
lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py
View file @
d9bfab5b
...
...
@@ -4,6 +4,7 @@ Unit tests for instructor_dashboard.py.
import
datetime
import
ddt
import
pytest
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
django.test.client
import
RequestFactory
...
...
@@ -320,6 +321,7 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase, XssT
# Max number of student per page is one. Patched setting MAX_STUDENTS_PER_PAGE_GRADE_BOOK = 1
self
.
assertEqual
(
len
(
response
.
mako_context
[
'students'
]),
1
)
# pylint: disable=no-member
@pytest.mark.django111_expected_failure
def
test_open_response_assessment_page
(
self
):
"""
Test that Open Responses is available only if course contains at least one ORA block
...
...
@@ -339,6 +341,7 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase, XssT
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertIn
(
ora_section
,
response
.
content
)
@pytest.mark.django111_expected_failure
def
test_open_response_assessment_page_orphan
(
self
):
"""
Tests that the open responses tab loads if the course contains an
...
...
lms/djangoapps/instructor_task/tests/test_integration.py
View file @
d9bfab5b
...
...
@@ -11,6 +11,7 @@ import textwrap
from
collections
import
namedtuple
import
ddt
import
pytest
from
celery.states
import
FAILURE
,
SUCCESS
from
django.contrib.auth.models
import
User
from
django.core.urlresolvers
import
reverse
...
...
@@ -67,6 +68,7 @@ class TestIntegrationTask(InstructorTaskModuleTestCase):
@attr
(
shard
=
3
)
@ddt.ddt
@pytest.mark.django111_expected_failure
class
TestRescoringTask
(
TestIntegrationTask
):
"""
Integration-style tests for rescoring problems in a background task.
...
...
lms/djangoapps/lti_provider/tests/test_views.py
View file @
d9bfab5b
...
...
@@ -2,6 +2,7 @@
Tests for the LTI provider views
"""
import
pytest
from
django.core.urlresolvers
import
reverse
from
django.test
import
TestCase
from
django.test.client
import
RequestFactory
...
...
@@ -163,6 +164,7 @@ class LtiLaunchTest(LtiTestMixin, TestCase):
@attr
(
shard
=
3
)
@pytest.mark.django111_expected_failure
class
LtiLaunchTestRender
(
LtiTestMixin
,
RenderXBlockTestMixin
,
ModuleStoreTestCase
):
"""
Tests for the rendering returned by lti_launch view.
...
...
lms/djangoapps/shoppingcart/tests/test_views.py
View file @
d9bfab5b
...
...
@@ -8,6 +8,7 @@ from decimal import Decimal
from
urlparse
import
urlparse
import
ddt
import
pytest
import
pytz
from
django.conf
import
settings
from
django.contrib.admin.sites
import
AdminSite
...
...
@@ -198,7 +199,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin):
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
"password"
)
def
test_add_course_to_cart_anon
(
self
):
resp
=
self
.
client
.
post
(
reverse
(
'
shoppingcart.views.
add_course_to_cart'
,
args
=
[
self
.
course_key
.
to_deprecated_string
()]))
resp
=
self
.
client
.
post
(
reverse
(
'add_course_to_cart'
,
args
=
[
self
.
course_key
.
to_deprecated_string
()]))
self
.
assertEqual
(
resp
.
status_code
,
403
)
@patch
(
'shoppingcart.views.render_to_response'
,
render_mock
)
...
...
@@ -260,7 +261,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin):
self
.
login_user
()
# add first course to user cart
resp
=
self
.
client
.
post
(
reverse
(
'
shoppingcart.views.
add_course_to_cart'
,
args
=
[
self
.
course_key
.
to_deprecated_string
()])
reverse
(
'add_course_to_cart'
,
args
=
[
self
.
course_key
.
to_deprecated_string
()])
)
self
.
assertEqual
(
resp
.
status_code
,
200
)
# add and apply the coupon code to course in the cart
...
...
@@ -273,7 +274,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin):
#now add the second course to cart, the coupon code should be
# applied when adding the second course to the cart
resp
=
self
.
client
.
post
(
reverse
(
'
shoppingcart.views.
add_course_to_cart'
,
args
=
[
self
.
testing_course
.
id
.
to_deprecated_string
()])
reverse
(
'add_course_to_cart'
,
args
=
[
self
.
testing_course
.
id
.
to_deprecated_string
()])
)
self
.
assertEqual
(
resp
.
status_code
,
200
)
#now check the user cart and see that the discount has been applied on both the courses
...
...
@@ -286,7 +287,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin):
def
test_add_course_to_cart_already_in_cart
(
self
):
PaidCourseRegistration
.
add_to_order
(
self
.
cart
,
self
.
course_key
)
self
.
login_user
()
resp
=
self
.
client
.
post
(
reverse
(
'
shoppingcart.views.
add_course_to_cart'
,
args
=
[
self
.
course_key
.
to_deprecated_string
()]))
resp
=
self
.
client
.
post
(
reverse
(
'add_course_to_cart'
,
args
=
[
self
.
course_key
.
to_deprecated_string
()]))
self
.
assertEqual
(
resp
.
status_code
,
400
)
self
.
assertIn
(
'The course {0} is already in your cart.'
.
format
(
self
.
course_key
.
to_deprecated_string
()),
resp
.
content
)
...
...
@@ -475,6 +476,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin):
self
.
assertIn
(
"Cart item quantity should not be greater than 1 when applying activation code"
,
resp
.
content
)
@ddt.data
(
True
,
False
)
@pytest.mark.django111_expected_failure
def
test_reg_code_uses_associated_mode
(
self
,
expired_mode
):
"""Tests the use of reg codes on verified courses, expired or active. """
course_key
=
self
.
course_key
.
to_deprecated_string
()
...
...
@@ -487,6 +489,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin):
self
.
assertIn
(
self
.
course
.
display_name
.
encode
(
'utf-8'
),
resp
.
content
)
@ddt.data
(
True
,
False
)
@pytest.mark.django111_expected_failure
def
test_reg_code_uses_unknown_mode
(
self
,
expired_mode
):
"""Tests the use of reg codes on verified courses, expired or active. """
course_key
=
self
.
course_key
.
to_deprecated_string
()
...
...
@@ -769,20 +772,20 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin):
def
test_add_course_to_cart_already_registered
(
self
):
CourseEnrollment
.
enroll
(
self
.
user
,
self
.
course_key
)
self
.
login_user
()
resp
=
self
.
client
.
post
(
reverse
(
'
shoppingcart.views.
add_course_to_cart'
,
args
=
[
self
.
course_key
.
to_deprecated_string
()]))
resp
=
self
.
client
.
post
(
reverse
(
'add_course_to_cart'
,
args
=
[
self
.
course_key
.
to_deprecated_string
()]))
self
.
assertEqual
(
resp
.
status_code
,
400
)
self
.
assertIn
(
'You are already registered in course {0}.'
.
format
(
self
.
course_key
.
to_deprecated_string
()),
resp
.
content
)
def
test_add_nonexistent_course_to_cart
(
self
):
self
.
login_user
()
resp
=
self
.
client
.
post
(
reverse
(
'
shoppingcart.views.
add_course_to_cart'
,
args
=
[
'non/existent/course'
]))
resp
=
self
.
client
.
post
(
reverse
(
'add_course_to_cart'
,
args
=
[
'non/existent/course'
]))
self
.
assertEqual
(
resp
.
status_code
,
404
)
self
.
assertIn
(
"The course you requested does not exist."
,
resp
.
content
)
def
test_add_course_to_cart_success
(
self
):
self
.
login_user
()
reverse
(
'
shoppingcart.views.
add_course_to_cart'
,
args
=
[
self
.
course_key
.
to_deprecated_string
()])
resp
=
self
.
client
.
post
(
reverse
(
'
shoppingcart.views.
add_course_to_cart'
,
args
=
[
self
.
course_key
.
to_deprecated_string
()]))
reverse
(
'add_course_to_cart'
,
args
=
[
self
.
course_key
.
to_deprecated_string
()])
resp
=
self
.
client
.
post
(
reverse
(
'add_course_to_cart'
,
args
=
[
self
.
course_key
.
to_deprecated_string
()]))
self
.
assertEqual
(
resp
.
status_code
,
200
)
self
.
assertTrue
(
PaidCourseRegistration
.
contained_in_order
(
self
.
cart
,
self
.
course_key
))
...
...
@@ -1379,7 +1382,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin):
self
.
_assert_404
(
reverse
(
'shoppingcart.views.show_cart'
,
args
=
[]))
self
.
_assert_404
(
reverse
(
'shoppingcart.views.clear_cart'
,
args
=
[]))
self
.
_assert_404
(
reverse
(
'shoppingcart.views.remove_item'
,
args
=
[]),
use_post
=
True
)
self
.
_assert_404
(
reverse
(
'
shoppingcart.views.
register_code_redemption'
,
args
=
[
"testing"
]))
self
.
_assert_404
(
reverse
(
'register_code_redemption'
,
args
=
[
"testing"
]))
self
.
_assert_404
(
reverse
(
'shoppingcart.views.use_code'
,
args
=
[]),
use_post
=
True
)
self
.
_assert_404
(
reverse
(
'shoppingcart.views.update_user_cart'
,
args
=
[]))
self
.
_assert_404
(
reverse
(
'shoppingcart.views.reset_code_redemption'
,
args
=
[]),
use_post
=
True
)
...
...
@@ -1440,6 +1443,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin):
}
)
@pytest.mark.django111_expected_failure
def
test_shopping_cart_navigation_link_not_in_microsite
(
self
):
"""
Tests shopping cart link is available in navigation header if request is not from a microsite.
...
...
@@ -1474,6 +1478,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin):
self
.
assertEqual
(
resp
.
status_code
,
200
)
self
.
assertIn
(
'<a class="shopping-cart"'
,
resp
.
content
)
@pytest.mark.django111_expected_failure
def
test_shopping_cart_navigation_link_in_microsite_courseware_page
(
self
):
"""
Tests shopping cart link is not available in navigation header if request is from a microsite
...
...
lms/djangoapps/student_account/test/test_views.py
View file @
d9bfab5b
...
...
@@ -8,6 +8,7 @@ from urllib import urlencode
import
ddt
import
mock
import
pytest
from
django.conf
import
settings
from
django.contrib
import
messages
from
django.contrib.auth
import
get_user_model
...
...
@@ -470,6 +471,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
(
'register_user'
,
'register'
),
)
@ddt.unpack
@pytest.mark.django111_expected_failure
def
test_hinted_login_dialog_disabled
(
self
,
url_name
,
auth_entry
):
"""Test that the dialog doesn't show up for hinted logins when disabled. """
self
.
google_provider
.
skip_hinted_login_dialog
=
True
...
...
@@ -513,6 +515,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
(
'register_user'
,
'register'
),
)
@ddt.unpack
@pytest.mark.django111_expected_failure
def
test_settings_tpa_hinted_login_dialog_disabled
(
self
,
url_name
,
auth_entry
):
"""Test that the dialog doesn't show up for hinted logins when disabled via settings.THIRD_PARTY_AUTH_HINT. """
self
.
google_provider
.
skip_hinted_login_dialog
=
True
...
...
@@ -585,6 +588,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
self
.
assertEqual
(
enterprise_cookie
.
value
,
''
)
@override_settings
(
SITE_NAME
=
settings
.
MICROSITE_TEST_HOSTNAME
)
@pytest.mark.django111_expected_failure
def
test_microsite_uses_old_login_page
(
self
):
# Retrieve the login page from a microsite domain
# and verify that we're served the old page.
...
...
@@ -595,6 +599,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
self
.
assertContains
(
resp
,
"Log into your Test Site Account"
)
self
.
assertContains
(
resp
,
"login-form"
)
@pytest.mark.django111_expected_failure
def
test_microsite_uses_old_register_page
(
self
):
# Retrieve the register page from a microsite domain
# and verify that we're served the old page.
...
...
lms/djangoapps/support/tests/test_views.py
View file @
d9bfab5b
...
...
@@ -9,6 +9,7 @@ import re
from
datetime
import
datetime
,
timedelta
import
ddt
import
pytest
from
django.core.urlresolvers
import
reverse
from
django.db.models
import
signals
from
nose.plugins.attrib
import
attr
...
...
@@ -66,6 +67,7 @@ class SupportViewAccessTests(SupportViewTestCase):
))
))
@ddt.unpack
@pytest.mark.django111_expected_failure
def
test_access
(
self
,
url_name
,
role
,
has_access
):
if
role
is
not
None
:
role
()
.
add_users
(
self
.
user
)
...
...
lms/static/sass/views/_program-marketing-page.scss
View file @
d9bfab5b
...
...
@@ -20,6 +20,11 @@
.btn
{
font-size
:
20px
;
font-weight
:
$font-weight-bold
;
.original-price
{
text-decoration
:
line-through
;
font-weight
:
$font-weight-normal
;
}
}
.btn
,
...
...
lms/templates/courseware/program_marketing.html
View file @
d9bfab5b
...
...
@@ -62,6 +62,8 @@ endorser_org = endorser_position.get('organization_name') or corporate_endorseme
faqs =
program['faq']
courses =
program['courses']
instructors =
program['instructors']
full_program_price_format =
'{0:.0f}'
if
program
['
full_program_price
'].
is_integer
()
else
'{
0:
.
2f
}'
full_program_price =
full_program_price_format.format(program['full_program_price'])
%
>
<div
id=
"program-details-hero"
>
<div
class=
"main-banner"
...
...
@@ -83,9 +85,30 @@ endorser_org = endorser_position.get('organization_name') or corporate_endorseme
<h2>
${program['subtitle']}
</h2>
</div>
<div>
## Note: Weird formatting to fix the inline spacing issue.
% if program.get('is_learner_eligible_for_one_click_purchase'):
<a
href=
"${buy_button_href}"
class=
"btn btn-success"
>
${_('Purchase the Program')}
<span>
${_('Purchase the Program (')}
</span
%
if
program.get('discount_data')
and
program['discount_data']['is_discounted']:
><span
aria-label=
"${_('Original Price')}"
class=
"original-price"
>
${Text(_('${oldPrice}')).format(
oldPrice=full_program_price_format.format(program['discount_data']['total_incl_tax_excl_discounts'])
)}
</span
><span
aria-label=
"${_('Discounted Price')}"
class=
"discount"
>
${Text(_('${newPrice}')).format(
newPrice=full_program_price,
)}
</span
><span
class=
"savings"
>
${Text(_('{currency})')).format(
discount_value=full_program_price_format.format(program['discount_data']['discount_value']),
currency=program['discount_data']['currency']
)}
</span>
% else:
>
<span>
${"${price})".format(price=full_program_price)}
</span>
% endif
</a>
% else:
<a
href=
"#courses"
class=
"btn btn-success"
>
...
...
lms/templates/shoppingcart/test/fake_payment_error.html
View file @
d9bfab5b
<
%
page
expression_filter=
"h"
/>
<html>
<head>
<title>
Payment Error
</title>
...
...
lms/templates/shoppingcart/test/fake_payment_page.html
View file @
d9bfab5b
<
%
page
expression_filter=
"h"
/>
<html>
<head><title>
Payment Form
</title>
</head>
...
...
openedx/core/djangoapps/catalog/tests/factories.py
View file @
d9bfab5b
...
...
@@ -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/content/course_overviews/models.py
View file @
d9bfab5b
...
...
@@ -17,6 +17,7 @@ from model_utils.models import TimeStampedModel
from
config_models.models
import
ConfigurationModel
from
lms.djangoapps
import
django_comment_client
from
openedx.core.djangoapps.catalog.models
import
CatalogIntegration
from
openedx.core.djangoapps.lang_pref.api
import
get_closest_released_language
from
openedx.core.djangoapps.models.course_details
import
CourseDetails
from
static_replace.models
import
AssetBaseUrlConfig
from
xmodule
import
course_metadata_utils
,
block_metadata_utils
...
...
@@ -610,6 +611,15 @@ class CourseOverview(TimeStampedModel):
"""
return
'self'
if
self
.
self_paced
else
'instructor'
@property
def
closest_released_language
(
self
):
"""
Returns the language code that most closely matches this course' language and is fully
supported by the LMS, or None if there are no fully supported languages that
match the target.
"""
return
get_closest_released_language
(
self
.
language
)
if
self
.
language
else
None
def
apply_cdn_to_urls
(
self
,
image_urls
):
"""
Given a dict of resolutions -> urls, return a copy with CDN applied.
...
...
openedx/core/djangoapps/content/course_overviews/signals.py
View file @
d9bfab5b
...
...
@@ -59,7 +59,8 @@ def _log_start_date_change(previous_course_overview, updated_course_overview):
new_start_str
=
'None'
if
updated_course_overview
.
start
is
not
None
:
new_start_str
=
updated_course_overview
.
start
.
isoformat
()
LOG
.
info
(
'Course start date changed: previous={0} new={1}'
.
format
(
LOG
.
info
(
'Course start date changed: course={0} previous={1} new={2}'
.
format
(
updated_course_overview
.
id
,
previous_start_str
,
new_start_str
,
))
...
...
openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py
View file @
d9bfab5b
...
...
@@ -18,6 +18,7 @@ from PIL import Image
from
lms.djangoapps.certificates.api
import
get_active_web_certificate
from
openedx.core.djangoapps.catalog.tests.mixins
import
CatalogIntegrationMixin
from
openedx.core.djangoapps.dark_lang.models
import
DarkLangConfig
from
openedx.core.djangoapps.models.course_details
import
CourseDetails
from
openedx.core.lib.courses
import
course_image_url
from
static_replace.models
import
AssetBaseUrlConfig
...
...
@@ -37,6 +38,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
check_mongo_calls_range
from
..models
import
CourseOverview
,
CourseOverviewImageSet
,
CourseOverviewImageConfig
from
.factories
import
CourseOverviewFactory
@attr
(
shard
=
3
)
...
...
@@ -289,6 +291,21 @@ class CourseOverviewTestCase(CatalogIntegrationMixin, ModuleStoreTestCase):
else
:
self
.
assertEqual
(
course_overview
.
language
,
course
.
language
)
@ddt.data
(
(
'fa'
,
'fa-ir'
,
'fa'
),
(
'fa'
,
'fa'
,
'fa'
),
(
'es-419'
,
'es-419'
,
'es-419'
),
(
'es-419'
,
'es-es'
,
'es-419'
),
(
'es-419'
,
'es'
,
'es-419'
),
(
'es-419'
,
None
,
None
),
(
'es-419'
,
'fr'
,
None
),
)
@ddt.unpack
def
test_closest_released_language
(
self
,
released_languages
,
course_language
,
expected_language
):
DarkLangConfig
(
released_languages
=
released_languages
,
enabled
=
True
,
changed_by
=
self
.
user
)
.
save
()
course_overview
=
CourseOverviewFactory
.
create
(
language
=
course_language
)
self
.
assertEqual
(
course_overview
.
closest_released_language
,
expected_language
)
@ddt.data
(
ModuleStoreEnum
.
Type
.
split
,
ModuleStoreEnum
.
Type
.
mongo
)
def
test_get_non_existent_course
(
self
,
modulestore_type
):
"""
...
...
openedx/core/djangoapps/external_auth/tests/test_shib.py
View file @
d9bfab5b
...
...
@@ -5,7 +5,10 @@ Tests for Shibboleth Authentication
@jbau
"""
import
unittest
from
importlib
import
import_module
from
urllib
import
urlencode
import
pytest
from
ddt
import
ddt
,
data
from
django.conf
import
settings
from
django.http
import
HttpResponseRedirect
...
...
@@ -14,14 +17,12 @@ from django.test.client import RequestFactory, Client as DjangoTestClient
from
django.test.utils
import
override_settings
from
django.core.urlresolvers
import
reverse
from
django.contrib.auth.models
import
AnonymousUser
,
User
from
importlib
import
import_module
from
openedx.core.djangoapps.external_auth.models
import
ExternalAuthMap
from
openedx.core.djangoapps.external_auth.views
import
(
shib_login
,
course_specific_login
,
course_specific_register
,
_flatten_to_ascii
)
from
mock
import
patch
from
nose.plugins.attrib
import
attr
from
urllib
import
urlencode
from
openedx.core.djangolib.testing.utils
import
CacheIsolationTestCase
from
student.views
import
change_enrollment
...
...
@@ -297,6 +298,7 @@ class ShibSPTest(CacheIsolationTestCase):
@unittest.skipUnless
(
settings
.
FEATURES
.
get
(
'AUTH_USE_SHIB'
),
"AUTH_USE_SHIB not set"
)
@data
(
*
gen_all_identities
())
@pytest.mark.django111_expected_failure
def
test_registration_form_submit
(
self
,
identity
):
"""
Tests user creation after the registration form that pops is submitted. If there is no shib
...
...
openedx/core/djangoapps/lang_pref/api.py
View file @
d9bfab5b
...
...
@@ -73,3 +73,22 @@ def all_languages():
"""
languages
=
[(
lang
[
0
],
_
(
lang
[
1
]))
for
lang
in
settings
.
ALL_LANGUAGES
]
# pylint: disable=translation-of-non-string
return
sorted
(
languages
,
key
=
lambda
lang
:
lang
[
1
])
def
get_closest_released_language
(
target_language_code
):
"""
Return the language code that most closely matches the target and is fully
supported by the LMS, or None if there are no fully supported languages that
match the target.
"""
match
=
None
languages
=
released_languages
()
for
language
in
languages
:
if
language
.
code
==
target_language_code
:
match
=
language
.
code
break
elif
(
match
is
None
)
and
(
language
.
code
[:
2
]
==
target_language_code
[:
2
]):
match
=
language
.
code
return
match
openedx/core/djangoapps/programs/tests/test_utils.py
View file @
d9bfab5b
This diff is collapsed.
Click to expand it.
openedx/core/djangoapps/programs/utils.py
View file @
d9bfab5b
...
...
@@ -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.
...
...
openedx/core/djangoapps/schedules/README.rst
View file @
d9bfab5b
...
...
@@ -101,9 +101,16 @@ Glossary
the number of emails each task must send.
- **Email Backend**: An external service that ACE will use to deliver emails.
Right now, ACE only supports `Sailthru <http://www.sailthru.com/>`
as an
For now, ACE only supports `Sailthru <http://www.sailthru.com/>`__
as an
email backend.
An Overview of edX's Dynamic Pacing System
------------------------------------------
.. image:: img/system_diagram.png
Running the Management Commands
-------------------------------
...
...
@@ -366,6 +373,78 @@ Course Update
- Their Schedule ``start_date`` must be 7, 14, or any increment of 7
days up to 77 days before the current date.
Analytics
~~~~~~~~~
To track the performance of these communications, there is an integration setup
with Google Analytics and Segment. When a message is sent a Segment event is
emitted that contains the unique message identifier and a bunch of other data
about the message that was sent. When a user opens an email, an invisible
tracking pixel is rendered that records an event in Google Analytics. When a
user clicks a link in the email,
`UTM parameters <https://en.wikipedia.org/wiki/UTM_parameters>`__ are included
in the query string which allow Google Analytics to know that the traffic was
driven to the LMS by that email.
Using these three pieces of information you can track many key metrics.
Specifically: you can monitor the number of messages sent, the ratio of messages
opened to messages sent, and the ratio of links clicked in messages to the
messages opened. These help you answer a few key questions: How many people
am I reaching? How many people are opening my messages? How many people are
persuaded to actually come back to my site after reading my message?
You can also filter Google Analytics to compare the behavior of the users
coming to your platform from these emails relative to other sources of traffic.
Enabling Tracking
^^^^^^^^^^^^^^^^^
- In either your site configuration or django settings set
``GOOGLE_ANALYTICS_TRACKING_ID`` to your Google Analytics tracking ID. This
will look something like UA-XXXXXXX-X
- In your django settings set ``LMS_SEGMENT_KEY`` to your Segment project
write key.
Emitted Events
^^^^^^^^^^^^^^
The segment event that is emitted when a message is sent is named
"edx.bi.email.sent" and contains the following information:
- ``send_uuid`` uniquely identifies this batch of emails that are being sent to
many learners.
- ``uuid`` uniquely identifies this particular message being sent to exactly
one learner.
- ``site`` is the site that the email was sent for.
- ``app_label`` will always be "schedules" for the emails sent from here.
- ``name`` will be the name of the message that was sent: recurringnudge_day3,
recurringnudge_day10, upgradereminder, or courseupdate.
- ``primary_course_id`` identifies the primary course discussed in the email if
the email was sent on behalf of several courses.
- ``language`` is the language the email was translated into.
- ``course_ids`` is a list of all courses that this email was sent on behalf of.
This can be truncated if the list of courses is long.
- ``num_courses`` is the actual number of courses covered by this message. This
may differ from the course_ids list if the list was truncated.
The Google Analytics event that is emitted when a learner opens an email has
the following properties:
- ``action`` is "edx.bi.email.opened"
- ``category`` is "email"
- ``label`` is the primary_course_id described above
- ``campaign source`` is "schedules"
- ``campaign medium`` is "email"
- ``campaign content`` is the unique identifier for the message
When the user clicks a link in the email the following UTM parameters are
included in the URL:
- ``campaign source`` is "schedules"
- ``campaign medium`` is "email"
- ``campaign content`` is the unique identifier for the message
- ``campaign term`` is the primary_course_id described above
Litmus
------
...
...
openedx/core/djangoapps/schedules/admin.py
View file @
d9bfab5b
...
...
@@ -8,6 +8,8 @@ from django.utils.translation import ugettext_lazy as _
from
openedx.core.djangolib.markup
import
HTML
from
.
import
models
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
opaque_keys.edx.keys
import
CourseKey
class
ScheduleExperienceAdminInline
(
admin
.
StackedInline
):
...
...
@@ -45,7 +47,10 @@ for (db_name, human_name) in models.ScheduleExperience.EXPERIENCES:
class
KnownErrorCases
(
admin
.
SimpleListFilter
):
title
=
_
(
'KnownErrorCases'
)
"""
Filter schedules by a list of known error cases.
"""
title
=
_
(
'Known Error Case'
)
parameter_name
=
'error'
...
...
@@ -59,14 +64,65 @@ class KnownErrorCases(admin.SimpleListFilter):
return
queryset
.
filter
(
start__lt
=
F
(
'enrollment__course__start'
))
class
CourseIdFilter
(
admin
.
SimpleListFilter
):
"""
Filter schedules to by course id using a dropdown list.
"""
template
=
"dropdown_filter.html"
title
=
_
(
"Course Id"
)
parameter_name
=
"course_id"
def
__init__
(
self
,
request
,
params
,
model
,
model_admin
):
super
(
CourseIdFilter
,
self
)
.
__init__
(
request
,
params
,
model
,
model_admin
)
self
.
unused_parameters
=
params
.
copy
()
self
.
unused_parameters
.
pop
(
self
.
parameter_name
,
None
)
def
value
(
self
):
value
=
super
(
CourseIdFilter
,
self
)
.
value
()
if
value
==
"None"
or
value
is
None
:
return
None
else
:
return
CourseKey
.
from_string
(
value
)
def
lookups
(
self
,
request
,
model_admin
):
return
(
(
overview
.
id
,
unicode
(
overview
.
id
))
for
overview
in
CourseOverview
.
objects
.
all
()
.
order_by
(
'id'
)
)
def
queryset
(
self
,
request
,
queryset
):
value
=
self
.
value
()
if
value
is
None
:
return
queryset
else
:
return
queryset
.
filter
(
enrollment__course_id
=
value
)
def
choices
(
self
,
changelist
):
# pylint: disable=unused-argument
yield
{
'selected'
:
self
.
value
()
is
None
,
'value'
:
None
,
'display'
:
_
(
'All'
),
}
for
lookup
,
title
in
self
.
lookup_choices
:
yield
{
'selected'
:
self
.
value
()
==
lookup
,
'value'
:
unicode
(
lookup
),
'display'
:
title
,
}
@admin.register
(
models
.
Schedule
)
class
ScheduleAdmin
(
admin
.
ModelAdmin
):
list_display
=
(
'username'
,
'course_id'
,
'active'
,
'start'
,
'upgrade_deadline'
,
'experience_display'
)
list_display_links
=
(
'start'
,
'upgrade_deadline'
,
'experience_display'
)
list_filter
=
(
'experience__experience_type'
,
'active'
,
KnownErrorCases
)
list_filter
=
(
CourseIdFilter
,
'experience__experience_type'
,
'active'
,
KnownErrorCases
)
raw_id_fields
=
(
'enrollment'
,)
readonly_fields
=
(
'modified'
,)
search_fields
=
(
'enrollment__user__username'
,
'enrollment__course__id'
,
)
search_fields
=
(
'enrollment__user__username'
,)
inlines
=
(
ScheduleExperienceAdminInline
,)
actions
=
[
'deactivate_schedules'
,
'activate_schedules'
]
+
experience_actions
...
...
openedx/core/djangoapps/schedules/img/system_diagram.png
0 → 100644
View file @
d9bfab5b
76.1 KB
openedx/core/djangoapps/schedules/resolvers.py
View file @
d9bfab5b
...
...
@@ -195,7 +195,7 @@ class BinnedSchedulesBaseResolver(PrefixedDebugLoggerMixin, RecipientResolver):
except
InvalidContextError
:
continue
yield
(
user
,
first_schedule
.
enrollment
.
course
.
language
,
template_context
)
yield
(
user
,
first_schedule
.
enrollment
.
course
.
closest_released_
language
,
template_context
)
def
get_template_context
(
self
,
user
,
user_schedules
):
"""
...
...
@@ -317,7 +317,7 @@ def _get_upsell_information_for_schedule(user, schedule):
enrollment
.
dynamic_upgrade_deadline
,
get_format
(
'DATE_FORMAT'
,
lang
=
course
.
language
,
lang
=
course
.
closest_released_
language
,
use_l10n
=
True
)
)
...
...
@@ -370,7 +370,7 @@ class CourseUpdateResolver(BinnedSchedulesBaseResolver):
})
template_context
.
update
(
_get_upsell_information_for_schedule
(
user
,
schedule
))
yield
(
user
,
schedule
.
enrollment
.
course
.
language
,
template_context
)
yield
(
user
,
schedule
.
enrollment
.
course
.
closest_released_
language
,
template_context
)
def
_get_trackable_course_home_url
(
course_id
):
...
...
openedx/core/djangoapps/schedules/templates/dropdown_filter.html
0 → 100644
View file @
d9bfab5b
{% load i18n %}
<h3>
{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}
</h3>
<form
method=
"GET"
>
{% for name, param in spec.unused_parameters.items %}
<input
type=
"hidden"
name=
"{{ name }}"
value=
"{{ param }}"
/>
{% endfor %}
<select
name=
"{{ spec.parameter_name }}"
>
{% for choice in choices %}
<option
{%
if
choice
.
selected
%}
selected=
"selected"
{%
endif
%}
value=
"{{ choice.value }}"
>
{{ choice.display }}
</option>
{% endfor %}
</select>
<input
type=
"submit"
value=
"Filter!"
/>
</form>
openedx/tests/xblock_integration/test_review_xblock.py
0 → 100644
View file @
d9bfab5b
This diff is collapsed.
Click to expand it.
requirements/edx/base.txt
View file @
d9bfab5b
...
...
@@ -47,7 +47,7 @@ edx-lint==0.4.3
astroid==1.3.8
edx-django-oauth2-provider==1.2.5
edx-django-sites-extensions==2.3.0
edx-enterprise==0.55.
0
edx-enterprise==0.55.
1
edx-oauth2-provider==1.2.2
edx-opaque-keys==0.4.0
edx-organizations==0.4.8
...
...
requirements/edx/github.txt
View file @
d9bfab5b
...
...
@@ -101,6 +101,8 @@ git+https://github.com/edx/xblock-utils.git@v1.0.5#egg=xblock-utils==1.0.5
git+https://github.com/edx/edx-user-state-client.git@1.0.1#egg=edx-user-state-client==1.0.1
git+https://github.com/edx/xblock-lti-consumer.git@v1.1.6#egg=lti_consumer-xblock==1.1.6
git+https://github.com/edx/edx-proctoring.git@1.3.1#egg=edx-proctoring==1.3.1
# This is here because all of the other XBlocks are located here. However, it is published to PyPI and will be installed that way
xblock-review==1.1.1
# Third Party XBlocks
...
...
scripts/xsslint_thresholds.json
View file @
d9bfab5b
...
...
@@ -8,16 +8,16 @@
"javascript-jquery-insert-into-target"
:
23
,
"javascript-jquery-insertion"
:
19
,
"javascript-jquery-prepend"
:
7
,
"mako-html-entities"
:
0
,
"mako-html-entities"
:
1
,
"mako-invalid-html-filter"
:
11
,
"mako-invalid-js-filter"
:
192
,
"mako-js-html-string"
:
0
,
"mako-js-missing-quotes"
:
0
,
"mako-missing-default"
:
1
81
,
"mako-missing-default"
:
1
62
,
"mako-multiple-page-tags"
:
0
,
"mako-unknown-context"
:
0
,
"mako-unparseable-expression"
:
0
,
"mako-unwanted-html-filter"
:
0
,
"mako-unwanted-html-filter"
:
2
,
"python-close-before-format"
:
0
,
"python-concat-html"
:
24
,
"python-custom-escape"
:
13
,
...
...
@@ -28,5 +28,5 @@
"python-wrap-html"
:
226
,
"underscore-not-escaped"
:
507
},
"total"
:
17
70
"total"
:
17
54
}
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