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
c12c5c92
Commit
c12c5c92
authored
Jun 10, 2015
by
Edward Zarecor
Browse files
Options
Browse Files
Download
Plain Diff
fixing post-release merge conflicts with DKH
parents
5faaca0d
1b3efaba
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
381 additions
and
44 deletions
+381
-44
cms/djangoapps/contentstore/tests/test_transcripts_utils.py
+101
-0
cms/djangoapps/contentstore/views/item.py
+8
-7
cms/djangoapps/contentstore/views/tests/test_item.py
+22
-0
common/djangoapps/enrollment/tests/test_views.py
+55
-2
common/djangoapps/enrollment/views.py
+17
-1
common/lib/xmodule/xmodule/js/src/video/01_initialize.js
+3
-6
common/lib/xmodule/xmodule/video_module/transcripts_utils.py
+32
-1
lms/djangoapps/branding/tests/test_page.py
+46
-0
lms/djangoapps/commerce/signals.py
+8
-0
lms/djangoapps/commerce/tests/test_signals.py
+7
-0
lms/djangoapps/courseware/tests/test_tabs.py
+1
-0
lms/envs/bok_choy.env.json
+2
-1
lms/envs/bok_choy.py
+3
-0
lms/static/sass/multicourse/_courses.scss
+58
-18
lms/static/sass/shared/_footer-edx.scss
+12
-6
lms/templates/courseware/courses.html
+6
-2
No files found.
cms/djangoapps/contentstore/tests/test_transcripts_utils.py
View file @
c12c5c92
...
@@ -270,6 +270,107 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
...
@@ -270,6 +270,107 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
self
.
clear_sub_content
(
good_youtube_sub
)
self
.
clear_sub_content
(
good_youtube_sub
)
@patch
(
'xmodule.video_module.transcripts_utils.requests.get'
)
def
test_get_transcript_name_youtube_server_success
(
self
,
mock_get
):
"""
Get transcript name from transcript_list fetch from youtube server api
depends on language code, default language in YOUTUBE Text Api is "en"
"""
youtube_text_api
=
copy
.
deepcopy
(
settings
.
YOUTUBE
[
'TEXT_API'
])
youtube_text_api
[
'params'
][
'v'
]
=
'dummy_video_id'
response_success
=
"""
<transcript_list>
<track id="1" name="Custom" lang_code="en" />
<track id="0" name="Custom1" lang_code="en-GB"/>
</transcript_list>
"""
mock_get
.
return_value
=
Mock
(
status_code
=
200
,
text
=
response_success
,
content
=
response_success
)
transcript_name
=
transcripts_utils
.
youtube_video_transcript_name
(
youtube_text_api
)
self
.
assertEqual
(
transcript_name
,
'Custom'
)
@patch
(
'xmodule.video_module.transcripts_utils.requests.get'
)
def
test_get_transcript_name_youtube_server_no_transcripts
(
self
,
mock_get
):
"""
When there are no transcripts of video transcript name will be None
"""
youtube_text_api
=
copy
.
deepcopy
(
settings
.
YOUTUBE
[
'TEXT_API'
])
youtube_text_api
[
'params'
][
'v'
]
=
'dummy_video_id'
response_success
=
"<transcript_list></transcript_list>"
mock_get
.
return_value
=
Mock
(
status_code
=
200
,
text
=
response_success
,
content
=
response_success
)
transcript_name
=
transcripts_utils
.
youtube_video_transcript_name
(
youtube_text_api
)
self
.
assertIsNone
(
transcript_name
)
@patch
(
'xmodule.video_module.transcripts_utils.requests.get'
)
def
test_get_transcript_name_youtube_server_language_not_exist
(
self
,
mock_get
):
"""
When the language does not exist in transcript_list transcript name will be None
"""
youtube_text_api
=
copy
.
deepcopy
(
settings
.
YOUTUBE
[
'TEXT_API'
])
youtube_text_api
[
'params'
][
'v'
]
=
'dummy_video_id'
youtube_text_api
[
'params'
][
'lang'
]
=
'abc'
response_success
=
"""
<transcript_list>
<track id="1" name="Custom" lang_code="en" />
<track id="0" name="Custom1" lang_code="en-GB"/>
</transcript_list>
"""
mock_get
.
return_value
=
Mock
(
status_code
=
200
,
text
=
response_success
,
content
=
response_success
)
transcript_name
=
transcripts_utils
.
youtube_video_transcript_name
(
youtube_text_api
)
self
.
assertIsNone
(
transcript_name
)
def
mocked_requests_get
(
*
args
,
**
kwargs
):
"""
This method will be used by the mock to replace requests.get
"""
# pylint: disable=no-method-argument
response_transcript_list
=
"""
<transcript_list>
<track id="1" name="Custom" lang_code="en" />
<track id="0" name="Custom1" lang_code="en-GB"/>
</transcript_list>
"""
response_transcript
=
textwrap
.
dedent
(
"""
<transcript>
<text start="0" dur="0.27"></text>
<text start="0.27" dur="2.45">Test text 1.</text>
<text start="2.72">Test text 2.</text>
<text start="5.43" dur="1.73">Test text 3.</text>
</transcript>
"""
)
if
kwargs
==
{
'params'
:
{
'lang'
:
'en'
,
'v'
:
'good_id_2'
}}:
return
Mock
(
status_code
=
200
,
text
=
''
)
elif
kwargs
==
{
'params'
:
{
'type'
:
'list'
,
'v'
:
'good_id_2'
}}:
return
Mock
(
status_code
=
200
,
text
=
response_transcript_list
,
content
=
response_transcript_list
)
elif
kwargs
==
{
'params'
:
{
'lang'
:
'en'
,
'v'
:
'good_id_2'
,
'name'
:
'Custom'
}}:
return
Mock
(
status_code
=
200
,
text
=
response_transcript
,
content
=
response_transcript
)
return
Mock
(
status_code
=
404
,
text
=
''
)
@patch
(
'xmodule.video_module.transcripts_utils.requests.get'
,
side_effect
=
mocked_requests_get
)
def
test_downloading_subs_using_transcript_name
(
self
,
mock_get
):
"""
Download transcript using transcript name in url
"""
good_youtube_sub
=
'good_id_2'
self
.
clear_sub_content
(
good_youtube_sub
)
transcripts_utils
.
download_youtube_subs
(
good_youtube_sub
,
self
.
course
,
settings
)
mock_get
.
assert_any_call
(
'http://video.google.com/timedtext'
,
params
=
{
'lang'
:
'en'
,
'v'
:
'good_id_2'
,
'name'
:
'Custom'
}
)
# Check asset status after import of transcript.
filename
=
'subs_{0}.srt.sjson'
.
format
(
good_youtube_sub
)
content_location
=
StaticContent
.
compute_location
(
self
.
course
.
id
,
filename
)
self
.
assertTrue
(
contentstore
()
.
find
(
content_location
))
self
.
clear_sub_content
(
good_youtube_sub
)
class
TestGenerateSubsFromSource
(
TestDownloadYoutubeSubs
):
class
TestGenerateSubsFromSource
(
TestDownloadYoutubeSubs
):
"""Tests for `generate_subs_from_source` function."""
"""Tests for `generate_subs_from_source` function."""
...
...
cms/djangoapps/contentstore/views/item.py
View file @
c12c5c92
...
@@ -332,13 +332,14 @@ def xblock_outline_handler(request, usage_key_string):
...
@@ -332,13 +332,14 @@ def xblock_outline_handler(request, usage_key_string):
response_format
=
request
.
REQUEST
.
get
(
'format'
,
'html'
)
response_format
=
request
.
REQUEST
.
get
(
'format'
,
'html'
)
if
response_format
==
'json'
or
'application/json'
in
request
.
META
.
get
(
'HTTP_ACCEPT'
,
'application/json'
):
if
response_format
==
'json'
or
'application/json'
in
request
.
META
.
get
(
'HTTP_ACCEPT'
,
'application/json'
):
store
=
modulestore
()
store
=
modulestore
()
root_xblock
=
store
.
get_item
(
usage_key
)
with
store
.
bulk_operations
(
usage_key
.
course_key
):
return
JsonResponse
(
create_xblock_info
(
root_xblock
=
store
.
get_item
(
usage_key
)
root_xblock
,
return
JsonResponse
(
create_xblock_info
(
include_child_info
=
True
,
root_xblock
,
course_outline
=
True
,
include_child_info
=
True
,
include_children_predicate
=
lambda
xblock
:
not
xblock
.
category
==
'vertical'
course_outline
=
True
,
))
include_children_predicate
=
lambda
xblock
:
not
xblock
.
category
==
'vertical'
))
else
:
else
:
return
Http404
return
Http404
...
...
cms/djangoapps/contentstore/views/tests/test_item.py
View file @
c12c5c92
...
@@ -1410,6 +1410,28 @@ class TestXBlockInfo(ItemTest):
...
@@ -1410,6 +1410,28 @@ class TestXBlockInfo(ItemTest):
json_response
=
json
.
loads
(
resp
.
content
)
json_response
=
json
.
loads
(
resp
.
content
)
self
.
validate_course_xblock_info
(
json_response
,
course_outline
=
True
)
self
.
validate_course_xblock_info
(
json_response
,
course_outline
=
True
)
def
test_xblock_outline_handler_mongo_calls
(
self
):
expected_calls
=
5
with
self
.
store
.
default_store
(
ModuleStoreEnum
.
Type
.
split
):
course
=
CourseFactory
.
create
()
chapter
=
ItemFactory
.
create
(
parent_location
=
course
.
location
,
category
=
'chapter'
,
display_name
=
'Week 1'
)
outline_url
=
reverse_usage_url
(
'xblock_outline_handler'
,
chapter
.
location
)
with
check_mongo_calls
(
expected_calls
):
self
.
client
.
get
(
outline_url
,
HTTP_ACCEPT
=
'application/json'
)
sequential
=
ItemFactory
.
create
(
parent_location
=
chapter
.
location
,
category
=
'sequential'
,
display_name
=
'Sequential 1'
)
ItemFactory
.
create
(
parent_location
=
sequential
.
location
,
category
=
'vertical'
,
display_name
=
'Vertical 1'
)
# calls should be same after adding two new children.
with
check_mongo_calls
(
expected_calls
):
self
.
client
.
get
(
outline_url
,
HTTP_ACCEPT
=
'application/json'
)
def
test_entrance_exam_chapter_xblock_info
(
self
):
def
test_entrance_exam_chapter_xblock_info
(
self
):
chapter
=
ItemFactory
.
create
(
chapter
=
ItemFactory
.
create
(
parent_location
=
self
.
course
.
location
,
category
=
'chapter'
,
display_name
=
"Entrance Exam"
,
parent_location
=
self
.
course
.
location
,
category
=
'chapter'
,
display_name
=
"Entrance Exam"
,
...
...
common/djangoapps/enrollment/tests/test_views.py
View file @
c12c5c92
...
@@ -2,6 +2,7 @@
...
@@ -2,6 +2,7 @@
Tests for user enrollment.
Tests for user enrollment.
"""
"""
import
json
import
json
import
itertools
import
unittest
import
unittest
import
datetime
import
datetime
...
@@ -91,11 +92,11 @@ class EnrollmentTestMixin(object):
...
@@ -91,11 +92,11 @@ class EnrollmentTestMixin(object):
return
response
return
response
def
assert_enrollment_activation
(
self
,
expected_activation
,
expected_mode
=
CourseMode
.
VERIFIED
):
def
assert_enrollment_activation
(
self
,
expected_activation
,
expected_mode
):
"""Change an enrollment's activation and verify its activation and mode are as expected."""
"""Change an enrollment's activation and verify its activation and mode are as expected."""
self
.
assert_enrollment_status
(
self
.
assert_enrollment_status
(
as_server
=
True
,
as_server
=
True
,
mode
=
Non
e
,
mode
=
expected_mod
e
,
is_active
=
expected_activation
,
is_active
=
expected_activation
,
expected_status
=
status
.
HTTP_200_OK
expected_status
=
status
.
HTTP_200_OK
)
)
...
@@ -637,6 +638,58 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
...
@@ -637,6 +638,58 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
self
.
assertTrue
(
is_active
)
self
.
assertTrue
(
is_active
)
self
.
assertEqual
(
course_mode
,
CourseMode
.
HONOR
)
self
.
assertEqual
(
course_mode
,
CourseMode
.
HONOR
)
@ddt.data
(
*
itertools
.
product
(
(
CourseMode
.
HONOR
,
CourseMode
.
VERIFIED
),
(
CourseMode
.
HONOR
,
CourseMode
.
VERIFIED
),
(
True
,
False
),
(
True
,
False
),
))
@ddt.unpack
def
test_change_mode_from_server
(
self
,
old_mode
,
new_mode
,
old_is_active
,
new_is_active
):
"""
Server-to-server calls should be allowed to change the mode of any
enrollment, as long as the enrollment is not being deactivated during
the same call (this is assumed to be an error on the client's side).
"""
for
mode
in
[
CourseMode
.
HONOR
,
CourseMode
.
VERIFIED
]:
CourseModeFactory
.
create
(
course_id
=
self
.
course
.
id
,
mode_slug
=
mode
,
mode_display_name
=
mode
,
)
# Set up the initial enrollment
self
.
assert_enrollment_status
(
as_server
=
True
,
mode
=
old_mode
,
is_active
=
old_is_active
)
course_mode
,
is_active
=
CourseEnrollment
.
enrollment_mode_for_user
(
self
.
user
,
self
.
course
.
id
)
self
.
assertEqual
(
is_active
,
old_is_active
)
self
.
assertEqual
(
course_mode
,
old_mode
)
expected_status
=
status
.
HTTP_400_BAD_REQUEST
if
(
old_mode
!=
new_mode
and
old_is_active
!=
new_is_active
and
not
new_is_active
)
else
status
.
HTTP_200_OK
# simulate the server-server api call under test
response
=
self
.
assert_enrollment_status
(
as_server
=
True
,
mode
=
new_mode
,
is_active
=
new_is_active
,
expected_status
=
expected_status
,
)
course_mode
,
is_active
=
CourseEnrollment
.
enrollment_mode_for_user
(
self
.
user
,
self
.
course
.
id
)
if
expected_status
==
status
.
HTTP_400_BAD_REQUEST
:
# nothing should have changed
self
.
assertEqual
(
is_active
,
old_is_active
)
self
.
assertEqual
(
course_mode
,
old_mode
)
# error message should contain specific text. Otto checks for this text in the message.
self
.
assertRegexpMatches
(
json
.
loads
(
response
.
content
)[
'message'
],
'Enrollment mode mismatch'
)
else
:
# call should have succeeded
self
.
assertEqual
(
is_active
,
new_is_active
)
self
.
assertEqual
(
course_mode
,
new_mode
)
def
test_change_mode_invalid_user
(
self
):
def
test_change_mode_invalid_user
(
self
):
"""
"""
Attempts to change an enrollment for a non-existent user should result in an HTTP 404 for non-server users,
Attempts to change an enrollment for a non-existent user should result in an HTTP 404 for non-server users,
...
...
common/djangoapps/enrollment/views.py
View file @
c12c5c92
...
@@ -3,6 +3,8 @@ The Enrollment API Views should be simple, lean HTTP endpoints for API access. T
...
@@ -3,6 +3,8 @@ The Enrollment API Views should be simple, lean HTTP endpoints for API access. T
consist primarily of authentication, request validation, and serialization.
consist primarily of authentication, request validation, and serialization.
"""
"""
import
logging
from
ipware.ip
import
get_ip
from
ipware.ip
import
get_ip
from
django.core.exceptions
import
ObjectDoesNotExist
from
django.core.exceptions
import
ObjectDoesNotExist
from
django.utils.decorators
import
method_decorator
from
django.utils.decorators
import
method_decorator
...
@@ -31,6 +33,9 @@ from enrollment.errors import (
...
@@ -31,6 +33,9 @@ from enrollment.errors import (
from
student.models
import
User
from
student.models
import
User
log
=
logging
.
getLogger
(
__name__
)
class
EnrollmentCrossDomainSessionAuth
(
SessionAuthenticationAllowInactiveUser
,
SessionAuthenticationCrossDomainCsrf
):
class
EnrollmentCrossDomainSessionAuth
(
SessionAuthenticationAllowInactiveUser
,
SessionAuthenticationCrossDomainCsrf
):
"""Session authentication that allows inactive users and cross-domain requests. """
"""Session authentication that allows inactive users and cross-domain requests. """
pass
pass
...
@@ -429,7 +434,18 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
...
@@ -429,7 +434,18 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
)
)
enrollment
=
api
.
get_enrollment
(
username
,
unicode
(
course_id
))
enrollment
=
api
.
get_enrollment
(
username
,
unicode
(
course_id
))
if
has_api_key_permissions
and
enrollment
and
enrollment
[
'mode'
]
!=
mode
:
mode_changed
=
enrollment
and
mode
is
not
None
and
enrollment
[
'mode'
]
!=
mode
active_changed
=
enrollment
and
is_active
is
not
None
and
enrollment
[
'is_active'
]
!=
is_active
if
has_api_key_permissions
and
(
mode_changed
or
active_changed
):
if
mode_changed
and
active_changed
and
not
is_active
:
# if the requester wanted to deactivate but specified the wrong mode, fail
# the request (on the assumption that the requester had outdated information
# about the currently active enrollment).
msg
=
u"Enrollment mode mismatch: active mode={}, requested mode={}. Won't deactivate."
.
format
(
enrollment
[
"mode"
],
mode
)
log
.
warning
(
msg
)
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
,
data
=
{
"message"
:
msg
})
response
=
api
.
update_enrollment
(
username
,
unicode
(
course_id
),
mode
=
mode
,
is_active
=
is_active
)
response
=
api
.
update_enrollment
(
username
,
unicode
(
course_id
),
mode
=
mode
,
is_active
=
is_active
)
else
:
else
:
# Will reactivate inactive enrollments.
# Will reactivate inactive enrollments.
...
...
common/lib/xmodule/xmodule/js/src/video/01_initialize.js
View file @
c12c5c92
...
@@ -522,12 +522,9 @@ function (VideoPlayer, i18n) {
...
@@ -522,12 +522,9 @@ function (VideoPlayer, i18n) {
this
.
youtubeXhr
this
.
youtubeXhr
.
always
(
function
(
json
,
status
)
{
.
always
(
function
(
json
,
status
)
{
var
err
=
$
.
isPlainObject
(
json
.
error
)
||
// It will work for both if statusCode is 200 or 410.
(
var
didSucceed
=
(
json
.
error
&&
json
.
error
.
code
===
410
)
||
status
===
'success'
||
status
===
'notmodified'
;
status
!==
'success'
&&
if
(
!
didSucceed
)
{
status
!==
'notmodified'
);
if
(
err
)
{
console
.
log
(
console
.
log
(
'[Video info]: YouTube returned an error for '
+
'[Video info]: YouTube returned an error for '
+
'video with id "'
+
id
+
'".'
'video with id "'
+
id
+
'".'
...
...
common/lib/xmodule/xmodule/video_module/transcripts_utils.py
View file @
c12c5c92
...
@@ -94,7 +94,32 @@ def save_subs_to_store(subs, subs_id, item, language='en'):
...
@@ -94,7 +94,32 @@ def save_subs_to_store(subs, subs_id, item, language='en'):
return
save_to_store
(
filedata
,
filename
,
'application/json'
,
item
.
location
)
return
save_to_store
(
filedata
,
filename
,
'application/json'
,
item
.
location
)
def
get_transcripts_from_youtube
(
youtube_id
,
settings
,
i18n
):
def
youtube_video_transcript_name
(
youtube_text_api
):
"""
Get the transcript name from available transcripts of video
with respect to language from youtube server
"""
# pylint: disable=no-member
utf8_parser
=
etree
.
XMLParser
(
encoding
=
'utf-8'
)
transcripts_param
=
{
'type'
:
'list'
,
'v'
:
youtube_text_api
[
'params'
][
'v'
]}
lang
=
youtube_text_api
[
'params'
][
'lang'
]
# get list of transcripts of specific video
# url-form
# http://video.google.com/timedtext?type=list&v={VideoId}
youtube_response
=
requests
.
get
(
'http://'
+
youtube_text_api
[
'url'
],
params
=
transcripts_param
)
if
youtube_response
.
status_code
==
200
and
youtube_response
.
text
:
# pylint: disable=no-member
youtube_data
=
etree
.
fromstring
(
youtube_response
.
content
,
parser
=
utf8_parser
)
# iterate all transcripts information from youtube server
for
element
in
youtube_data
:
# search specific language code such as 'en' in transcripts info list
if
element
.
tag
==
'track'
and
element
.
get
(
'lang_code'
,
''
)
==
lang
:
return
element
.
get
(
'name'
)
return
None
def
get_transcripts_from_youtube
(
youtube_id
,
settings
,
i18n
,
youtube_transcript_name
=
''
):
"""
"""
Gets transcripts from youtube for youtube_id.
Gets transcripts from youtube for youtube_id.
...
@@ -109,6 +134,12 @@ def get_transcripts_from_youtube(youtube_id, settings, i18n):
...
@@ -109,6 +134,12 @@ def get_transcripts_from_youtube(youtube_id, settings, i18n):
youtube_text_api
=
copy
.
deepcopy
(
settings
.
YOUTUBE
[
'TEXT_API'
])
youtube_text_api
=
copy
.
deepcopy
(
settings
.
YOUTUBE
[
'TEXT_API'
])
youtube_text_api
[
'params'
][
'v'
]
=
youtube_id
youtube_text_api
[
'params'
][
'v'
]
=
youtube_id
# if the transcript name is not empty on youtube server we have to pass
# name param in url in order to get transcript
# example http://video.google.com/timedtext?lang=en&v={VideoId}&name={transcript_name}
youtube_transcript_name
=
youtube_video_transcript_name
(
youtube_text_api
)
if
youtube_transcript_name
:
youtube_text_api
[
'params'
][
'name'
]
=
youtube_transcript_name
data
=
requests
.
get
(
'http://'
+
youtube_text_api
[
'url'
],
params
=
youtube_text_api
[
'params'
])
data
=
requests
.
get
(
'http://'
+
youtube_text_api
[
'url'
],
params
=
youtube_text_api
[
'params'
])
if
data
.
status_code
!=
200
or
not
data
.
text
:
if
data
.
status_code
!=
200
or
not
data
.
text
:
...
...
lms/djangoapps/branding/tests/test_page.py
View file @
c12c5c92
...
@@ -201,6 +201,52 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase):
...
@@ -201,6 +201,52 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase):
@patch
(
'student.views.render_to_response'
,
RENDER_MOCK
)
@patch
(
'student.views.render_to_response'
,
RENDER_MOCK
)
@patch
(
'courseware.views.render_to_response'
,
RENDER_MOCK
)
@patch
(
'courseware.views.render_to_response'
,
RENDER_MOCK
)
@patch.dict
(
'django.conf.settings.FEATURES'
,
{
'ENABLE_COURSE_DISCOVERY'
:
False
})
@patch.dict
(
'django.conf.settings.FEATURES'
,
{
'ENABLE_COURSE_DISCOVERY'
:
False
})
def
test_course_discovery_off
(
self
):
"""
Asserts that the Course Discovery UI elements follow the
feature flag settings
"""
response
=
self
.
client
.
get
(
'/'
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# assert that the course discovery UI is not present
self
.
assertNotIn
(
'Search for a course'
,
response
.
content
)
# check the /courses view
response
=
self
.
client
.
get
(
reverse
(
'branding.views.courses'
))
self
.
assertEqual
(
response
.
status_code
,
200
)
# assert that the course discovery UI is not present
self
.
assertNotIn
(
'Search for a course'
,
response
.
content
)
self
.
assertNotIn
(
'<aside aria-label="Refine your search" class="search-facets phone-menu">'
,
response
.
content
)
# make sure we have the special css class on the section
self
.
assertIn
(
'<section class="courses no-course-discovery">'
,
response
.
content
)
@patch
(
'student.views.render_to_response'
,
RENDER_MOCK
)
@patch
(
'courseware.views.render_to_response'
,
RENDER_MOCK
)
@patch.dict
(
'django.conf.settings.FEATURES'
,
{
'ENABLE_COURSE_DISCOVERY'
:
True
})
def
test_course_discovery_on
(
self
):
"""
Asserts that the Course Discovery UI elements follow the
feature flag settings
"""
response
=
self
.
client
.
get
(
'/'
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# assert that the course discovery UI is not present
self
.
assertIn
(
'Search for a course'
,
response
.
content
)
# check the /courses view
response
=
self
.
client
.
get
(
reverse
(
'branding.views.courses'
))
self
.
assertEqual
(
response
.
status_code
,
200
)
# assert that the course discovery UI is not present
self
.
assertIn
(
'Search for a course'
,
response
.
content
)
self
.
assertIn
(
'<aside aria-label="Refine your search" class="search-facets phone-menu">'
,
response
.
content
)
self
.
assertIn
(
'<section class="courses">'
,
response
.
content
)
@patch
(
'student.views.render_to_response'
,
RENDER_MOCK
)
@patch
(
'courseware.views.render_to_response'
,
RENDER_MOCK
)
@patch.dict
(
'django.conf.settings.FEATURES'
,
{
'ENABLE_COURSE_DISCOVERY'
:
False
})
def
test_course_cards_sorted_by_default_sorting
(
self
):
def
test_course_cards_sorted_by_default_sorting
(
self
):
response
=
self
.
client
.
get
(
'/'
)
response
=
self
.
client
.
get
(
'/'
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
...
...
lms/djangoapps/commerce/signals.py
View file @
c12c5c92
...
@@ -5,6 +5,7 @@ import logging
...
@@ -5,6 +5,7 @@ import logging
from
urlparse
import
urljoin
from
urlparse
import
urljoin
from
django.conf
import
settings
from
django.conf
import
settings
from
django.contrib.auth.models
import
AnonymousUser
from
django.core.mail
import
EmailMultiAlternatives
from
django.core.mail
import
EmailMultiAlternatives
from
django.dispatch
import
receiver
from
django.dispatch
import
receiver
from
django.utils.translation
import
ugettext
as
_
from
django.utils.translation
import
ugettext
as
_
...
@@ -32,6 +33,13 @@ def handle_unenroll_done(sender, course_enrollment=None, skip_refund=False, **kw
...
@@ -32,6 +33,13 @@ def handle_unenroll_done(sender, course_enrollment=None, skip_refund=False, **kw
if
course_enrollment
and
course_enrollment
.
refundable
():
if
course_enrollment
and
course_enrollment
.
refundable
():
try
:
try
:
request_user
=
get_request_user
()
or
course_enrollment
.
user
request_user
=
get_request_user
()
or
course_enrollment
.
user
if
isinstance
(
request_user
,
AnonymousUser
):
# Assume the request was initiated via server-to-server
# api call (presumably Otto). In this case we cannot
# construct a client to call Otto back anyway, because
# the client does not work anonymously, and furthermore,
# there's certainly no need to inform Otto about this request.
return
refund_seat
(
course_enrollment
,
request_user
)
refund_seat
(
course_enrollment
,
request_user
)
except
:
# pylint: disable=bare-except
except
:
# pylint: disable=bare-except
# don't assume the signal was fired with `send_robust`.
# don't assume the signal was fired with `send_robust`.
...
...
lms/djangoapps/commerce/tests/test_signals.py
View file @
c12c5c92
"""
"""
Tests for signal handling in commerce djangoapp.
Tests for signal handling in commerce djangoapp.
"""
"""
from
django.contrib.auth.models
import
AnonymousUser
from
django.test
import
TestCase
from
django.test
import
TestCase
from
django.test.utils
import
override_settings
from
django.test.utils
import
override_settings
...
@@ -109,6 +110,12 @@ class TestRefundSignal(TestCase):
...
@@ -109,6 +110,12 @@ class TestRefundSignal(TestCase):
self
.
assertTrue
(
mock_refund_seat
.
called
)
self
.
assertTrue
(
mock_refund_seat
.
called
)
self
.
assertEqual
(
mock_refund_seat
.
call_args
[
0
],
(
self
.
course_enrollment
,
self
.
requester
))
self
.
assertEqual
(
mock_refund_seat
.
call_args
[
0
],
(
self
.
course_enrollment
,
self
.
requester
))
# HTTP user is another server (AnonymousUser): do not try to initiate a refund at all.
mock_get_request_user
.
return_value
=
AnonymousUser
()
mock_refund_seat
.
reset_mock
()
self
.
send_signal
()
self
.
assertFalse
(
mock_refund_seat
.
called
)
@mock.patch
(
'commerce.signals.log.warning'
)
@mock.patch
(
'commerce.signals.log.warning'
)
def
test_not_authorized_warning
(
self
,
mock_log_warning
):
def
test_not_authorized_warning
(
self
,
mock_log_warning
):
"""
"""
...
...
lms/djangoapps/courseware/tests/test_tabs.py
View file @
c12c5c92
...
@@ -511,6 +511,7 @@ class TabListTestCase(TabTestCase):
...
@@ -511,6 +511,7 @@ class TabListTestCase(TabTestCase):
{
'type'
:
CourseInfoTab
.
type
,
'name'
:
'fake_name'
},
{
'type'
:
CourseInfoTab
.
type
,
'name'
:
'fake_name'
},
{
'type'
:
'discussion'
,
'name'
:
'fake_name'
},
{
'type'
:
'discussion'
,
'name'
:
'fake_name'
},
{
'type'
:
ExternalLinkCourseTab
.
type
,
'name'
:
'fake_name'
,
'link'
:
'fake_link'
},
{
'type'
:
ExternalLinkCourseTab
.
type
,
'name'
:
'fake_name'
,
'link'
:
'fake_link'
},
{
'type'
:
ExternalLinkCourseTab
.
type
,
'name'
:
'fake_name'
,
'link'
:
'fake_link'
},
{
'type'
:
'textbooks'
},
{
'type'
:
'textbooks'
},
{
'type'
:
'pdf_textbooks'
},
{
'type'
:
'pdf_textbooks'
},
{
'type'
:
'html_textbooks'
},
{
'type'
:
'html_textbooks'
},
...
...
lms/envs/bok_choy.env.json
View file @
c12c5c92
...
@@ -86,7 +86,8 @@
...
@@ -86,7 +86,8 @@
"ALLOW_AUTOMATED_SIGNUPS"
:
true
,
"ALLOW_AUTOMATED_SIGNUPS"
:
true
,
"AUTOMATIC_AUTH_FOR_TESTING"
:
true
,
"AUTOMATIC_AUTH_FOR_TESTING"
:
true
,
"MODE_CREATION_FOR_TESTING"
:
true
,
"MODE_CREATION_FOR_TESTING"
:
true
,
"AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING"
:
true
"AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING"
:
true
,
"ENABLE_COURSE_DISCOVERY"
:
true
},
},
"FEEDBACK_SUBMISSION_EMAIL"
:
""
,
"FEEDBACK_SUBMISSION_EMAIL"
:
""
,
"GITHUB_REPO_ROOT"
:
"** OVERRIDDEN **"
,
"GITHUB_REPO_ROOT"
:
"** OVERRIDDEN **"
,
...
...
lms/envs/bok_choy.py
View file @
c12c5c92
...
@@ -94,6 +94,9 @@ FEATURES['MILESTONES_APP'] = True
...
@@ -94,6 +94,9 @@ FEATURES['MILESTONES_APP'] = True
# Enable pre-requisite course
# Enable pre-requisite course
FEATURES
[
'ENABLE_PREREQUISITE_COURSES'
]
=
True
FEATURES
[
'ENABLE_PREREQUISITE_COURSES'
]
=
True
# Enable Course Discovery
FEATURES
[
'ENABLE_COURSE_DISCOVERY'
]
=
True
# Enable student notes
# Enable student notes
FEATURES
[
'ENABLE_EDXNOTES'
]
=
True
FEATURES
[
'ENABLE_EDXNOTES'
]
=
True
...
...
lms/static/sass/multicourse/_courses.scss
View file @
c12c5c92
...
@@ -41,38 +41,78 @@ $facet-background-color: #007db8;
...
@@ -41,38 +41,78 @@ $facet-background-color: #007db8;
.courses
{
.courses
{
@include
rtl
()
{
$layout-direction
:
"RTL"
;
}
@include
rtl
()
{
$layout-direction
:
"RTL"
;
}
@include
span-columns
(
9
);
@include
media
(
$bp-medium
)
{
.courses-listing
.courses-listing-item
{
@include
span-columns
(
4
);
@include
fill-parent
();
}
margin
:
(
$baseline
*
0
.75
)
0
(
$baseline
*
1
.5
)
0
;
max-height
:
$course-card-height
;
@include
media
(
$bp-large
)
{
@include
span-columns
(
8
);
}
}
@include
media
(
$bp-huge
)
{
/* Style grid settings if course discovery turned on */
&
:not
(
.no-course-discovery
)
{
@include
span-columns
(
9
);
@include
span-columns
(
9
);
@include
media
(
$bp-medium
)
{
@include
span-columns
(
4
);
}
@include
media
(
$bp-large
)
{
@include
span-columns
(
8
);
}
@include
media
(
$bp-huge
)
{
@include
span-columns
(
9
);
}
.courses-listing
.courses-listing-item
{
@include
media
(
$bp-medium
)
{
@include
span-columns
(
8
);
// 4 of 8
@include
omega
(
1n
);
}
@include
media
(
$bp-large
)
{
@include
span-columns
(
6
);
// 6 of 12
@include
omega
(
2n
);
}
@include
media
(
$bp-huge
)
{
@include
span-columns
(
4
);
// 4 of 12
@include
omega
(
3n
);
}
}
}
}
.courses-listing
.courses-listing-item
{
/* Style grid settings if course discovery turned off */
@include
fill-parent
();
&
.no-course-discovery
{
margin
:
(
$baseline
*
0
.75
)
0
(
$baseline
*
1
.5
)
0
;
@include
span-columns
(
12
);
max-height
:
$course-card-height
;
@include
media
(
$bp-medium
)
{
@include
media
(
$bp-medium
)
{
@include
span-columns
(
8
);
// 4 of 8
@include
span-columns
(
8
);
@include
omega
(
1n
);
}
}
@include
media
(
$bp-large
)
{
@include
media
(
$bp-large
)
{
@include
span-columns
(
6
);
// 6 of 12
@include
span-columns
(
12
);
@include
omega
(
2n
);
}
}
@include
media
(
$bp-huge
)
{
@include
media
(
$bp-huge
)
{
@include
span-columns
(
4
);
// 4 of 12
@include
span-columns
(
12
);
@include
omega
(
3n
);
}
.courses-listing
.courses-listing-item
{
@include
media
(
$bp-medium
)
{
@include
span-columns
(
4
);
// 4 of 8
@include
omega
(
2n
);
}
@include
media
(
$bp-large
)
{
@include
span-columns
(
4
);
// 4 of 12
@include
omega
(
3n
);
}
@include
media
(
$bp-huge
)
{
@include
span-columns
(
3
);
// 3 of 12
@include
omega
(
4n
);
}
}
}
}
}
}
}
...
...
lms/static/sass/shared/_footer-edx.scss
View file @
c12c5c92
...
@@ -80,12 +80,15 @@ footer#footer-edx-v3 {
...
@@ -80,12 +80,15 @@ footer#footer-edx-v3 {
}
}
}
}
.social-media-links
,
.mobile-app-links
{
.mobile-app-links
{
@include
clearfix
();
@include
clearfix
();
position
:
relative
;
width
:
260px
;
height
:
42px
;
}
}
.social-media-links
{
.social-media-links
{
@include
clearfix
();
margin-bottom
:
30px
;
margin-bottom
:
30px
;
}
}
...
@@ -119,17 +122,20 @@ footer#footer-edx-v3 {
...
@@ -119,17 +122,20 @@ footer#footer-edx-v3 {
}
}
.app-link
{
.app-link
{
@include
float
(
left
);
position
:
absolute
;
@include
margin-right
(
10px
);
top
:
0
;
position
:
relative
;
display
:
inline-block
;
&
:first-of-type
{
@include
left
(
0
);
}
&
:last-of-type
{
&
:last-of-type
{
@include
margin-
right
(
0
);
@include
right
(
0
);
}
}
img
{
img
{
height
:
40px
;
height
:
40px
;
max-width
:
200px
;
}
}
}
}
...
...
lms/templates/courseware/courses.html
View file @
c12c5c92
...
@@ -29,7 +29,7 @@
...
@@ -29,7 +29,7 @@
<
%
block
name=
"pagetitle"
>
${_("Courses")}
</
%
block>
<
%
block
name=
"pagetitle"
>
${_("Courses")}
</
%
block>
<
%
<
%
platform_name =
microsite.get_value('platform_name',
settings
.
PLATFORM_NAME
)
platform_name =
microsite.get_value('platform_name',
settings
.
PLATFORM_NAME
)
course_discovery_enabled =
settings.FEATURES.get('ENABLE_COURSE_DISCOVERY')
if
self
.
stanford_theme_enabled
()
:
if
self
.
stanford_theme_enabled
()
:
course_index_overlay_text =
_("Explore
free
courses
from
{
university_name
}.").
format
(
university_name=
"Stanford University"
)
course_index_overlay_text =
_("Explore
free
courses
from
{
university_name
}.").
format
(
university_name=
"Stanford University"
)
logo_file =
static.url('themes/stanford/images/seal.png')
logo_file =
static.url('themes/stanford/images/seal.png')
...
@@ -66,6 +66,7 @@
...
@@ -66,6 +66,7 @@
<section
class=
"courses-container"
>
<section
class=
"courses-container"
>
% if course_discovery_enabled:
<div
id=
"discovery-form"
role=
"search"
aria-label=
"course"
>
<div
id=
"discovery-form"
role=
"search"
aria-label=
"course"
>
<form>
<form>
<input
class=
"discovery-input"
placeholder=
"${_('Search for a course')}"
type=
"text"
/>
<!-- removes spacing
<input
class=
"discovery-input"
placeholder=
"${_('Search for a course')}"
type=
"text"
/>
<!-- removes spacing
...
@@ -83,8 +84,9 @@
...
@@ -83,8 +84,9 @@
<div
id=
"filter-bar"
class=
"filters hide-phone"
>
<div
id=
"filter-bar"
class=
"filters hide-phone"
>
</div>
</div>
% endif
<section
class=
"courses"
>
<section
class=
"courses
${'' if course_discovery_enabled else ' no-course-discovery'}
"
>
<ul
class=
"courses-listing"
>
<ul
class=
"courses-listing"
>
%for course in courses:
%for course in courses:
<li
class=
"courses-listing-item"
>
<li
class=
"courses-listing-item"
>
...
@@ -95,8 +97,10 @@
...
@@ -95,8 +97,10 @@
</section>
</section>
% if course_discovery_enabled:
<aside
aria-label=
"${_('Refine your search')}"
class=
"search-facets phone-menu"
>
<aside
aria-label=
"${_('Refine your search')}"
class=
"search-facets phone-menu"
>
</aside>
</aside>
% endif
</section>
</section>
</section>
</section>
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