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
f5223a36
Commit
f5223a36
authored
May 03, 2016
by
Douglas Hall
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' into malikshahzad228/additional_course_fields
parents
d64e4b06
6d315a9a
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
431 additions
and
508 deletions
+431
-508
cms/djangoapps/contentstore/tests/test_libraries.py
+68
-2
cms/djangoapps/contentstore/views/component.py
+2
-1
cms/djangoapps/contentstore/views/item.py
+50
-0
cms/djangoapps/contentstore/views/preview.py
+0
-26
common/lib/xmodule/xmodule/library_content_module.py
+4
-6
common/lib/xmodule/xmodule/x_module.py
+3
-2
common/static/js/vendor/jasmine-jquery.js
+0
-366
lms/djangoapps/bulk_email/models.py
+6
-1
lms/djangoapps/bulk_email/tests/test_models.py
+29
-0
lms/djangoapps/instructor/views/instructor_dashboard.py
+8
-5
lms/static/sass/_build-lms-v2.scss
+2
-0
lms/static/sass/base-v2/_extends.scss
+6
-0
lms/static/sass/multicourse/_dashboard.scss
+36
-6
lms/static/sass/shared-v2/_footer.scss
+196
-0
lms/static/sass/shared-v2/_header.scss
+2
-2
lms/static/sass/shared/_footer-edx.scss
+4
-0
lms/static/sass/shared/_footer.scss
+0
-85
lms/templates/footer.html
+1
-1
lms/templates/navigation.html
+1
-1
themes/edx.org/lms/templates/dashboard.html
+13
-4
No files found.
cms/djangoapps/contentstore/tests/test_libraries.py
View file @
f5223a36
...
@@ -23,6 +23,8 @@ from mock import Mock
...
@@ -23,6 +23,8 @@ from mock import Mock
from
opaque_keys.edx.locator
import
CourseKey
,
LibraryLocator
from
opaque_keys.edx.locator
import
CourseKey
,
LibraryLocator
from
openedx.core.djangoapps.content.course_structures.tests
import
SignalDisconnectTestMixin
from
openedx.core.djangoapps.content.course_structures.tests
import
SignalDisconnectTestMixin
from
xblock_django.user_service
import
DjangoXBlockUserService
from
xblock_django.user_service
import
DjangoXBlockUserService
from
xmodule.x_module
import
STUDIO_VIEW
from
student
import
auth
class
LibraryTestCase
(
ModuleStoreTestCase
):
class
LibraryTestCase
(
ModuleStoreTestCase
):
...
@@ -30,16 +32,22 @@ class LibraryTestCase(ModuleStoreTestCase):
...
@@ -30,16 +32,22 @@ class LibraryTestCase(ModuleStoreTestCase):
Common functionality for content libraries tests
Common functionality for content libraries tests
"""
"""
def
setUp
(
self
):
def
setUp
(
self
):
user_password
=
super
(
LibraryTestCase
,
self
)
.
setUp
()
self
.
user_password
=
super
(
LibraryTestCase
,
self
)
.
setUp
()
self
.
client
=
AjaxEnabledTestClient
()
self
.
client
=
AjaxEnabledTestClient
()
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
user_password
)
self
.
_login_as_staff_user
(
logout_first
=
False
)
self
.
lib_key
=
self
.
_create_library
()
self
.
lib_key
=
self
.
_create_library
()
self
.
library
=
modulestore
()
.
get_library
(
self
.
lib_key
)
self
.
library
=
modulestore
()
.
get_library
(
self
.
lib_key
)
self
.
session_data
=
{}
# Used by _bind_module
self
.
session_data
=
{}
# Used by _bind_module
def
_login_as_staff_user
(
self
,
logout_first
=
True
):
""" Login as a staff user """
if
logout_first
:
self
.
client
.
logout
()
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
user_password
)
def
_create_library
(
self
,
org
=
"org"
,
library
=
"lib"
,
display_name
=
"Test Library"
):
def
_create_library
(
self
,
org
=
"org"
,
library
=
"lib"
,
display_name
=
"Test Library"
):
"""
"""
Helper method used to create a library. Uses the REST API.
Helper method used to create a library. Uses the REST API.
...
@@ -729,6 +737,64 @@ class TestLibraryAccess(SignalDisconnectTestMixin, LibraryTestCase):
...
@@ -729,6 +737,64 @@ class TestLibraryAccess(SignalDisconnectTestMixin, LibraryTestCase):
lc_block
=
self
.
_refresh_children
(
lc_block
,
status_code_expected
=
200
if
expected_result
else
403
)
lc_block
=
self
.
_refresh_children
(
lc_block
,
status_code_expected
=
200
if
expected_result
else
403
)
self
.
assertEqual
(
len
(
lc_block
.
children
),
1
if
expected_result
else
0
)
self
.
assertEqual
(
len
(
lc_block
.
children
),
1
if
expected_result
else
0
)
def
test_studio_user_permissions
(
self
):
"""
Test that user could attach to the problem only libraries that he has access (or which were created by him).
This test was created on the basis of bug described in the pull requests on github:
https://github.com/edx/edx-platform/pull/11331
https://github.com/edx/edx-platform/pull/11611
"""
self
.
_create_library
(
org
=
'admin_org_1'
,
library
=
'lib_adm_1'
,
display_name
=
'admin_lib_1'
)
self
.
_create_library
(
org
=
'admin_org_2'
,
library
=
'lib_adm_2'
,
display_name
=
'admin_lib_2'
)
self
.
_login_as_non_staff_user
()
self
.
_create_library
(
org
=
'staff_org_1'
,
library
=
'lib_staff_1'
,
display_name
=
'staff_lib_1'
)
self
.
_create_library
(
org
=
'staff_org_2'
,
library
=
'lib_staff_2'
,
display_name
=
'staff_lib_2'
)
with
modulestore
()
.
default_store
(
ModuleStoreEnum
.
Type
.
split
):
course
=
CourseFactory
.
create
()
instructor_role
=
CourseInstructorRole
(
course
.
id
)
auth
.
add_users
(
self
.
user
,
instructor_role
,
self
.
non_staff_user
)
lib_block
=
ItemFactory
.
create
(
category
=
'library_content'
,
parent_location
=
course
.
location
,
user_id
=
self
.
non_staff_user
.
id
,
publish_item
=
False
)
def
_get_settings_html
():
"""
Helper function to get block settings HTML
Used to check the available libraries.
"""
edit_view_url
=
reverse_usage_url
(
"xblock_view_handler"
,
lib_block
.
location
,
{
"view_name"
:
STUDIO_VIEW
})
resp
=
self
.
client
.
get_json
(
edit_view_url
)
self
.
assertEquals
(
resp
.
status_code
,
200
)
return
parse_json
(
resp
)[
'html'
]
self
.
_login_as_staff_user
()
staff_settings_html
=
_get_settings_html
()
self
.
assertIn
(
'staff_lib_1'
,
staff_settings_html
)
self
.
assertIn
(
'staff_lib_2'
,
staff_settings_html
)
self
.
assertIn
(
'admin_lib_1'
,
staff_settings_html
)
self
.
assertIn
(
'admin_lib_2'
,
staff_settings_html
)
self
.
_login_as_non_staff_user
()
response
=
self
.
client
.
get_json
(
LIBRARY_REST_URL
)
staff_libs
=
parse_json
(
response
)
self
.
assertEquals
(
2
,
len
(
staff_libs
))
non_staff_settings_html
=
_get_settings_html
()
self
.
assertIn
(
'staff_lib_1'
,
non_staff_settings_html
)
self
.
assertIn
(
'staff_lib_2'
,
non_staff_settings_html
)
self
.
assertNotIn
(
'admin_lib_1'
,
non_staff_settings_html
)
self
.
assertNotIn
(
'admin_lib_2'
,
non_staff_settings_html
)
@ddt.ddt
@ddt.ddt
@override_settings
(
SEARCH_ENGINE
=
None
)
@override_settings
(
SEARCH_ENGINE
=
None
)
...
...
cms/djangoapps/contentstore/views/component.py
View file @
f5223a36
...
@@ -21,7 +21,7 @@ from xblock.runtime import Mixologist
...
@@ -21,7 +21,7 @@ from xblock.runtime import Mixologist
from
contentstore.utils
import
get_lms_link_for_item
from
contentstore.utils
import
get_lms_link_for_item
from
contentstore.views.helpers
import
get_parent_xblock
,
is_unit
,
xblock_type_display_name
from
contentstore.views.helpers
import
get_parent_xblock
,
is_unit
,
xblock_type_display_name
from
contentstore.views.item
import
create_xblock_info
,
add_container_page_publishing_info
from
contentstore.views.item
import
create_xblock_info
,
add_container_page_publishing_info
,
StudioEditModuleRuntime
from
opaque_keys.edx.keys
import
UsageKey
from
opaque_keys.edx.keys
import
UsageKey
...
@@ -330,6 +330,7 @@ def component_handler(request, usage_key_string, handler, suffix=''):
...
@@ -330,6 +330,7 @@ def component_handler(request, usage_key_string, handler, suffix=''):
usage_key
=
UsageKey
.
from_string
(
usage_key_string
)
usage_key
=
UsageKey
.
from_string
(
usage_key_string
)
descriptor
=
modulestore
()
.
get_item
(
usage_key
)
descriptor
=
modulestore
()
.
get_item
(
usage_key
)
descriptor
.
xmodule_runtime
=
StudioEditModuleRuntime
(
request
.
user
)
# Let the module handle the AJAX
# Let the module handle the AJAX
req
=
django_to_webob_request
(
request
)
req
=
django_to_webob_request
(
request
)
...
...
cms/djangoapps/contentstore/views/item.py
View file @
f5223a36
...
@@ -21,6 +21,7 @@ from opaque_keys.edx.locator import LibraryUsageLocator
...
@@ -21,6 +21,7 @@ from opaque_keys.edx.locator import LibraryUsageLocator
from
pytz
import
UTC
from
pytz
import
UTC
from
xblock.fields
import
Scope
from
xblock.fields
import
Scope
from
xblock.fragment
import
Fragment
from
xblock.fragment
import
Fragment
from
xblock_django.user_service
import
DjangoXBlockUserService
from
cms.lib.xblock.authoring_mixin
import
VISIBILITY_VIEW
from
cms.lib.xblock.authoring_mixin
import
VISIBILITY_VIEW
from
contentstore.utils
import
(
from
contentstore.utils
import
(
...
@@ -51,6 +52,7 @@ from xmodule.modulestore.inheritance import own_metadata
...
@@ -51,6 +52,7 @@ from xmodule.modulestore.inheritance import own_metadata
from
xmodule.tabs
import
CourseTabList
from
xmodule.tabs
import
CourseTabList
from
xmodule.x_module
import
PREVIEW_VIEWS
,
STUDIO_VIEW
,
STUDENT_VIEW
,
DEPRECATION_VSCOMPAT_EVENT
from
xmodule.x_module
import
PREVIEW_VIEWS
,
STUDIO_VIEW
,
STUDENT_VIEW
,
DEPRECATION_VSCOMPAT_EVENT
__all__
=
[
__all__
=
[
'orphan_handler'
,
'xblock_handler'
,
'xblock_view_handler'
,
'xblock_outline_handler'
,
'xblock_container_handler'
'orphan_handler'
,
'xblock_handler'
,
'xblock_view_handler'
,
'xblock_outline_handler'
,
'xblock_container_handler'
]
]
...
@@ -198,6 +200,49 @@ def xblock_handler(request, usage_key_string):
...
@@ -198,6 +200,49 @@ def xblock_handler(request, usage_key_string):
)
)
class
StudioPermissionsService
(
object
):
"""
Service that can provide information about a user's permissions.
Deprecated. To be replaced by a more general authorization service.
Only used by LibraryContentDescriptor (and library_tools.py).
"""
def
__init__
(
self
,
user
):
self
.
_user
=
user
def
can_read
(
self
,
course_key
):
""" Does the user have read access to the given course/library? """
return
has_studio_read_access
(
self
.
_user
,
course_key
)
def
can_write
(
self
,
course_key
):
""" Does the user have read access to the given course/library? """
return
has_studio_write_access
(
self
.
_user
,
course_key
)
class
StudioEditModuleRuntime
(
object
):
"""
An extremely minimal ModuleSystem shim used for XBlock edits and studio_view.
(i.e. whenever we're not using PreviewModuleSystem.) This is required to make information
about the current user (especially permissions) available via services as needed.
"""
def
__init__
(
self
,
user
):
self
.
_user
=
user
def
service
(
self
,
block
,
service_name
):
"""
This block is not bound to a user but some blocks (LibraryContentModule) may need
user-specific services to check for permissions, etc.
If we return None here, CombinedSystem will load services from the descriptor runtime.
"""
if
block
.
service_declaration
(
service_name
)
is
not
None
:
if
service_name
==
"user"
:
return
DjangoXBlockUserService
(
self
.
_user
)
if
service_name
==
"studio_user_permissions"
:
return
StudioPermissionsService
(
self
.
_user
)
return
None
@require_http_methods
((
"GET"
))
@require_http_methods
((
"GET"
))
@login_required
@login_required
@expect_json
@expect_json
...
@@ -231,6 +276,9 @@ def xblock_view_handler(request, usage_key_string, view_name):
...
@@ -231,6 +276,9 @@ def xblock_view_handler(request, usage_key_string, view_name):
))
))
if
view_name
in
(
STUDIO_VIEW
,
VISIBILITY_VIEW
):
if
view_name
in
(
STUDIO_VIEW
,
VISIBILITY_VIEW
):
if
view_name
==
STUDIO_VIEW
and
xblock
.
xmodule_runtime
is
None
:
xblock
.
xmodule_runtime
=
StudioEditModuleRuntime
(
request
.
user
)
try
:
try
:
fragment
=
xblock
.
render
(
view_name
)
fragment
=
xblock
.
render
(
view_name
)
# catch exceptions indiscriminately, since after this point they escape the
# catch exceptions indiscriminately, since after this point they escape the
...
@@ -375,6 +423,7 @@ def _update_with_callback(xblock, user, old_metadata=None, old_content=None):
...
@@ -375,6 +423,7 @@ def _update_with_callback(xblock, user, old_metadata=None, old_content=None):
old_metadata
=
own_metadata
(
xblock
)
old_metadata
=
own_metadata
(
xblock
)
if
old_content
is
None
:
if
old_content
is
None
:
old_content
=
xblock
.
get_explicitly_set_fields_by_scope
(
Scope
.
content
)
old_content
=
xblock
.
get_explicitly_set_fields_by_scope
(
Scope
.
content
)
xblock
.
xmodule_runtime
=
StudioEditModuleRuntime
(
user
)
xblock
.
editor_saved
(
user
,
old_metadata
,
old_content
)
xblock
.
editor_saved
(
user
,
old_metadata
,
old_content
)
# Update after the callback so any changes made in the callback will get persisted.
# Update after the callback so any changes made in the callback will get persisted.
...
@@ -624,6 +673,7 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_
...
@@ -624,6 +673,7 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_
# Allow an XBlock to do anything fancy it may need to when duplicated from another block.
# Allow an XBlock to do anything fancy it may need to when duplicated from another block.
# These blocks may handle their own children or parenting if needed. Let them return booleans to
# These blocks may handle their own children or parenting if needed. Let them return booleans to
# let us know if we need to handle these or not.
# let us know if we need to handle these or not.
dest_module
.
xmodule_runtime
=
StudioEditModuleRuntime
(
user
)
children_handled
=
dest_module
.
studio_post_duplicate
(
store
,
source_item
)
children_handled
=
dest_module
.
studio_post_duplicate
(
store
,
source_item
)
# Children are not automatically copied over (and not all xblocks have a 'children' attribute).
# Children are not automatically copied over (and not all xblocks have a 'children' attribute).
...
...
cms/djangoapps/contentstore/views/preview.py
View file @
f5223a36
...
@@ -16,7 +16,6 @@ from xmodule.x_module import PREVIEW_VIEWS, STUDENT_VIEW, AUTHOR_VIEW
...
@@ -16,7 +16,6 @@ from xmodule.x_module import PREVIEW_VIEWS, STUDENT_VIEW, AUTHOR_VIEW
from
xmodule.contentstore.django
import
contentstore
from
xmodule.contentstore.django
import
contentstore
from
xmodule.error_module
import
ErrorDescriptor
from
xmodule.error_module
import
ErrorDescriptor
from
xmodule.exceptions
import
NotFoundError
,
ProcessingError
from
xmodule.exceptions
import
NotFoundError
,
ProcessingError
from
xmodule.library_tools
import
LibraryToolsService
from
xmodule.services
import
SettingsService
from
xmodule.services
import
SettingsService
from
xmodule.modulestore.django
import
modulestore
,
ModuleI18nService
from
xmodule.modulestore.django
import
modulestore
,
ModuleI18nService
from
xmodule.mixin
import
wrap_with_license
from
xmodule.mixin
import
wrap_with_license
...
@@ -150,28 +149,6 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
...
@@ -150,28 +149,6 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
return
result
return
result
class
StudioPermissionsService
(
object
):
"""
Service that can provide information about a user's permissions.
Deprecated. To be replaced by a more general authorization service.
Only used by LibraryContentDescriptor (and library_tools.py).
"""
def
__init__
(
self
,
request
):
super
(
StudioPermissionsService
,
self
)
.
__init__
()
self
.
_request
=
request
def
can_read
(
self
,
course_key
):
""" Does the user have read access to the given course/library? """
return
has_studio_read_access
(
self
.
_request
.
user
,
course_key
)
def
can_write
(
self
,
course_key
):
""" Does the user have read access to the given course/library? """
return
has_studio_write_access
(
self
.
_request
.
user
,
course_key
)
def
_preview_module_system
(
request
,
descriptor
,
field_data
):
def
_preview_module_system
(
request
,
descriptor
,
field_data
):
"""
"""
Returns a ModuleSystem for the specified descriptor that is specialized for
Returns a ModuleSystem for the specified descriptor that is specialized for
...
@@ -213,8 +190,6 @@ def _preview_module_system(request, descriptor, field_data):
...
@@ -213,8 +190,6 @@ def _preview_module_system(request, descriptor, field_data):
# stick the license wrapper in front
# stick the license wrapper in front
wrappers
.
insert
(
0
,
wrap_with_license
)
wrappers
.
insert
(
0
,
wrap_with_license
)
descriptor
.
runtime
.
_services
[
'studio_user_permissions'
]
=
StudioPermissionsService
(
request
)
# pylint: disable=protected-access
return
PreviewModuleSystem
(
return
PreviewModuleSystem
(
static_url
=
settings
.
STATIC_URL
,
static_url
=
settings
.
STATIC_URL
,
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
...
@@ -241,7 +216,6 @@ def _preview_module_system(request, descriptor, field_data):
...
@@ -241,7 +216,6 @@ def _preview_module_system(request, descriptor, field_data):
services
=
{
services
=
{
"field-data"
:
field_data
,
"field-data"
:
field_data
,
"i18n"
:
ModuleI18nService
,
"i18n"
:
ModuleI18nService
,
"library_tools"
:
LibraryToolsService
(
modulestore
()),
"settings"
:
SettingsService
(),
"settings"
:
SettingsService
(),
"user"
:
DjangoXBlockUserService
(
request
.
user
),
"user"
:
DjangoXBlockUserService
(
request
.
user
),
},
},
...
...
common/lib/xmodule/xmodule/library_content_module.py
View file @
f5223a36
...
@@ -576,12 +576,10 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe
...
@@ -576,12 +576,10 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe
"""
"""
lib_tools
=
self
.
runtime
.
service
(
self
,
'library_tools'
)
lib_tools
=
self
.
runtime
.
service
(
self
,
'library_tools'
)
user_perms
=
self
.
runtime
.
service
(
self
,
'studio_user_permissions'
)
user_perms
=
self
.
runtime
.
service
(
self
,
'studio_user_permissions'
)
all_libraries
=
lib_tools
.
list_available_libraries
()
all_libraries
=
[
if
user_perms
:
(
key
,
name
)
for
key
,
name
in
lib_tools
.
list_available_libraries
()
all_libraries
=
[
if
user_perms
.
can_read
(
key
)
or
self
.
source_library_id
==
unicode
(
key
)
(
key
,
name
)
for
key
,
name
in
all_libraries
]
if
user_perms
.
can_read
(
key
)
or
self
.
source_library_id
==
unicode
(
key
)
]
all_libraries
.
sort
(
key
=
lambda
entry
:
entry
[
1
])
# Sort by name
all_libraries
.
sort
(
key
=
lambda
entry
:
entry
[
1
])
# Sort by name
if
self
.
source_library_id
and
self
.
source_library_key
not
in
[
entry
[
0
]
for
entry
in
all_libraries
]:
if
self
.
source_library_id
and
self
.
source_library_key
not
in
[
entry
[
0
]
for
entry
in
all_libraries
]:
all_libraries
.
append
((
self
.
source_library_id
,
_
(
u"Invalid Library"
)))
all_libraries
.
append
((
self
.
source_library_id
,
_
(
u"Invalid Library"
)))
...
...
common/lib/xmodule/xmodule/x_module.py
View file @
f5223a36
...
@@ -1482,8 +1482,9 @@ class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime):
...
@@ -1482,8 +1482,9 @@ class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime):
"""
"""
potential_set
=
set
(
super
(
DescriptorSystem
,
self
)
.
applicable_aside_types
(
block
))
potential_set
=
set
(
super
(
DescriptorSystem
,
self
)
.
applicable_aside_types
(
block
))
if
getattr
(
block
,
'xmodule_runtime'
,
None
)
is
not
None
:
if
getattr
(
block
,
'xmodule_runtime'
,
None
)
is
not
None
:
application_set
=
set
(
block
.
xmodule_runtime
.
applicable_aside_types
(
block
))
if
hasattr
(
block
.
xmodule_runtime
,
'applicable_aside_types'
):
return
list
(
potential_set
.
intersection
(
application_set
))
application_set
=
set
(
block
.
xmodule_runtime
.
applicable_aside_types
(
block
))
return
list
(
potential_set
.
intersection
(
application_set
))
return
list
(
potential_set
)
return
list
(
potential_set
)
def
resource_url
(
self
,
resource
):
def
resource_url
(
self
,
resource
):
...
...
common/static/js/vendor/jasmine-jquery.js
deleted
100644 → 0
View file @
d64e4b06
var
readFixtures
=
function
()
{
return
jasmine
.
getFixtures
().
proxyCallTo_
(
'read'
,
arguments
)
}
var
preloadFixtures
=
function
()
{
jasmine
.
getFixtures
().
proxyCallTo_
(
'preload'
,
arguments
)
}
var
loadFixtures
=
function
()
{
jasmine
.
getFixtures
().
proxyCallTo_
(
'load'
,
arguments
)
}
var
appendLoadFixtures
=
function
()
{
jasmine
.
getFixtures
().
proxyCallTo_
(
'appendLoad'
,
arguments
)
}
var
setFixtures
=
function
(
html
)
{
jasmine
.
getFixtures
().
proxyCallTo_
(
'set'
,
arguments
)
}
var
appendSetFixtures
=
function
()
{
jasmine
.
getFixtures
().
proxyCallTo_
(
'appendSet'
,
arguments
)
}
var
sandbox
=
function
(
attributes
)
{
return
jasmine
.
getFixtures
().
sandbox
(
attributes
)
}
var
spyOnEvent
=
function
(
selector
,
eventName
)
{
jasmine
.
JQuery
.
events
.
spyOn
(
selector
,
eventName
)
}
jasmine
.
getFixtures
=
function
()
{
return
jasmine
.
currentFixtures_
=
jasmine
.
currentFixtures_
||
new
jasmine
.
Fixtures
()
}
jasmine
.
Fixtures
=
function
()
{
this
.
containerId
=
'jasmine-fixtures'
this
.
fixturesCache_
=
{}
this
.
fixturesPath
=
'spec/javascripts/fixtures'
}
jasmine
.
Fixtures
.
prototype
.
set
=
function
(
html
)
{
this
.
cleanUp
()
this
.
createContainer_
(
html
)
}
jasmine
.
Fixtures
.
prototype
.
appendSet
=
function
(
html
)
{
this
.
addToContainer_
(
html
)
}
jasmine
.
Fixtures
.
prototype
.
preload
=
function
()
{
this
.
read
.
apply
(
this
,
arguments
)
}
jasmine
.
Fixtures
.
prototype
.
load
=
function
()
{
this
.
cleanUp
()
this
.
createContainer_
(
this
.
read
.
apply
(
this
,
arguments
))
}
jasmine
.
Fixtures
.
prototype
.
appendLoad
=
function
()
{
this
.
addToContainer_
(
this
.
read
.
apply
(
this
,
arguments
))
}
jasmine
.
Fixtures
.
prototype
.
read
=
function
()
{
var
htmlChunks
=
[]
var
fixtureUrls
=
arguments
for
(
var
urlCount
=
fixtureUrls
.
length
,
urlIndex
=
0
;
urlIndex
<
urlCount
;
urlIndex
++
)
{
htmlChunks
.
push
(
this
.
getFixtureHtml_
(
fixtureUrls
[
urlIndex
]))
}
return
htmlChunks
.
join
(
''
)
}
jasmine
.
Fixtures
.
prototype
.
clearCache
=
function
()
{
this
.
fixturesCache_
=
{}
}
jasmine
.
Fixtures
.
prototype
.
cleanUp
=
function
()
{
jQuery
(
'#'
+
this
.
containerId
).
remove
()
}
jasmine
.
Fixtures
.
prototype
.
sandbox
=
function
(
attributes
)
{
var
attributesToSet
=
attributes
||
{}
return
jQuery
(
'<div id="sandbox" />'
).
attr
(
attributesToSet
)
}
jasmine
.
Fixtures
.
prototype
.
createContainer_
=
function
(
html
)
{
var
container
if
(
html
instanceof
jQuery
)
{
container
=
jQuery
(
'<div id="'
+
this
.
containerId
+
'" />'
)
container
.
html
(
html
)
}
else
{
container
=
'<div id="'
+
this
.
containerId
+
'">'
+
html
+
'</div>'
}
jQuery
(
'body'
).
append
(
container
)
}
jasmine
.
Fixtures
.
prototype
.
addToContainer_
=
function
(
html
){
var
container
=
jQuery
(
'body'
).
find
(
'#'
+
this
.
containerId
).
append
(
html
)
if
(
!
container
.
length
){
this
.
createContainer_
(
html
)
}
}
jasmine
.
Fixtures
.
prototype
.
getFixtureHtml_
=
function
(
url
)
{
if
(
typeof
this
.
fixturesCache_
[
url
]
===
'undefined'
)
{
this
.
loadFixtureIntoCache_
(
url
)
}
return
this
.
fixturesCache_
[
url
]
}
jasmine
.
Fixtures
.
prototype
.
loadFixtureIntoCache_
=
function
(
relativeUrl
)
{
var
url
=
this
.
makeFixtureUrl_
(
relativeUrl
)
var
request
=
new
XMLHttpRequest
()
request
.
open
(
"GET"
,
url
+
"?"
+
new
Date
().
getTime
(),
false
)
request
.
send
(
null
)
this
.
fixturesCache_
[
relativeUrl
]
=
request
.
responseText
}
jasmine
.
Fixtures
.
prototype
.
makeFixtureUrl_
=
function
(
relativeUrl
){
return
this
.
fixturesPath
.
match
(
'/$'
)
?
this
.
fixturesPath
+
relativeUrl
:
this
.
fixturesPath
+
'/'
+
relativeUrl
}
jasmine
.
Fixtures
.
prototype
.
proxyCallTo_
=
function
(
methodName
,
passedArguments
)
{
return
this
[
methodName
].
apply
(
this
,
passedArguments
)
}
jasmine
.
JQuery
=
function
()
{}
jasmine
.
JQuery
.
browserTagCaseIndependentHtml
=
function
(
html
)
{
return
jQuery
(
'<div/>'
).
append
(
html
).
html
()
}
jasmine
.
JQuery
.
elementToString
=
function
(
element
)
{
var
domEl
=
$
(
element
).
get
(
0
)
if
(
domEl
==
undefined
||
domEl
.
cloneNode
)
return
jQuery
(
'<div />'
).
append
(
$
(
element
).
clone
()).
html
()
else
return
element
.
toString
()
}
jasmine
.
JQuery
.
matchersClass
=
{};
!
function
(
namespace
)
{
var
data
=
{
spiedEvents
:
{},
handlers
:
[]
}
namespace
.
events
=
{
spyOn
:
function
(
selector
,
eventName
)
{
var
handler
=
function
(
e
)
{
data
.
spiedEvents
[[
selector
,
eventName
]]
=
e
}
jQuery
(
selector
).
bind
(
eventName
,
handler
)
data
.
handlers
.
push
(
handler
)
},
wasTriggered
:
function
(
selector
,
eventName
)
{
return
!!
(
data
.
spiedEvents
[[
selector
,
eventName
]])
},
wasPrevented
:
function
(
selector
,
eventName
)
{
return
data
.
spiedEvents
[[
selector
,
eventName
]].
isDefaultPrevented
()
},
cleanUp
:
function
()
{
data
.
spiedEvents
=
{}
data
.
handlers
=
[]
}
}
}(
jasmine
.
JQuery
)
!
function
(){
var
jQueryMatchers
=
{
toHaveClass
:
function
(
className
)
{
return
this
.
actual
.
hasClass
(
className
)
},
toHaveCss
:
function
(
css
){
for
(
var
prop
in
css
){
if
(
this
.
actual
.
css
(
prop
)
!==
css
[
prop
])
return
false
}
return
true
},
toBeVisible
:
function
()
{
return
this
.
actual
.
is
(
':visible'
)
},
toBeHidden
:
function
()
{
return
this
.
actual
.
is
(
':hidden'
)
},
toBeSelected
:
function
()
{
return
this
.
actual
.
is
(
':selected'
)
},
toBeChecked
:
function
()
{
return
this
.
actual
.
is
(
':checked'
)
},
toBeEmpty
:
function
()
{
return
this
.
actual
.
is
(
':empty'
)
},
toExist
:
function
()
{
return
$
(
document
).
find
(
this
.
actual
).
length
},
toHaveAttr
:
function
(
attributeName
,
expectedAttributeValue
)
{
return
hasProperty
(
this
.
actual
.
attr
(
attributeName
),
expectedAttributeValue
)
},
toHaveProp
:
function
(
propertyName
,
expectedPropertyValue
)
{
return
hasProperty
(
this
.
actual
.
prop
(
propertyName
),
expectedPropertyValue
)
},
toHaveId
:
function
(
id
)
{
return
this
.
actual
.
attr
(
'id'
)
==
id
},
toHaveHtml
:
function
(
html
)
{
return
this
.
actual
.
html
()
==
jasmine
.
JQuery
.
browserTagCaseIndependentHtml
(
html
)
},
toContainHtml
:
function
(
html
){
var
actualHtml
=
this
.
actual
.
html
()
var
expectedHtml
=
jasmine
.
JQuery
.
browserTagCaseIndependentHtml
(
html
)
return
(
actualHtml
.
indexOf
(
expectedHtml
)
>=
0
)
},
toHaveText
:
function
(
text
)
{
var
trimmedText
=
$
.
trim
(
this
.
actual
.
text
())
if
(
text
&&
jQuery
.
isFunction
(
text
.
test
))
{
return
text
.
test
(
trimmedText
)
}
else
{
return
trimmedText
==
text
}
},
toHaveValue
:
function
(
value
)
{
return
this
.
actual
.
val
()
==
value
},
toHaveData
:
function
(
key
,
expectedValue
)
{
return
hasProperty
(
this
.
actual
.
data
(
key
),
expectedValue
)
},
toBe
:
function
(
selector
)
{
return
this
.
actual
.
is
(
selector
)
},
toContain
:
function
(
selector
)
{
return
this
.
actual
.
find
(
selector
).
length
},
toBeDisabled
:
function
(
selector
){
return
this
.
actual
.
is
(
':disabled'
)
},
toBeFocused
:
function
(
selector
)
{
return
this
.
actual
.
is
(
':focus'
)
},
toHandle
:
function
(
event
)
{
var
events
=
this
.
actual
.
data
(
'events'
)
if
(
!
events
||
!
event
||
typeof
event
!==
"string"
)
{
return
false
}
var
namespaces
=
event
.
split
(
"."
)
var
eventType
=
namespaces
.
shift
()
var
sortedNamespaces
=
namespaces
.
slice
(
0
).
sort
()
var
namespaceRegExp
=
new
RegExp
(
"(^|
\\
.)"
+
sortedNamespaces
.
join
(
"
\\
.(?:.*
\\
.)?"
)
+
"(
\\
.|$)"
)
if
(
events
[
eventType
]
&&
namespaces
.
length
)
{
for
(
var
i
=
0
;
i
<
events
[
eventType
].
length
;
i
++
)
{
var
namespace
=
events
[
eventType
][
i
].
namespace
if
(
namespaceRegExp
.
test
(
namespace
))
{
return
true
}
}
}
else
{
return
events
[
eventType
]
&&
events
[
eventType
].
length
>
0
}
},
// tests the existence of a specific event binding + handler
toHandleWith
:
function
(
eventName
,
eventHandler
)
{
var
stack
=
this
.
actual
.
data
(
"events"
)[
eventName
]
for
(
var
i
=
0
;
i
<
stack
.
length
;
i
++
)
{
if
(
stack
[
i
].
handler
==
eventHandler
)
return
true
}
return
false
}
}
var
hasProperty
=
function
(
actualValue
,
expectedValue
)
{
if
(
expectedValue
===
undefined
)
return
actualValue
!==
undefined
return
actualValue
==
expectedValue
}
var
bindMatcher
=
function
(
methodName
)
{
var
builtInMatcher
=
jasmine
.
Matchers
.
prototype
[
methodName
]
jasmine
.
JQuery
.
matchersClass
[
methodName
]
=
function
()
{
if
(
this
.
actual
&&
(
this
.
actual
instanceof
jQuery
||
jasmine
.
isDomNode
(
this
.
actual
)))
{
this
.
actual
=
$
(
this
.
actual
)
var
result
=
jQueryMatchers
[
methodName
].
apply
(
this
,
arguments
)
var
element
;
if
(
this
.
actual
.
get
&&
(
element
=
this
.
actual
.
get
()[
0
])
&&
!
$
.
isWindow
(
element
)
&&
element
.
tagName
!==
"HTML"
)
this
.
actual
=
jasmine
.
JQuery
.
elementToString
(
this
.
actual
)
return
result
}
if
(
builtInMatcher
)
{
return
builtInMatcher
.
apply
(
this
,
arguments
)
}
return
false
}
}
for
(
var
methodName
in
jQueryMatchers
)
{
bindMatcher
(
methodName
)
}
}()
beforeEach
(
function
()
{
this
.
addMatchers
(
jasmine
.
JQuery
.
matchersClass
)
this
.
addMatchers
({
toHaveBeenTriggeredOn
:
function
(
selector
)
{
this
.
message
=
function
()
{
return
[
"Expected event "
+
this
.
actual
+
" to have been triggered on "
+
selector
,
"Expected event "
+
this
.
actual
+
" not to have been triggered on "
+
selector
]
}
return
jasmine
.
JQuery
.
events
.
wasTriggered
(
$
(
selector
),
this
.
actual
)
}
})
this
.
addMatchers
({
toHaveBeenPreventedOn
:
function
(
selector
)
{
this
.
message
=
function
()
{
return
[
"Expected event "
+
this
.
actual
+
" to have been prevented on "
+
selector
,
"Expected event "
+
this
.
actual
+
" not to have been prevented on "
+
selector
]
}
return
jasmine
.
JQuery
.
events
.
wasPrevented
(
selector
,
this
.
actual
)
}
})
})
afterEach
(
function
()
{
jasmine
.
getFixtures
().
cleanUp
()
jasmine
.
JQuery
.
events
.
cleanUp
()
})
lms/djangoapps/bulk_email/models.py
View file @
f5223a36
...
@@ -12,6 +12,7 @@ file and check it in at the same time as your model changes. To do that,
...
@@ -12,6 +12,7 @@ file and check it in at the same time as your model changes. To do that,
"""
"""
import
logging
import
logging
import
markupsafe
from
django.conf
import
settings
from
django.conf
import
settings
from
django.contrib.auth.models
import
User
from
django.contrib.auth.models
import
User
from
django.db
import
models
from
django.db
import
models
...
@@ -176,7 +177,7 @@ class CourseEmailTemplate(models.Model):
...
@@ -176,7 +177,7 @@ class CourseEmailTemplate(models.Model):
which is rendered using format() with the provided `context` dict.
which is rendered using format() with the provided `context` dict.
Any keywords encoded in the form
%%
KEYWORD
%%
found in the message
Any keywords encoded in the form
%%
KEYWORD
%%
found in the message
body are subtituted with user data before the body is inserted into
body are sub
s
tituted with user data before the body is inserted into
the template.
the template.
Output is returned as a unicode string. It is not encoded as utf-8.
Output is returned as a unicode string. It is not encoded as utf-8.
...
@@ -215,6 +216,10 @@ class CourseEmailTemplate(models.Model):
...
@@ -215,6 +216,10 @@ class CourseEmailTemplate(models.Model):
Convert HTML text body (`htmltext`) into HTML email message using the
Convert HTML text body (`htmltext`) into HTML email message using the
stored HTML template and the provided `context` dict.
stored HTML template and the provided `context` dict.
"""
"""
# HTML-escape string values in the context (used for keyword substitution).
for
key
,
value
in
context
.
iteritems
():
if
isinstance
(
value
,
basestring
):
context
[
key
]
=
markupsafe
.
escape
(
value
)
return
CourseEmailTemplate
.
_render
(
self
.
html_template
,
htmltext
,
context
)
return
CourseEmailTemplate
.
_render
(
self
.
html_template
,
htmltext
,
context
)
...
...
lms/djangoapps/bulk_email/tests/test_models.py
View file @
f5223a36
...
@@ -97,6 +97,15 @@ class CourseEmailTemplateTest(TestCase):
...
@@ -97,6 +97,15 @@ class CourseEmailTemplateTest(TestCase):
context
[
'course_image_url'
]
=
"/location/of/course/image/url"
context
[
'course_image_url'
]
=
"/location/of/course/image/url"
return
context
return
context
def
_add_xss_fields
(
self
,
context
):
""" Add fields to the context for XSS testing. """
context
[
'course_title'
]
=
"<script>alert('Course Title!');</alert>"
context
[
'name'
]
=
"<script>alert('Profile Name!');</alert>"
# Must have user_id and course_id present in order to do keyword substitution
context
[
'user_id'
]
=
12345
context
[
'course_id'
]
=
"course-v1:edx+100+1"
return
context
def
test_get_template
(
self
):
def
test_get_template
(
self
):
# Get the default template, which has name=None
# Get the default template, which has name=None
template
=
CourseEmailTemplate
.
get_template
()
template
=
CourseEmailTemplate
.
get_template
()
...
@@ -134,11 +143,31 @@ class CourseEmailTemplateTest(TestCase):
...
@@ -134,11 +143,31 @@ class CourseEmailTemplateTest(TestCase):
context
=
self
.
_get_sample_html_context
()
context
=
self
.
_get_sample_html_context
()
template
.
render_htmltext
(
"My new html text."
,
context
)
template
.
render_htmltext
(
"My new html text."
,
context
)
def
test_render_html_xss
(
self
):
template
=
CourseEmailTemplate
.
get_template
()
context
=
self
.
_add_xss_fields
(
self
.
_get_sample_html_context
())
message
=
template
.
render_htmltext
(
"Dear
%%
USER_FULLNAME
%%
, thanks for enrolling in
%%
COURSE_DISPLAY_NAME
%%
."
,
context
)
self
.
assertNotIn
(
"<script>"
,
message
)
self
.
assertIn
(
"<script>alert('Course Title!');</alert>"
,
message
)
self
.
assertIn
(
"<script>alert('Profile Name!');</alert>"
,
message
)
def
test_render_plain
(
self
):
def
test_render_plain
(
self
):
template
=
CourseEmailTemplate
.
get_template
()
template
=
CourseEmailTemplate
.
get_template
()
context
=
self
.
_get_sample_plain_context
()
context
=
self
.
_get_sample_plain_context
()
template
.
render_plaintext
(
"My new plain text."
,
context
)
template
.
render_plaintext
(
"My new plain text."
,
context
)
def
test_render_plain_no_escaping
(
self
):
template
=
CourseEmailTemplate
.
get_template
()
context
=
self
.
_add_xss_fields
(
self
.
_get_sample_plain_context
())
message
=
template
.
render_plaintext
(
"Dear
%%
USER_FULLNAME
%%
, thanks for enrolling in
%%
COURSE_DISPLAY_NAME
%%
."
,
context
)
self
.
assertNotIn
(
"<script>"
,
message
)
self
.
assertIn
(
context
[
'course_title'
],
message
)
self
.
assertIn
(
context
[
'name'
],
message
)
@attr
(
'shard_1'
)
@attr
(
'shard_1'
)
class
CourseAuthorizationTest
(
TestCase
):
class
CourseAuthorizationTest
(
TestCase
):
...
...
lms/djangoapps/instructor/views/instructor_dashboard.py
View file @
f5223a36
...
@@ -52,6 +52,8 @@ from class_dashboard.dashboard_data import get_section_display_name, get_array_s
...
@@ -52,6 +52,8 @@ from class_dashboard.dashboard_data import get_section_display_name, get_array_s
from
.tools
import
get_units_with_due_date
,
title_or_url
,
bulk_email_is_enabled_for_course
from
.tools
import
get_units_with_due_date
,
title_or_url
,
bulk_email_is_enabled_for_course
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
openedx.core.djangolib.markup
import
Text
,
HTML
log
=
logging
.
getLogger
(
__name__
)
log
=
logging
.
getLogger
(
__name__
)
...
@@ -111,13 +113,13 @@ def instructor_dashboard_2(request, course_id):
...
@@ -111,13 +113,13 @@ def instructor_dashboard_2(request, course_id):
if
settings
.
ANALYTICS_DASHBOARD_URL
:
if
settings
.
ANALYTICS_DASHBOARD_URL
:
# Construct a URL to the external analytics dashboard
# Construct a URL to the external analytics dashboard
analytics_dashboard_url
=
'{0}/courses/{1}'
.
format
(
settings
.
ANALYTICS_DASHBOARD_URL
,
unicode
(
course_key
))
analytics_dashboard_url
=
'{0}/courses/{1}'
.
format
(
settings
.
ANALYTICS_DASHBOARD_URL
,
unicode
(
course_key
))
link_start
=
"<a href=
\"
{}
\"
target=
\"
_blank
\"
>"
.
format
(
analytics_dashboard_url
)
link_start
=
HTML
(
"<a href=
\"
{}
\"
target=
\"
_blank
\"
>"
)
.
format
(
analytics_dashboard_url
)
analytics_dashboard_message
=
_
(
analytics_dashboard_message
=
_
(
"To gain insights into student enrollment and participation {link_start}"
"To gain insights into student enrollment and participation {link_start}"
"visit {analytics_dashboard_name}, our new course analytics product{link_end}."
"visit {analytics_dashboard_name}, our new course analytics product{link_end}."
)
)
analytics_dashboard_message
=
analytics_dashboard_message
.
format
(
analytics_dashboard_message
=
Text
(
analytics_dashboard_message
)
.
format
(
link_start
=
link_start
,
link_end
=
"</a>"
,
analytics_dashboard_name
=
settings
.
ANALYTICS_DASHBOARD_NAME
)
link_start
=
link_start
,
link_end
=
HTML
(
"</a>"
)
,
analytics_dashboard_name
=
settings
.
ANALYTICS_DASHBOARD_NAME
)
# Temporarily show the "Analytics" section until we have a better way of linking to Insights
# Temporarily show the "Analytics" section until we have a better way of linking to Insights
sections
.
append
(
_section_analytics
(
course
,
access
))
sections
.
append
(
_section_analytics
(
course
,
access
))
...
@@ -629,8 +631,9 @@ def _section_send_email(course, access):
...
@@ -629,8 +631,9 @@ def _section_send_email(course, access):
def
_get_dashboard_link
(
course_key
):
def
_get_dashboard_link
(
course_key
):
""" Construct a URL to the external analytics dashboard """
""" Construct a URL to the external analytics dashboard """
analytics_dashboard_url
=
'{0}/courses/{1}'
.
format
(
settings
.
ANALYTICS_DASHBOARD_URL
,
unicode
(
course_key
))
analytics_dashboard_url
=
'{0}/courses/{1}'
.
format
(
settings
.
ANALYTICS_DASHBOARD_URL
,
unicode
(
course_key
))
link
=
u"<a href=
\"
{0}
\"
target=
\"
_blank
\"
>{1}</a>"
.
format
(
analytics_dashboard_url
,
link
=
HTML
(
u"<a href=
\"
{0}
\"
target=
\"
_blank
\"
>{1}</a>"
)
.
format
(
settings
.
ANALYTICS_DASHBOARD_NAME
)
analytics_dashboard_url
,
settings
.
ANALYTICS_DASHBOARD_NAME
)
return
link
return
link
...
...
lms/static/sass/_build-lms-v2.scss
View file @
f5223a36
...
@@ -6,8 +6,10 @@
...
@@ -6,8 +6,10 @@
// Configuration
// Configuration
@import
'config'
;
@import
'config'
;
@import
'base/variables'
;
@import
'base/variables'
;
@import
'base-v2/extends'
;
// Extensions
// Extensions
@import
'shared-v2/base'
;
@import
'shared-v2/base'
;
@import
'shared-v2/navigation'
;
@import
'shared-v2/navigation'
;
@import
'shared-v2/header'
;
@import
'shared-v2/header'
;
@import
'shared-v2/footer'
;
lms/static/sass/base-v2/_extends.scss
0 → 100644
View file @
f5223a36
// Adds a simple extend that indicates that this user interface element should not print
%ui-print-excluded
{
@media
print
{
display
:none
;
}
}
lms/static/sass/multicourse/_dashboard.scss
View file @
f5223a36
...
@@ -23,12 +23,42 @@
...
@@ -23,12 +23,42 @@
border-top
:
3px
solid
$blue
;
border-top
:
3px
solid
$blue
;
padding
:
$baseline
0
;
padding
:
$baseline
0
;
.copy
{
.course-advertise
{
@extend
%t-copy-sub1
;
@include
clearfix
();
}
box-sizing
:
border-box
;
padding
:
$baseline
;
.btn-find-courses
{
background-color
:
$body-bg
;
@extend
%btn-pl-elevated-alt
;
border
:
1px
solid
$border-color-l3
;
.advertise-message
{
@include
font-size
(
12
);
color
:
$gray-d4
;
margin-bottom
:
$baseline
;
}
.ad-link
{
@include
text-align
(
center
);
.btn-find-courses
{
padding-bottom
:
12px
;
padding-top
:
12px
;
}
a
{
@include
font-size
(
16
);
@include
line-height
(
1
.2
);
padding
:
$baseline
*
0
.5
;
border
:
1px
solid
$blue
;
color
:
$blue
;
text-decoration
:
none
;
display
:
block
;
&
:hover
,
&
:focus
,
&
:active
{
color
:
$white
;
background-color
:
$blue
;
}
span
{
@include
margin-left
(
$baseline
*
0
.25
);
}
}
}
}
}
}
}
...
...
lms/static/sass/shared-v2/_footer.scss
0 → 100644
View file @
f5223a36
// Open edX: LMS footer
// ====================
.wrapper-footer
{
@extend
%ui-print-excluded
;
margin-top
:
(
$baseline
*
2
)
+
px
;
box-shadow
:
0
-1px
5px
0
$shadow-l1
;
border-top
:
1px
solid
tint
(
palette
(
grayscale
,
light
)
,
50%
);
padding
:
25px
(
$baseline
/
2
+
px
)
(
$baseline
*
1
.5
+
px
)
(
$baseline
/
2
+
px
);
background
:
$footer-bg
;
clear
:
both
;
footer
#footer-openedx
{
@include
clearfix
();
box-sizing
:
border-box
;
margin
:
0
auto
;
p
,
ol
,
ul
{
font-family
:
$sans-serif
;
// override needed for poorly scoped font-family styling on p a:link {}
a
{
font-family
:
$sans-serif
;
}
}
a
{
@extend
%link-text
;
border-bottom
:
none
;
&
:hover
,
&
:focus
,
&
:active
{
border-bottom
:
1px
dotted
$link-color
;
}
}
// colophon
.colophon
{
@include
span
(
12
);
@include
susy-media
(
$bp-screen-sm
)
{
@include
span
(
8
);
}
.nav-colophon
{
@include
clearfix
();
margin
:
$footer_margin
;
li
{
@include
float
(
left
);
margin-right
:
(
$baseline
*
0
.75
)
+
px
;
a
{
color
:
tint
(
$black
,
20%
);
&
:hover
,
&
:focus
,
&
:active
{
color
:
$link-color
;
}
}
&
:last-child
{
@include
margin-right
(
0
);
}
}
}
.colophon-about
{
@include
clearfix
();
img
{
@include
float
(
left
);
width
:
68px
;
height
:
34px
;
margin-right
:
0
;
}
p
{
@include
float
(
left
);
@include
span
(
9
);
margin-left
:
$baseline
+
px
;
padding-left
:
$baseline
+
px
;
font-size
:
font-size
(
small
);
background
:
transparent
url(/static/images/bg-footer-divider.jpg)
0
0
no-repeat
;
}
}
}
// references
.references
{
@include
span
(
4
);
margin
:
-10px
0
0
0
;
display
:
inline-block
;
}
.wrapper-logo
{
margin
:
(
$baseline
*
0
.75
)
+
px
0
;
a
{
display
:
inline-block
;
&
:hover
{
border-bottom
:
0
;
}
}
}
.copyright
{
@include
text-align
(
left
);
margin
:
-2px
0
8px
0
;
font-size
:
font-size
(
xx-small
);
color
:
palette
(
grayscale
,
dark
);
}
.nav-legal
{
@include
clearfix
();
@include
text-align
(
left
);
li
{
display
:
inline-block
;
font-size
:
font-size
(
xx-small
);
}
.nav-legal-02
a
{
&
:before
{
@include
margin-right
((
$baseline
/
4
)
+
px
);
content
:
"-"
;
}
}
}
.nav-social
{
@include
text-align
(
right
);
margin
:
0
;
li
{
display
:
inline-block
;
&
:last-child
{
margin-right
:
0
;
}
a
{
display
:
block
;
&
:hover
,
&
:focus
,
&
:active
{
border
:
none
;
}
}
img
{
display
:
block
;
}
}
}
// platform Open edX logo and link
.footer-about-openedx
{
@include
span
(
12
);
@include
text-align
(
right
);
vertical-align
:
bottom
;
@include
susy-media
(
$bp-screen-sm
)
{
@include
span
(
4
);
@include
margin-right
(
0
);
}
a
{
@include
float
(
right
);
display
:
inline-block
;
&
:hover
{
border-bottom
:
none
;
}
}
}
}
}
// marketing site design syncing
.view-register
,
.view-login
,
.view-passwordreset
{
.wrapper-footer
footer
{
width
:
960px
;
.colophon-about
img
{
margin-top
:
(
$baseline
*
1
.5
)
+
px
;
}
}
}
lms/static/sass/shared-v2/_header.scss
View file @
f5223a36
...
@@ -2,7 +2,7 @@
...
@@ -2,7 +2,7 @@
.header-global
{
.header-global
{
@extend
%ui-depth1
;
@extend
%ui-depth1
;
@include
box-sizing
(
border-box
)
;
box-sizing
:
border-box
;
position
:
relative
;
position
:
relative
;
width
:
100%
;
width
:
100%
;
border-bottom
:
4px
solid
$courseware-border-bottom-color
;
border-bottom
:
4px
solid
$courseware-border-bottom-color
;
...
@@ -11,7 +11,7 @@
...
@@ -11,7 +11,7 @@
.wrapper-header
{
.wrapper-header
{
@include
clearfix
();
@include
clearfix
();
@include
box-sizing
(
border-box
)
;
box-sizing
:
border-box
;
height
:
74px
;
height
:
74px
;
margin
:
0
auto
;
margin
:
0
auto
;
padding
:
10px
10px
0
;
padding
:
10px
10px
0
;
...
...
lms/static/sass/shared/_footer-edx.scss
View file @
f5223a36
...
@@ -17,6 +17,10 @@ footer#footer-edx-v3 {
...
@@ -17,6 +17,10 @@ footer#footer-edx-v3 {
background
:
$edx-footer-bg-color
;
background
:
$edx-footer-bg-color
;
padding
:
20px
;
padding
:
20px
;
border-top
:
1px
solid
$courseware-button-border-color
;
border-top
:
1px
solid
$courseware-button-border-color
;
// To match the Pattern Library
-webkit-font-smoothing
:
antialiased
;
-moz-osx-font-smoothing
:
grayscale
;
.footer-content-wrapper
{
.footer-content-wrapper
{
@include
outer-container
;
@include
outer-container
;
...
...
lms/static/sass/shared/_footer.scss
View file @
f5223a36
...
@@ -191,9 +191,7 @@
...
@@ -191,9 +191,7 @@
// edx theme overrides
// edx theme overrides
&
.edx-footer
{
&
.edx-footer
{
footer
{
footer
{
.copyright
{
.copyright
{
text-align
:
right
;
text-align
:
right
;
}
}
...
@@ -216,86 +214,3 @@
...
@@ -216,86 +214,3 @@
}
}
}
}
}
}
// edX theme: LMS Footer
// ====================
$edx-footer-spacing
:
(
$baseline
*
0
.75
);
$edx-footer-link-color
:
$link-color
;
$edx-footer-bg-color
:
rgb
(
252
,
252
,
252
);
%edx-footer-reset
{
@include
box-sizing
(
border-box
);
}
%edx-footer-section
{
@include
float
(
left
);
min-height
:
(
$baseline
*
17
.5
);
@include
margin-right
(
flex-gutter
());
@include
border-right
(
1px
solid
rgb
(
230
,
230
,
230
));
@include
padding-right
(
$baseline
*
1
.5
);
// CASE: last child
&
:last-child
{
@include
margin-right
(
0
);
border
:
none
;
@include
padding-right
(
0
);
}
}
%edx-footer-title
{
// TODO: refactor _typography.scss to extend this set of styling
@extend
%t-title
;
@extend
%t-weight4
;
@include
font-size
(
15
);
@include
line-height
(
15
);
text-transform
:
none
;
letter-spacing
:
inherit
;
color
:
rgb
(
61
,
62
,
63
);
}
%edx-footer-link
{
@extend
%t-copy-sub1
;
@include
transition
(
color
$tmg-f2
ease-in-out
0
);
display
:
block
;
margin-bottom
:
(
$baseline
/
2
);
// NOTE: resetting poor link styles
border
:
none
;
padding
:
0
;
color
:
$edx-footer-link-color
;
.copy
{
@include
transition
(
border-color
$tmg-f2
ease-in-out
0
);
display
:
inline-block
;
border-bottom
:
1px
solid
transparent
;
padding
:
0
0
(
$baseline
/
20
)
0
;
color
:
$edx-footer-link-color
;
}
// STATE: hover + focused
&
:hover
,
&
:focus
{
color
:
saturate
(
$edx-footer-link-color
,
25%
);
// NOTE: resetting poor link styles
border
:
none
;
.copy
{
border-bottom-color
:
saturate
(
$edx-footer-link-color
,
25%
);
}
}
// CASE: last child
&
:last-child
{
margin-bottom
:
0
;
}
// CASE: has visual emphasis
&
.has-emphasis
{
@extend
%t-weight4
;
.copy
{
@extend
%t-weight4
;
}
}
}
lms/templates/footer.html
View file @
f5223a36
...
@@ -9,7 +9,7 @@
...
@@ -9,7 +9,7 @@
<
%
namespace
name=
'static'
file=
'static_content.html'
/>
<
%
namespace
name=
'static'
file=
'static_content.html'
/>
<div
class=
"wrapper wrapper-footer"
>
<div
class=
"wrapper wrapper-footer"
>
<footer
id=
"footer-openedx"
<footer
id=
"footer-openedx"
class=
"grid-container"
##
When
rendering
the
footer
through
the
branding
API
,
##
When
rendering
the
footer
through
the
branding
API
,
##
the
direction
may
not
be
set
on
the
parent
element
,
##
the
direction
may
not
be
set
on
the
parent
element
,
##
so
we
set
it
here
.
##
so
we
set
it
here
.
...
...
lms/templates/navigation.html
View file @
f5223a36
...
@@ -71,7 +71,7 @@ site_status_msg = get_site_status_msg(course_id)
...
@@ -71,7 +71,7 @@ site_status_msg = get_site_status_msg(course_id)
% endif
% endif
% if user.is_authenticated():
% if user.is_authenticated():
<ol
class=
"left nav-global authenticated"
>
<ol
class=
"left nav-global
list-inline
authenticated"
>
<
%
block
name=
"navigation_global_links_authenticated"
>
<
%
block
name=
"navigation_global_links_authenticated"
>
% if settings.FEATURES.get('COURSES_ARE_BROWSABLE') and not show_program_listing:
% if settings.FEATURES.get('COURSES_ARE_BROWSABLE') and not show_program_listing:
<li
class=
"item nav-global-01"
>
<li
class=
"item nav-global-01"
>
...
...
themes/edx.org/lms/templates/dashboard.html
View file @
f5223a36
...
@@ -157,10 +157,19 @@ from openedx.core.djangolib.markup import Text, HTML
...
@@ -157,10 +157,19 @@ from openedx.core.djangolib.markup import Text, HTML
% endif
% endif
% if settings.FEATURES.get('COURSES_ARE_BROWSABLE'):
% if settings.FEATURES.get('COURSES_ARE_BROWSABLE'):
<div
class=
"wrapper-find-courses"
>
<div
class=
"wrapper-find-courses"
>
<p
class=
"copy"
>
${_("Check out our recently launched courses and what's new in your favorite subjects")}
</p>
<div
class=
"course-advertise"
>
<p><a
class=
"btn-find-courses"
href=
"${marketing_link('COURSES')}"
>
${_("Find New Courses")}
</a></p>
<div
class=
"advertise-message"
>
</div>
${_("Browse recently launched courses and see what's new in your favorite subjects.")}
</div>
<div
class=
"ad-link"
>
<a
class=
"btn-find-courses"
href=
"${marketing_link('COURSES')}"
>
<span
class=
"icon fa fa-search"
aria-hidden=
"true"
></span>
${_("Explore New Courses")}
</a>
</div>
</div>
</div>
% endif
% endif
<section
class=
"profile-sidebar"
id=
"profile-sidebar"
role=
"region"
aria-label=
"Account Status Info"
>
<section
class=
"profile-sidebar"
id=
"profile-sidebar"
role=
"region"
aria-label=
"Account Status Info"
>
...
...
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