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
3cbbb8f3
Commit
3cbbb8f3
authored
May 25, 2015
by
muzaffaryousaf
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add/Remove Bookmark button to each unit in LMS courseware.
TNL-1957
parent
c9b87aa0
Hide whitespace changes
Inline
Side-by-side
Showing
33 changed files
with
654 additions
and
111 deletions
+654
-111
common/lib/xmodule/xmodule/css/sequence/display.scss
+11
-2
common/lib/xmodule/xmodule/js/src/sequence/display.coffee
+16
-1
common/lib/xmodule/xmodule/library_content_module.py
+1
-0
common/lib/xmodule/xmodule/public/js/vertical_student_view.js
+16
-0
common/lib/xmodule/xmodule/seq_module.py
+16
-4
common/lib/xmodule/xmodule/tests/test_vertical.py
+14
-1
common/lib/xmodule/xmodule/vertical_block.py
+7
-0
common/test/acceptance/pages/lms/bookmarks.py
+6
-5
common/test/acceptance/pages/lms/course_nav.py
+2
-2
common/test/acceptance/pages/lms/courseware.py
+27
-0
common/test/acceptance/tests/lms/test_bookmarks.py
+145
-61
common/test/acceptance/tests/lms/test_lms_courseware.py
+35
-2
lms/djangoapps/bookmarks/tests/test_views.py
+2
-2
lms/djangoapps/courseware/module_render.py
+3
-0
lms/djangoapps/courseware/tests/test_module_render.py
+9
-7
lms/djangoapps/courseware/tests/test_split_module.py
+1
-1
lms/djangoapps/courseware/views.py
+3
-1
lms/static/js/bookmarks/collections/bookmarks.js
+5
-2
lms/static/js/bookmarks/main.js
+3
-3
lms/static/js/bookmarks/views/bookmark_button.js
+93
-0
lms/static/js/bookmarks/views/bookmarks_list.js
+3
-1
lms/static/js/bookmarks/views/bookmarks_list_button.js
+0
-2
lms/static/js/fixtures/bookmarks/bookmark_button.html
+13
-0
lms/static/js/fixtures/bookmarks/bookmarks.html
+1
-1
lms/static/js/spec/bookmarks/bookmark_button_view_spec.js
+114
-0
lms/static/js/spec/bookmarks/bookmarks_list_view_spec.js
+5
-5
lms/static/js/spec/main.js
+9
-1
lms/static/sass/course/courseware/_courseware.scss
+4
-0
lms/static/sass/views/_bookmarks.scss
+59
-4
lms/templates/bookmark_button.html
+11
-0
lms/templates/courseware/courseware.html
+10
-1
lms/templates/seq_module.html
+5
-2
lms/templates/vert_module.html
+5
-0
No files found.
common/lib/xmodule/xmodule/css/sequence/display.scss
View file @
3cbbb8f3
$sequence--border-color
:
#C8C8C8
;
$link-color
:
rgb
(
26
,
161
,
222
);
// repeated extends - needed since LMS styling was referenced
.block-link
{
border-left
:
1px
solid
lighten
(
$sequence--border-color
,
10%
);
...
...
@@ -36,7 +36,7 @@ $sequence--border-color: #C8C8C8;
// TODO (cpennington): This doesn't work anymore. XModules aren't able to
// import from external sources.
@extend
.topbar
;
margin
:
-4px
0
(
$baseline
*
1
.5
)
;
margin
:
-4px
0
$baseline
;
position
:
relative
;
border-bottom
:
none
;
z-index
:
0
;
...
...
@@ -119,6 +119,10 @@ $sequence--border-color: #C8C8C8;
-webkit-font-smoothing
:
antialiased
;
// Clear up the lines on the icons
}
i
.fa-bookmark
{
color
:
$link-color
;
}
&
.inactive
{
.icon
{
...
...
@@ -142,6 +146,10 @@ $sequence--border-color: #C8C8C8;
.icon
{
color
:
rgb
(
10
,
10
,
10
);
}
i
.fa-bookmark
{
color
:
$link-color
;
}
}
}
...
...
@@ -295,3 +303,4 @@ nav.sequence-bottom {
outline
:
none
;
}
}
common/lib/xmodule/xmodule/js/src/sequence/display.coffee
View file @
3cbbb8f3
...
...
@@ -18,6 +18,8 @@ class @Sequence
bind
:
->
@
$
(
'#sequence-list a'
).
click
@
goto
@
el
.
on
'bookmark:add'
,
@
addBookmarkIconToActiveNavItem
@
el
.
on
'bookmark:remove'
,
@
removeBookmarkIconFromActiveNavItem
initProgress
:
->
@
progressTable
=
{}
# "#problem_#{id}" -> progress
...
...
@@ -102,8 +104,9 @@ class @Sequence
@
mark_active
new_position
current_tab
=
@
contents
.
eq
(
new_position
-
1
)
@
content_container
.
html
(
current_tab
.
text
()).
attr
(
"aria-labelledby"
,
current_tab
.
attr
(
"aria-labelledby"
))
bookmarked
=
if
@
el
.
find
(
'.active .bookmark-icon'
).
hasClass
(
'bookmarked'
)
then
true
else
false
@
content_container
.
html
(
current_tab
.
text
()).
attr
(
"aria-labelledby"
,
current_tab
.
attr
(
"aria-labelledby"
)).
data
(
'bookmarked'
,
bookmarked
)
XBlock
.
initializeBlocks
(
@
content_container
,
@
requestToken
)
window
.
update_schematics
()
# For embedded circuit simulator exercises in 6.002x
...
...
@@ -116,6 +119,8 @@ class @Sequence
sequence_links
=
@
content_container
.
find
(
'a.seqnav'
)
sequence_links
.
click
@
goto
@
el
.
find
(
'.path'
).
html
(
@
el
.
find
(
'.nav-item.active'
).
data
(
'path'
))
@
sr_container
.
focus
();
# @$("a.active").blur()
...
...
@@ -180,3 +185,13 @@ class @Sequence
element
.
removeClass
(
"inactive"
)
.
removeClass
(
"visited"
)
.
addClass
(
"active"
)
addBookmarkIconToActiveNavItem
:
(
event
)
=>
event
.
preventDefault
()
@
el
.
find
(
'.nav-item.active .bookmark-icon'
).
removeClass
(
'is-hidden'
).
addClass
(
'bookmarked'
)
@
el
.
find
(
'.nav-item.active .bookmark-icon-sr'
).
text
(
gettext
(
'Bookmarked'
))
removeBookmarkIconFromActiveNavItem
:
(
event
)
=>
event
.
preventDefault
()
@
el
.
find
(
'.nav-item.active .bookmark-icon'
).
removeClass
(
'bookmarked'
).
addClass
(
'is-hidden'
)
@
el
.
find
(
'.nav-item.active .bookmark-icon-sr'
).
text
(
''
)
common/lib/xmodule/xmodule/library_content_module.py
View file @
3cbbb8f3
...
...
@@ -316,6 +316,7 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
fragment
.
add_content
(
self
.
system
.
render_template
(
'vert_module.html'
,
{
'items'
:
contents
,
'xblock_context'
:
context
,
'show_bookmark_button'
:
False
,
}))
return
fragment
...
...
common/lib/xmodule/xmodule/public/js/vertical_student_view.js
0 → 100644
View file @
3cbbb8f3
/* JavaScript for Vertical Student View. */
window
.
VerticalStudentView
=
function
(
runtime
,
element
)
{
RequireJS
.
require
([
'js/bookmarks/views/bookmark_button'
],
function
(
BookmarkButton
)
{
var
$element
=
$
(
element
);
var
$bookmarkButtonElement
=
$element
.
find
(
'.bookmark-button'
);
return
new
BookmarkButton
({
el
:
$bookmarkButtonElement
,
bookmarkId
:
$bookmarkButtonElement
.
data
(
'bookmarkId'
),
usageId
:
$element
.
data
(
'usageId'
),
bookmarked
:
$element
.
parent
(
'#seq_content'
).
data
(
'bookmarked'
),
apiUrl
:
$
(
".courseware-bookmarks-button"
).
data
(
'bookmarksApiUrl'
)
});
});
};
common/lib/xmodule/xmodule/seq_module.py
View file @
3cbbb8f3
...
...
@@ -55,7 +55,6 @@ class SequenceFields(object):
scope
=
Scope
.
settings
,
)
class
ProctoringFields
(
object
):
"""
Fields that are specific to Proctored or Timed Exams
...
...
@@ -119,9 +118,12 @@ class ProctoringFields(object):
@XBlock.wants
(
'proctoring'
)
@XBlock.wants
(
'credit'
)
class
SequenceModule
(
SequenceFields
,
ProctoringFields
,
XModule
):
''' Layout module which lays out content in a temporal sequence
'''
@XBlock.needs
(
"user"
)
@XBlock.needs
(
"bookmarks"
)
class
SequenceModule
(
SequenceFields
,
XModule
):
"""
Layout module which lays out content in a temporal sequence
"""
js
=
{
'coffee'
:
[
resource_string
(
__name__
,
'js/src/sequence/display.coffee'
)],
'js'
:
[
resource_string
(
__name__
,
'js/src/sequence/display/jquery.sequence.js'
)],
...
...
@@ -182,7 +184,12 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
contents
=
[]
fragment
=
Fragment
()
context
=
context
or
{}
bookmarks_service
=
self
.
runtime
.
service
(
self
,
"bookmarks"
)
context
[
"username"
]
=
self
.
runtime
.
service
(
self
,
"user"
)
.
get_current_user
()
.
opt_attrs
[
'edx-platform.username'
]
display_names
=
[
self
.
get_parent
()
.
display_name
or
''
,
self
.
display_name
or
''
]
# Is this sequential part of a timed or proctored exam?
if
self
.
is_time_limited
:
view_html
=
self
.
_time_limited_student_view
(
context
)
...
...
@@ -194,6 +201,9 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
return
fragment
for
child
in
self
.
get_display_items
():
is_bookmarked
=
bookmarks_service
.
is_bookmarked
(
usage_key
=
child
.
scope_ids
.
usage_id
)
context
[
"bookmarked"
]
=
is_bookmarked
progress
=
child
.
get_progress
()
rendered_child
=
child
.
render
(
STUDENT_VIEW
,
context
)
fragment
.
add_frag_resources
(
rendered_child
)
...
...
@@ -209,6 +219,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
'progress_detail'
:
Progress
.
to_js_detail_str
(
progress
),
'type'
:
child
.
get_icon_class
(),
'id'
:
child
.
scope_ids
.
usage_id
.
to_deprecated_string
(),
'bookmarked'
:
is_bookmarked
,
'path'
:
" > "
.
join
(
display_names
+
[
child
.
display_name
or
''
]),
}
if
childinfo
[
'title'
]
==
''
:
childinfo
[
'title'
]
=
child
.
display_name_with_default
...
...
common/lib/xmodule/xmodule/tests/test_vertical.py
View file @
3cbbb8f3
...
...
@@ -37,18 +37,31 @@ class BaseVerticalBlockTest(XModuleXmlImportTest):
self
.
vertical
=
course_seq
.
get_children
()[
0
]
self
.
vertical
.
xmodule_runtime
=
self
.
module_system
self
.
username
=
"bilbo"
self
.
default_context
=
{
"bookmarked"
:
False
,
"username"
:
self
.
username
}
class
VerticalBlockTestCase
(
BaseVerticalBlockTest
):
"""
Tests for the VerticalBlock.
"""
def
assert_bookmark_info_in
(
self
,
content
):
"""
Assert content has all the bookmark info.
"""
self
.
assertIn
(
'bookmark_id'
,
content
)
self
.
assertIn
(
'{},{}'
.
format
(
self
.
username
,
unicode
(
self
.
vertical
.
location
)),
content
)
self
.
assertIn
(
'bookmarked'
,
content
)
self
.
assertIn
(
'show_bookmark_button'
,
content
)
def
test_render_student_view
(
self
):
"""
Test the rendering of the student view.
"""
html
=
self
.
module_system
.
render
(
self
.
vertical
,
STUDENT_VIEW
,
{}
)
.
content
html
=
self
.
module_system
.
render
(
self
.
vertical
,
STUDENT_VIEW
,
self
.
default_context
)
.
content
self
.
assertIn
(
self
.
test_html_1
,
html
)
self
.
assertIn
(
self
.
test_html_2
,
html
)
self
.
assert_bookmark_info_in
(
html
)
def
test_render_studio_view
(
self
):
"""
...
...
common/lib/xmodule/xmodule/vertical_block.py
View file @
3cbbb8f3
...
...
@@ -54,7 +54,14 @@ class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParse
fragment
.
add_content
(
self
.
system
.
render_template
(
'vert_module.html'
,
{
'items'
:
contents
,
'xblock_context'
:
context
,
'show_bookmark_button'
:
True
,
'bookmarked'
:
child_context
[
'bookmarked'
],
'bookmark_id'
:
"{},{}"
.
format
(
child_context
[
'username'
],
unicode
(
self
.
location
))
}))
fragment
.
add_javascript_url
(
self
.
runtime
.
local_resource_url
(
self
,
'public/js/vertical_student_view.js'
))
fragment
.
initialize_js
(
'VerticalStudentView'
)
return
fragment
def
author_view
(
self
,
context
):
...
...
common/test/acceptance/pages/lms/bookmarks.py
View file @
3cbbb8f3
...
...
@@ -7,7 +7,7 @@ from .course_page import CoursePage
class
BookmarksPage
(
CoursePage
):
"""
Coursware Bookmarks Page.
Cours
e
ware Bookmarks Page.
"""
url
=
None
url_path
=
"courseware/"
...
...
@@ -23,10 +23,11 @@ class BookmarksPage(CoursePage):
""" Check if bookmarks button is visible """
return
self
.
q
(
css
=
self
.
BOOKMARKS_BUTTON_SELECTOR
)
.
visible
def
click_bookmarks_button
(
self
):
def
click_bookmarks_button
(
self
,
wait_for_results
=
True
):
""" Click on Bookmarks button """
self
.
q
(
css
=
self
.
BOOKMARKS_BUTTON_SELECTOR
)
.
first
.
click
()
EmptyPromise
(
self
.
results_present
,
"Bookmarks results present"
)
.
fulfill
()
if
wait_for_results
:
EmptyPromise
(
self
.
results_present
,
"Bookmarks results present"
)
.
fulfill
()
def
results_present
(
self
):
""" Check if bookmarks results are present """
...
...
@@ -53,9 +54,9 @@ class BookmarksPage(CoursePage):
breadcrumbs
=
self
.
q
(
css
=
self
.
BOOKMARKED_BREADCRUMBS
)
.
text
return
[
breadcrumb
.
replace
(
'
\n
'
,
''
)
.
split
(
'-'
)
for
breadcrumb
in
breadcrumbs
]
def
click_bookmark
(
self
,
index
):
def
click_bookmark
ed_block
(
self
,
index
):
"""
Click on bookmark at index `index`
Click on bookmark
ed block
at index `index`
Arguments:
index (int): bookmark index in the list
...
...
common/test/acceptance/pages/lms/course_nav.py
View file @
3cbbb8f3
...
...
@@ -193,13 +193,13 @@ class CourseNavPage(PageObject):
)
# Regular expression to remove HTML span tags from a string
REMOVE_SPAN_TAG_RE
=
re
.
compile
(
r'<
span.+/span>
'
)
REMOVE_SPAN_TAG_RE
=
re
.
compile
(
r'<
/span>(.+)<span
'
)
def
_clean_seq_titles
(
self
,
element
):
"""
Clean HTML of sequence titles, stripping out span tags and returning the first line.
"""
return
self
.
REMOVE_SPAN_TAG_RE
.
s
ub
(
''
,
element
.
get_attribute
(
'innerHTML'
))
.
strip
()
.
split
(
'
\n
'
)[
0
]
return
self
.
REMOVE_SPAN_TAG_RE
.
s
earch
(
element
.
get_attribute
(
'innerHTML'
))
.
groups
()[
0
]
.
strip
()
def
go_to_sequential_position
(
self
,
sequential_position
):
"""
...
...
common/test/acceptance/pages/lms/courseware.py
View file @
3cbbb8f3
...
...
@@ -3,6 +3,7 @@ Courseware page.
"""
from
.course_page
import
CoursePage
from
bok_choy.promise
import
EmptyPromise
from
selenium.webdriver.common.action_chains
import
ActionChains
...
...
@@ -177,6 +178,32 @@ class CoursewarePage(CoursePage):
attribute_value
=
lambda
el
:
el
.
get_attribute
(
'data-id'
)
return
self
.
q
(
css
=
'#sequence-list a'
)
.
filter
(
get_active
)
.
map
(
attribute_value
)
.
results
[
0
]
@property
def
breadcrumb
(
self
):
""" Return the course tree breadcrumb shown above the sequential bar """
return
[
part
.
strip
()
for
part
in
self
.
q
(
css
=
'.path'
)
.
text
[
0
]
.
split
(
'>'
)]
def
bookmark_button_visible
(
self
):
""" Check if bookmark button is visible """
EmptyPromise
(
lambda
:
self
.
q
(
css
=
'.bookmark-button'
)
.
visible
,
"Bookmark button visible"
)
.
fulfill
()
return
True
@property
def
bookmark_button_state
(
self
):
""" Return `bookmarked` if button is in bookmarked state else '' """
return
'bookmarked'
if
self
.
q
(
css
=
'.bookmark-button.bookmarked'
)
.
present
else
''
@property
def
bookmark_icon_visible
(
self
):
""" Check if bookmark icon is visible on active sequence nav item """
return
self
.
q
(
css
=
'.active .bookmark-icon'
)
.
visible
def
click_bookmark_unit_button
(
self
):
""" Bookmark a unit by clicking on Bookmark button """
previous_state
=
self
.
bookmark_button_state
self
.
q
(
css
=
'.bookmark-button'
)
.
first
.
click
()
EmptyPromise
(
lambda
:
self
.
bookmark_button_state
!=
previous_state
,
"Bookmark button toggled"
)
.
fulfill
()
class
CoursewareSequentialTabPage
(
CoursePage
):
"""
...
...
common/test/acceptance/tests/lms/test_bookmarks.py
View file @
3cbbb8f3
...
...
@@ -2,17 +2,15 @@
"""
End-to-end tests for the courseware unit bookmarks.
"""
import
json
import
requests
from
...pages.studio.auto_auth
import
AutoAuthPage
from
...pages.lms.bookmarks
import
BookmarksPage
from
...pages.lms.courseware
import
CoursewarePage
from
...pages.lms.course_nav
import
CourseNavPage
from
...pages.studio.overview
import
CourseOutlinePage
from
...pages.common.logout
import
LogoutPage
from
...fixtures.course
import
CourseFixture
,
XBlockFixtureDesc
from
...fixtures
import
LMS_BASE_URL
from
..helpers
import
EventsTestMixin
,
UniqueCourseTest
,
is_404_page
...
...
@@ -22,30 +20,29 @@ class BookmarksTestMixin(EventsTestMixin, UniqueCourseTest):
"""
USERNAME
=
"STUDENT"
EMAIL
=
"student@example.com"
COURSE_TREE_INFO
=
[
[
'TestSection1'
,
'TestSubsection1'
,
'TestProblem1'
],
[
'TestSection2'
,
'TestSubsection2'
,
'TestProblem2'
]
]
def
create_course_fixture
(
self
):
""" Create course fixture """
def
create_course_fixture
(
self
,
num_chapters
):
"""
Create course fixture
Arguments:
num_chapters: number of chapters to create
"""
self
.
course_fixture
=
CourseFixture
(
# pylint: disable=attribute-defined-outside-init
self
.
course_info
[
'org'
],
self
.
course_info
[
'number'
],
self
.
course_info
[
'run'
],
self
.
course_info
[
'display_name'
]
)
self
.
course_fixture
.
add_children
(
XBlockFixtureDesc
(
'chapter'
,
self
.
COURSE_TREE_INFO
[
0
][
0
])
.
add_children
(
XBlockFixtureDesc
(
'sequential'
,
self
.
COURSE_TREE_INFO
[
0
][
1
])
.
add_children
(
XBlockFixtureDesc
(
'problem'
,
self
.
COURSE_TREE_INFO
[
0
][
2
])
xblocks
=
[]
for
index
in
range
(
num_chapters
):
xblocks
+=
[
XBlockFixtureDesc
(
'chapter'
,
'TestSection{}'
.
format
(
index
))
.
add_children
(
XBlockFixtureDesc
(
'sequential'
,
'TestSubsection{}'
.
format
(
index
))
.
add_children
(
XBlockFixtureDesc
(
'vertical'
,
'TestVertical{}'
.
format
(
index
))
)
)
),
XBlockFixtureDesc
(
'chapter'
,
self
.
COURSE_TREE_INFO
[
1
][
0
])
.
add_children
(
XBlockFixtureDesc
(
'sequential'
,
self
.
COURSE_TREE_INFO
[
1
][
1
])
.
add_children
(
XBlockFixtureDesc
(
'problem'
,
self
.
COURSE_TREE_INFO
[
1
][
2
])
)
)
)
.
install
()
]
self
.
course_fixture
.
add_children
(
*
xblocks
)
.
install
()
class
BookmarksTest
(
BookmarksTestMixin
):
...
...
@@ -66,35 +63,64 @@ class BookmarksTest(BookmarksTestMixin):
self
.
course_info
[
'run'
]
)
self
.
create_course_fixture
()
self
.
courseware_page
=
CoursewarePage
(
self
.
browser
,
self
.
course_id
)
self
.
bookmarks_page
=
BookmarksPage
(
self
.
browser
,
self
.
course_id
)
self
.
course_nav
=
CourseNavPage
(
self
.
browser
)
def
_test_setup
(
self
,
num_chapters
=
2
):
"""
Setup test settings.
Arguments:
num_chapters: number of chapters to create in course
"""
self
.
create_course_fixture
(
num_chapters
)
# Auto-auth register for the course.
AutoAuthPage
(
self
.
browser
,
username
=
self
.
USERNAME
,
email
=
self
.
EMAIL
,
course_id
=
self
.
course_id
)
.
visit
()
self
.
courseware_page
=
CoursewarePage
(
self
.
browser
,
self
.
course_id
)
self
.
courseware_page
.
visit
()
self
.
bookmarks
=
BookmarksPage
(
self
.
browser
,
self
.
course_id
)
# Use auto-auth to retrieve the session for a logged in user
self
.
session
=
requests
.
Session
()
response
=
self
.
session
.
get
(
LMS_BASE_URL
+
"/auto_auth?username=STUDENT&email=student@example.com"
)
self
.
assertTrue
(
response
.
ok
,
"Failed to get session info"
)
def
_bookmark_unit
(
self
,
index
):
"""
Bookmark a unit
Arguments:
index: unit index to bookmark
"""
self
.
course_nav
.
go_to_section
(
'TestSection{}'
.
format
(
index
),
'TestSubsection{}'
.
format
(
index
))
self
.
courseware_page
.
click_bookmark_unit_button
()
def
_bookmark_units
(
self
,
num_units
):
"""
Bookmark first `num_units` units by visiting them
def
_bookmark_unit
(
self
,
course_id
,
usage_id
):
""" Bookmark a single unit """
csrftoken
=
self
.
session
.
cookies
[
'csrftoken'
]
headers
=
{
'Content-type'
:
'application/json'
,
"X-CSRFToken"
:
csrftoken
}
url
=
LMS_BASE_URL
+
"/api/bookmarks/v0/bookmarks/?course_id="
+
course_id
+
'&fields=path'
data
=
json
.
dumps
({
'usage_id'
:
usage_id
})
Arguments:
num_units(int): Number of units to bookmarks
"""
for
index
in
range
(
num_units
):
self
.
_bookmark_unit
(
index
)
def
_breadcrumb
(
self
,
num_units
):
"""
Creates breadcrumbs for the first `num_units`
response
=
self
.
session
.
post
(
url
,
data
=
data
,
headers
=
headers
,
cookies
=
self
.
session
.
cookies
)
response
=
json
.
loads
(
response
.
text
)
self
.
assertTrue
(
response
[
'usage_id'
]
==
usage_id
,
"Failed to bookmark unit"
)
Arguments:
num_units(int): Number of units for which we want to create breadcrumbs
def
_bookmarks_blocks
(
self
,
xblocks
):
""" Bookmark all units in a course """
for
xblock
in
xblocks
:
self
.
_bookmark_unit
(
self
.
course_id
,
usage_id
=
xblock
.
locator
)
Returns:
list of breadcrumbs
"""
breadcrumbs
=
[]
for
index
in
range
(
num_units
):
breadcrumbs
.
append
(
[
'TestSection{}'
.
format
(
index
),
'TestSubsection{}'
.
format
(
index
),
'TestVertical{}'
.
format
(
index
)
]
)
return
breadcrumbs
def
_delete_section
(
self
,
index
):
""" Delete a section at index `index` """
...
...
@@ -119,6 +145,39 @@ class BookmarksTest(BookmarksTestMixin):
self
.
courseware_page
.
visit
()
self
.
courseware_page
.
wait_for_page
()
def
_toggle_bookmark_and_verify
(
self
,
bookmark_icon_state
,
bookmark_button_state
,
bookmarked_count
):
"""
Bookmark/Un-Bookmark a unit and then verify
"""
self
.
assertTrue
(
self
.
courseware_page
.
bookmark_button_visible
)
self
.
courseware_page
.
click_bookmark_unit_button
()
self
.
assertEqual
(
self
.
courseware_page
.
bookmark_icon_visible
,
bookmark_icon_state
)
self
.
assertEqual
(
self
.
courseware_page
.
bookmark_button_state
,
bookmark_button_state
)
self
.
bookmarks_page
.
click_bookmarks_button
()
self
.
assertEqual
(
self
.
bookmarks_page
.
count
(),
bookmarked_count
)
def
test_bookmark_button
(
self
):
"""
Scenario: Bookmark unit button toggles correctly
Given that I am a registered user
And I visit my courseware page
For first 2 units
I visit the unit
And I can see the Bookmark button
When I click on Bookmark button
Then unit should be bookmarked
Then I click again on the bookmark button
And I should see a unit un-bookmarked
"""
self
.
_test_setup
()
for
index
in
range
(
2
):
self
.
course_nav
.
go_to_section
(
'TestSection{}'
.
format
(
index
),
'TestSubsection{}'
.
format
(
index
))
self
.
_toggle_bookmark_and_verify
(
True
,
'bookmarked'
,
1
)
self
.
bookmarks_page
.
click_bookmarks_button
(
False
)
self
.
_toggle_bookmark_and_verify
(
False
,
''
,
0
)
def
test_empty_bookmarks_list
(
self
):
"""
Scenario: An empty bookmarks list is shown if there are no bookmarked units.
...
...
@@ -130,15 +189,16 @@ class BookmarksTest(BookmarksTestMixin):
Then I should see an empty bookmarks list
And empty bookmarks list content is correct
"""
self
.
assertTrue
(
self
.
bookmarks
.
bookmarks_button_visible
())
self
.
bookmarks
.
click_bookmarks_button
()
self
.
assertEqual
(
self
.
bookmarks
.
results_header_text
(),
'MY BOOKMARKS'
)
self
.
assertEqual
(
self
.
bookmarks
.
empty_header_text
(),
'You have not bookmarked any courseware pages yet.'
)
self
.
_test_setup
()
self
.
assertTrue
(
self
.
bookmarks_page
.
bookmarks_button_visible
())
self
.
bookmarks_page
.
click_bookmarks_button
()
self
.
assertEqual
(
self
.
bookmarks_page
.
results_header_text
(),
'MY BOOKMARKS'
)
self
.
assertEqual
(
self
.
bookmarks_page
.
empty_header_text
(),
'You have not bookmarked any courseware pages yet.'
)
empty_list_text
=
(
"Use bookmarks to help you easily return to courseware pages. To bookmark a page, "
"select Bookmark in the upper right corner of that page. To see a list of all your "
"bookmarks, select Bookmarks in the upper left corner of any courseware page."
)
self
.
assertEqual
(
self
.
bookmarks
.
empty_list_text
(),
empty_list_text
)
self
.
assertEqual
(
self
.
bookmarks
_page
.
empty_list_text
(),
empty_list_text
)
def
test_bookmarks_list
(
self
):
"""
...
...
@@ -160,27 +220,30 @@ class BookmarksTest(BookmarksTestMixin):
# discarded by the current version of MySQL we are using due to the
# lack of support. Due to which order of bookmarked units will be
# incorrect.
xblocks
=
self
.
course_fixture
.
get_nested_xblocks
(
category
=
"problem"
)
self
.
_bookmark
s_blocks
(
xblocks
)
self
.
_test_setup
(
)
self
.
_bookmark
_units
(
2
)
self
.
bookmarks
.
click_bookmarks_button
()
self
.
assertTrue
(
self
.
bookmarks
.
results_present
())
self
.
assertEqual
(
self
.
bookmarks
.
results_header_text
(),
'MY BOOKMARKS'
)
self
.
assertEqual
(
self
.
bookmarks
.
count
(),
2
)
self
.
bookmarks
_page
.
click_bookmarks_button
()
self
.
assertTrue
(
self
.
bookmarks
_page
.
results_present
())
self
.
assertEqual
(
self
.
bookmarks
_page
.
results_header_text
(),
'MY BOOKMARKS'
)
self
.
assertEqual
(
self
.
bookmarks
_page
.
count
(),
2
)
bookmarked_breadcrumbs
=
self
.
bookmarks
.
breadcrumbs
()
bookmarked_breadcrumbs
=
self
.
bookmarks
_page
.
breadcrumbs
()
# Verify bookmarked breadcrumbs
self
.
assertItemsEqual
(
bookmarked_breadcrumbs
,
self
.
COURSE_TREE_INFO
)
breadcrumbs
=
self
.
_breadcrumb
(
2
)
self
.
assertItemsEqual
(
bookmarked_breadcrumbs
,
breadcrumbs
)
# get usage ids for units
xblocks
=
self
.
course_fixture
.
get_nested_xblocks
(
category
=
"vertical"
)
xblock_usage_ids
=
[
xblock
.
locator
for
xblock
in
xblocks
]
# Verify link navigation
for
index
in
range
(
2
):
self
.
bookmarks
.
click_bookmar
k
(
index
)
self
.
bookmarks
_page
.
click_bookmarked_bloc
k
(
index
)
self
.
courseware_page
.
wait_for_page
()
self
.
assertTrue
(
self
.
courseware_page
.
active_usage_id
()
in
xblock_usage_ids
)
self
.
courseware_page
.
visit
()
.
wait_for_page
()
self
.
bookmarks
.
click_bookmarks_button
()
self
.
bookmarks
_page
.
click_bookmarks_button
()
def
test_unreachable_bookmark
(
self
):
"""
...
...
@@ -195,13 +258,34 @@ class BookmarksTest(BookmarksTestMixin):
When I click on deleted bookmark
Then I should navigated to 404 page
"""
self
.
_
bookmarks_blocks
(
self
.
course_fixture
.
get_nested_xblocks
(
category
=
"problem"
)
)
self
.
_
test_setup
(
)
self
.
_bookmark_units
(
2
)
self
.
_delete_section
(
0
)
self
.
bookmarks
.
click_bookmarks_button
()
self
.
assertTrue
(
self
.
bookmarks
.
results_present
())
self
.
assertEqual
(
self
.
bookmarks
.
count
(),
2
)
self
.
bookmarks
_page
.
click_bookmarks_button
()
self
.
assertTrue
(
self
.
bookmarks
_page
.
results_present
())
self
.
assertEqual
(
self
.
bookmarks
_page
.
count
(),
2
)
self
.
bookmarks
.
click_bookmark
(
0
)
self
.
bookmarks
_page
.
click_bookmarked_block
(
1
)
self
.
assertTrue
(
is_404_page
(
self
.
browser
))
def
test_page_size_limit
(
self
):
"""
Scenario: We can get more bookmarks if page size is greater than default page size.
Note:
* Current Bookmarks API page_size value is 10.
* page_size value in bookmarks client side is set to 500.
Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the units available
Then I click on Bookmarks button
And I should see a bookmarked list
And bookmark list contains 11 bookmarked items
"""
self
.
_test_setup
(
11
)
self
.
_bookmark_units
(
11
)
self
.
bookmarks_page
.
click_bookmarks_button
()
self
.
assertTrue
(
self
.
bookmarks_page
.
results_present
())
self
.
assertEqual
(
self
.
bookmarks_page
.
count
(),
11
)
common/test/acceptance/tests/lms/test_lms_courseware.py
View file @
3cbbb8f3
...
...
@@ -29,6 +29,7 @@ class CoursewareTest(UniqueCourseTest):
super
(
CoursewareTest
,
self
)
.
setUp
()
self
.
courseware_page
=
CoursewarePage
(
self
.
browser
,
self
.
course_id
)
self
.
course_nav
=
CourseNavPage
(
self
.
browser
)
self
.
course_outline
=
CourseOutlinePage
(
self
.
browser
,
...
...
@@ -38,12 +39,12 @@ class CoursewareTest(UniqueCourseTest):
)
# Install a course with sections/problems, tabs, updates, and handouts
course_fix
=
CourseFixture
(
self
.
course_fix
=
CourseFixture
(
self
.
course_info
[
'org'
],
self
.
course_info
[
'number'
],
self
.
course_info
[
'run'
],
self
.
course_info
[
'display_name'
]
)
course_fix
.
add_children
(
self
.
course_fix
.
add_children
(
XBlockFixtureDesc
(
'chapter'
,
'Test Section 1'
)
.
add_children
(
XBlockFixtureDesc
(
'sequential'
,
'Test Subsection 1'
)
.
add_children
(
XBlockFixtureDesc
(
'problem'
,
'Test Problem 1'
)
...
...
@@ -67,6 +68,18 @@ class CoursewareTest(UniqueCourseTest):
self
.
problem_page
=
ProblemPage
(
self
.
browser
)
self
.
assertEqual
(
self
.
problem_page
.
problem_name
,
'TEST PROBLEM 1'
)
def
_change_problem_release_date_in_studio
(
self
):
"""
"""
self
.
course_outline
.
q
(
css
=
".subsection-header-actions .configure-button"
)
.
first
.
click
()
self
.
course_outline
.
q
(
css
=
"#start_date"
)
.
fill
(
"01/01/2030"
)
self
.
course_outline
.
q
(
css
=
".action-save"
)
.
first
.
click
()
def
_create_breadcrumb
(
self
,
index
):
""" Create breadcrumb """
return
[
'Test Section {}'
.
format
(
index
),
'Test Subsection {}'
.
format
(
index
),
'Test Problem {}'
.
format
(
index
)]
def
_auto_auth
(
self
,
username
,
email
,
staff
):
"""
Logout and login with given credentials.
...
...
@@ -92,6 +105,9 @@ class CoursewareTest(UniqueCourseTest):
# Set release date for subsection in future.
self
.
course_outline
.
change_problem_release_date_in_studio
()
# Wait for 2 seconds to save new date.
time
.
sleep
(
2
)
# Logout and login as a student.
LogoutPage
(
self
.
browser
)
.
visit
()
self
.
_auto_auth
(
self
.
USERNAME
,
self
.
EMAIL
,
False
)
...
...
@@ -246,6 +262,23 @@ class ProctoredExamTest(UniqueCourseTest):
self
.
courseware_page
.
start_timed_exam
()
self
.
assertTrue
(
self
.
courseware_page
.
is_timer_bar_present
)
def
test_course_tree_breadcrumb
(
self
):
"""
Scenario: Correct course tree breadcrumb is shown.
Given that I am a registered user
And I visit my courseware page
Then I should see correct course tree breadcrumb
"""
self
.
courseware_page
.
visit
()
xblocks
=
self
.
course_fix
.
get_nested_xblocks
(
category
=
"problem"
)
for
index
in
range
(
1
,
len
(
xblocks
)
+
1
):
self
.
course_nav
.
go_to_section
(
'Test Section {}'
.
format
(
index
),
'Test Subsection {}'
.
format
(
index
))
courseware_page_breadcrumb
=
self
.
courseware_page
.
breadcrumb
expected_breadcrumb
=
self
.
_create_breadcrumb
(
index
)
self
.
assertEqual
(
courseware_page_breadcrumb
,
expected_breadcrumb
)
def
test_time_allotted_field_is_not_visible_with_none_exam
(
self
):
"""
Given that I am a staff member
...
...
lms/djangoapps/bookmarks/tests/test_views.py
View file @
3cbbb8f3
...
...
@@ -145,8 +145,8 @@ class BookmarksViewTestsMixin(ModuleStoreTestCase):
class
BookmarksListViewTests
(
BookmarksViewTestsMixin
):
"""
This contains the tests for GET & POST methods of bookmark.views.BookmarksListView class
GET /api/bookmarks/v
0
/bookmarks/?course_id={course_id1}
POST /api/bookmarks/v
0
/bookmarks
GET /api/bookmarks/v
1
/bookmarks/?course_id={course_id1}
POST /api/bookmarks/v
1
/bookmarks
"""
@ddt.data
(
(
'course_id={}'
,
False
),
...
...
lms/djangoapps/courseware/module_render.py
View file @
3cbbb8f3
...
...
@@ -42,6 +42,7 @@ from courseware.entrance_exams import (
)
from
edxmako.shortcuts
import
render_to_string
from
eventtracking
import
tracker
from
lms.djangoapps.bookmarks.services
import
BookmarksService
from
lms.djangoapps.lms_xblock.field_data
import
LmsFieldData
from
lms.djangoapps.lms_xblock.runtime
import
LmsModuleSystem
,
unquote_slashes
,
quote_slashes
from
lms.djangoapps.lms_xblock.models
import
XBlockAsidesConfig
...
...
@@ -715,6 +716,8 @@ def get_module_system_for_user(user, student_data, # TODO # pylint: disable=to
"reverification"
:
ReverificationService
(),
'proctoring'
:
ProctoringService
(),
'credit'
:
CreditService
(),
'reverification'
:
ReverificationService
(),
'bookmarks'
:
BookmarksService
(
user
=
user
),
},
get_user_role
=
lambda
:
get_user_role
(
user
,
course_id
),
descriptor_runtime
=
descriptor
.
_runtime
,
# pylint: disable=protected-access
...
...
lms/djangoapps/courseware/tests/test_module_render.py
View file @
3cbbb8f3
...
...
@@ -73,6 +73,7 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
@XBlock.needs
(
"i18n"
)
@XBlock.needs
(
"fs"
)
@XBlock.needs
(
"user"
)
@XBlock.needs
(
"bookmarks"
)
class
PureXBlock
(
XBlock
):
"""
Pure XBlock to use in tests.
...
...
@@ -1232,6 +1233,7 @@ class ViewInStudioTest(ModuleStoreTestCase):
self
.
request
.
user
=
self
.
staff_user
self
.
request
.
session
=
{}
self
.
module
=
None
self
.
default_context
=
{
'bookmarked'
:
False
,
'username'
:
self
.
user
.
username
}
def
_get_module
(
self
,
course_id
,
descriptor
,
location
):
"""
...
...
@@ -1290,14 +1292,14 @@ class MongoViewInStudioTest(ViewInStudioTest):
def
test_view_in_studio_link_studio_course
(
self
):
"""Regular Studio courses should see 'View in Studio' links."""
self
.
setup_mongo_course
()
result_fragment
=
self
.
module
.
render
(
STUDENT_VIEW
)
result_fragment
=
self
.
module
.
render
(
STUDENT_VIEW
,
context
=
self
.
default_context
)
self
.
assertIn
(
'View Unit in Studio'
,
result_fragment
.
content
)
def
test_view_in_studio_link_only_in_top_level_vertical
(
self
):
"""Regular Studio courses should not see 'View in Studio' for child verticals of verticals."""
self
.
setup_mongo_course
()
# Render the parent vertical, then check that there is only a single "View Unit in Studio" link.
result_fragment
=
self
.
module
.
render
(
STUDENT_VIEW
)
result_fragment
=
self
.
module
.
render
(
STUDENT_VIEW
,
context
=
self
.
default_context
)
# The single "View Unit in Studio" link should appear before the first xmodule vertical definition.
parts
=
result_fragment
.
content
.
split
(
'data-block-type="vertical"'
)
self
.
assertEqual
(
3
,
len
(
parts
),
"Did not find two vertical blocks"
)
...
...
@@ -1308,7 +1310,7 @@ class MongoViewInStudioTest(ViewInStudioTest):
def
test_view_in_studio_link_xml_authored
(
self
):
"""Courses that change 'course_edit_method' setting can hide 'View in Studio' links."""
self
.
setup_mongo_course
(
course_edit_method
=
'XML'
)
result_fragment
=
self
.
module
.
render
(
STUDENT_VIEW
)
result_fragment
=
self
.
module
.
render
(
STUDENT_VIEW
,
context
=
self
.
default_context
)
self
.
assertNotIn
(
'View Unit in Studio'
,
result_fragment
.
content
)
...
...
@@ -1321,19 +1323,19 @@ class MixedViewInStudioTest(ViewInStudioTest):
def
test_view_in_studio_link_mongo_backed
(
self
):
"""Mixed mongo courses that are mongo backed should see 'View in Studio' links."""
self
.
setup_mongo_course
()
result_fragment
=
self
.
module
.
render
(
STUDENT_VIEW
)
result_fragment
=
self
.
module
.
render
(
STUDENT_VIEW
,
context
=
self
.
default_context
)
self
.
assertIn
(
'View Unit in Studio'
,
result_fragment
.
content
)
def
test_view_in_studio_link_xml_authored
(
self
):
"""Courses that change 'course_edit_method' setting can hide 'View in Studio' links."""
self
.
setup_mongo_course
(
course_edit_method
=
'XML'
)
result_fragment
=
self
.
module
.
render
(
STUDENT_VIEW
)
result_fragment
=
self
.
module
.
render
(
STUDENT_VIEW
,
context
=
self
.
default_context
)
self
.
assertNotIn
(
'View Unit in Studio'
,
result_fragment
.
content
)
def
test_view_in_studio_link_xml_backed
(
self
):
"""Course in XML only modulestore should not see 'View in Studio' links."""
self
.
setup_xml_course
()
result_fragment
=
self
.
module
.
render
(
STUDENT_VIEW
)
result_fragment
=
self
.
module
.
render
(
STUDENT_VIEW
,
context
=
self
.
default_context
)
self
.
assertNotIn
(
'View Unit in Studio'
,
result_fragment
.
content
)
...
...
@@ -1826,7 +1828,7 @@ class LMSXBlockServiceBindingTest(ModuleStoreTestCase):
self
.
request_token
=
Mock
()
@XBlock.register_temp_plugin
(
PureXBlock
,
identifier
=
'pure'
)
@ddt.data
(
"user"
,
"i18n"
,
"fs"
,
"field-data"
)
@ddt.data
(
"user"
,
"i18n"
,
"fs"
,
"field-data"
,
"bookmarks"
)
def
test_expected_services_exist
(
self
,
expected_service
):
"""
Tests that the 'user', 'i18n', and 'fs' services are provided by the LMS runtime.
...
...
lms/djangoapps/courseware/tests/test_split_module.py
View file @
3cbbb8f3
...
...
@@ -119,7 +119,7 @@ class SplitTestBase(ModuleStoreTestCase):
content
=
resp
.
content
# Assert we see the proper icon in the top display
self
.
assertIn
(
'<a class="{} inactive progress-0"'
.
format
(
self
.
ICON_CLASSES
[
user_tag
]),
content
)
self
.
assertIn
(
'<a class="{} inactive progress-0
nav-item
"'
.
format
(
self
.
ICON_CLASSES
[
user_tag
]),
content
)
# And proper tooltips
for
tooltip
in
self
.
TOOLTIPS
[
user_tag
]:
self
.
assertIn
(
tooltip
,
content
)
...
...
lms/djangoapps/courseware/views.py
View file @
3cbbb8f3
...
...
@@ -403,6 +403,8 @@ def _index_bulk_op(request, course_key, chapter, section, position):
if
survey
.
utils
.
must_answer_survey
(
course
,
user
):
return
redirect
(
reverse
(
'course_survey'
,
args
=
[
unicode
(
course
.
id
)]))
bookmarks_api_url
=
reverse
(
'bookmarks'
)
try
:
field_data_cache
=
FieldDataCache
.
cache_for_descriptor_descendents
(
course_key
,
user
,
course
,
depth
=
2
)
...
...
@@ -428,7 +430,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
'studio_url'
:
studio_url
,
'masquerade'
:
masquerade
,
'xqa_server'
:
settings
.
FEATURES
.
get
(
'XQA_SERVER'
,
"http://your_xqa_server.com"
),
'
reverifications'
:
fetch_reverify_banner_info
(
request
,
course_key
)
,
'
bookmarks_api_url'
:
bookmarks_api_url
,
'language_preference'
:
language_preference
,
}
...
...
lms/static/js/bookmarks/collections/bookmarks.js
View file @
3cbbb8f3
...
...
@@ -5,8 +5,11 @@
'use strict'
;
return
Backbone
.
Collection
.
extend
({
model
:
BookmarkModel
,
url
:
'/api/bookmarks/v0/bookmarks/'
,
model
:
BookmarkModel
,
url
:
function
()
{
return
$
(
".courseware-bookmarks-button"
).
data
(
'bookmarksApiUrl'
);
},
parse
:
function
(
response
)
{
return
response
.
results
;
...
...
lms/static/js/bookmarks/main.js
View file @
3cbbb8f3
RequireJS
.
require
([
'js/bookmarks/views/bookmarks_button'
],
function
(
BookmarksButton
)
{
'js/bookmarks/views/bookmarks_
list_
button'
],
function
(
Bookmarks
List
Button
)
{
'use strict'
;
return
new
BookmarksButton
();
return
new
Bookmarks
List
Button
();
});
lms/static/js/bookmarks/views/bookmark_button.js
0 → 100644
View file @
3cbbb8f3
;(
function
(
define
,
undefined
)
{
'use strict'
;
define
([
'gettext'
,
'jquery'
,
'underscore'
,
'backbone'
,
'js/views/message'
],
function
(
gettext
,
$
,
_
,
Backbone
,
MessageView
)
{
return
Backbone
.
View
.
extend
({
errorIcon
:
'<i class="fa fa-fw fa-exclamation-triangle message-error" aria-hidden="true"></i>'
,
errorMessage
:
gettext
(
'An error has occurred. Please try again.'
),
srAddBookmarkText
:
gettext
(
'Click to add'
),
srRemoveBookmarkText
:
gettext
(
'Click to remove'
),
events
:
{
'click'
:
'toggleBookmark'
},
initialize
:
function
(
options
)
{
this
.
apiUrl
=
options
.
apiUrl
;
this
.
bookmarkId
=
options
.
bookmarkId
;
this
.
bookmarked
=
options
.
bookmarked
;
this
.
usageId
=
options
.
usageId
;
this
.
setBookmarkState
(
this
.
bookmarked
);
},
toggleBookmark
:
function
(
event
)
{
event
.
preventDefault
();
if
(
this
.
$el
.
hasClass
(
'bookmarked'
))
{
this
.
removeBookmark
();
}
else
{
this
.
addBookmark
();
}
},
addBookmark
:
function
()
{
var
view
=
this
;
$
.
ajax
({
data
:
{
usage_id
:
view
.
usageId
},
type
:
"POST"
,
url
:
view
.
apiUrl
,
dataType
:
'json'
,
success
:
function
()
{
view
.
$el
.
trigger
(
'bookmark:add'
);
view
.
setBookmarkState
(
true
);
},
error
:
function
()
{
view
.
showError
();
}
});
},
removeBookmark
:
function
()
{
var
view
=
this
;
var
deleteUrl
=
view
.
apiUrl
+
view
.
bookmarkId
+
'/'
;
$
.
ajax
({
type
:
"DELETE"
,
url
:
deleteUrl
,
success
:
function
()
{
view
.
$el
.
trigger
(
'bookmark:remove'
);
view
.
setBookmarkState
(
false
);
},
error
:
function
()
{
view
.
showError
();
}
});
},
setBookmarkState
:
function
(
bookmarked
)
{
if
(
bookmarked
)
{
this
.
$el
.
addClass
(
'bookmarked'
);
this
.
$el
.
attr
(
'aria-pressed'
,
'true'
);
this
.
$el
.
find
(
'.bookmark-sr'
).
text
(
this
.
srRemoveBookmarkText
);
}
else
{
this
.
$el
.
removeClass
(
'bookmarked'
);
this
.
$el
.
attr
(
'aria-pressed'
,
'false'
);
this
.
$el
.
find
(
'.bookmark-sr'
).
text
(
this
.
srAddBookmarkText
);
}
},
showError
:
function
()
{
if
(
!
this
.
messageView
)
{
this
.
messageView
=
new
MessageView
({
el
:
$
(
'.coursewide-message-banner'
),
templateId
:
'#message_banner-tpl'
});
}
this
.
messageView
.
showMessage
(
this
.
errorMessage
,
this
.
errorIcon
);
}
});
});
}).
call
(
this
,
define
||
RequireJS
.
define
);
lms/static/js/bookmarks/views/bookmarks_list.js
View file @
3cbbb8f3
...
...
@@ -16,6 +16,8 @@
errorMessage
:
gettext
(
'An error has occurred. Please try again.'
),
loadingMessage
:
gettext
(
'Loading'
),
PAGE_SIZE
:
500
,
events
:
{
'click .bookmarks-results-list-item'
:
'visitBookmark'
},
...
...
@@ -48,7 +50,7 @@
this
.
collection
.
fetch
({
reset
:
true
,
data
:
{
course_id
:
this
.
courseId
,
fields
:
'display_name,path'
}
data
:
{
course_id
:
this
.
courseId
,
page_size
:
this
.
PAGE_SIZE
,
fields
:
'display_name,path'
}
}).
done
(
function
()
{
view
.
hideLoadingMessage
();
view
.
render
();
...
...
lms/static/js/bookmarks/views/bookmarks_button.js
→
lms/static/js/bookmarks/views/bookmarks_
list_
button.js
View file @
3cbbb8f3
...
...
@@ -16,8 +16,6 @@
},
initialize
:
function
()
{
this
.
template
=
_
.
template
(
$
(
'#bookmarks_button-tpl'
).
text
());
this
.
bookmarksListView
=
new
BookmarksListView
({
collection
:
new
BookmarksCollection
(),
loadingMessageView
:
new
MessageView
({
el
:
$
(
this
.
loadingMessageElement
)}),
...
...
lms/static/js/fixtures/bookmarks/bookmark_button.html
0 → 100644
View file @
3cbbb8f3
<div
class=
"coursewide-message-banner"
aria-live=
"polite"
></div>
<div
class=
"xblock xblock-student_view xblock-student_view-vertical xblock-initialized"
>
<div
class=
"bookmark-button-wrapper"
>
<button
class=
"btn bookmark-button"
aria-pressed=
"false"
data-bookmark-id=
"bilbo,usage_1"
>
<span
class=
"sr bookmark-sr"
></span>
Bookmark
</button>
</div>
</div>
lms/static/js/fixtures/bookmarks/bookmarks.html
View file @
3cbbb8f3
<div
class=
"courseware-bookmarks-button"
>
<div
class=
"courseware-bookmarks-button"
data-bookmarks-api-url=
"/api/bookmarks/v1/bookmarks/"
>
<button
type=
"button"
class=
"bookmarks-list-button is-inactive"
aria-pressed=
"false"
>
Bookmarks
</button>
...
...
lms/static/js/spec/bookmarks/bookmark_button_view_spec.js
0 → 100644
View file @
3cbbb8f3
define
([
'backbone'
,
'jquery'
,
'underscore'
,
'js/common_helpers/ajax_helpers'
,
'js/common_helpers/template_helpers'
,
'js/bookmarks/views/bookmark_button'
],
function
(
Backbone
,
$
,
_
,
AjaxHelpers
,
TemplateHelpers
,
BookmarkButtonView
)
{
'use strict'
;
describe
(
"bookmarks.button"
,
function
()
{
var
API_URL
=
'bookmarks/api/v1/bookmarks/'
;
beforeEach
(
function
()
{
loadFixtures
(
'js/fixtures/bookmarks/bookmark_button.html'
);
TemplateHelpers
.
installTemplates
(
[
'templates/fields/message_banner'
]
);
});
var
createBookmarkButtonView
=
function
(
isBookmarked
)
{
return
new
BookmarkButtonView
({
el
:
'.bookmark-button'
,
bookmarked
:
isBookmarked
,
bookmarkId
:
'bilbo,usage_1'
,
usageId
:
'usage_1'
,
apiUrl
:
API_URL
});
};
var
verifyBookmarkButtonState
=
function
(
view
,
bookmarked
)
{
if
(
bookmarked
)
{
expect
(
view
.
$el
).
toHaveAttr
(
'aria-pressed'
,
'true'
);
expect
(
view
.
$el
).
toHaveClass
(
'bookmarked'
);
expect
(
view
.
$el
.
find
(
'.bookmark-sr'
).
text
()).
toBe
(
'Click to remove'
);
}
else
{
expect
(
view
.
$el
).
toHaveAttr
(
'aria-pressed'
,
'false'
);
expect
(
view
.
$el
).
not
.
toHaveClass
(
'bookmarked'
);
expect
(
view
.
$el
.
find
(
'.bookmark-sr'
).
text
()).
toBe
(
'Click to add'
);
}
expect
(
view
.
$el
.
data
(
'bookmarkId'
)).
toBe
(
'bilbo,usage_1'
);
};
it
(
"rendered correctly "
,
function
()
{
var
view
=
createBookmarkButtonView
(
false
);
verifyBookmarkButtonState
(
view
,
false
);
// with bookmarked true
view
=
createBookmarkButtonView
(
true
);
verifyBookmarkButtonState
(
view
,
true
);
});
it
(
"bookmark/un-bookmark the block correctly"
,
function
()
{
var
addBookmarkedData
=
{
bookmarked
:
true
,
handler
:
'removeBookmark'
,
event
:
'bookmark:remove'
,
method
:
'DELETE'
,
url
:
API_URL
+
'bilbo,usage_1/'
,
body
:
null
};
var
removeBookmarkData
=
{
bookmarked
:
false
,
handler
:
'addBookmark'
,
event
:
'bookmark:add'
,
method
:
'POST'
,
url
:
API_URL
,
body
:
'usage_id=usage_1'
};
var
requests
=
AjaxHelpers
.
requests
(
this
);
_
.
each
([[
addBookmarkedData
,
removeBookmarkData
],
[
removeBookmarkData
,
addBookmarkedData
]],
function
(
actionsData
)
{
var
firstActionData
=
actionsData
[
0
];
var
secondActionData
=
actionsData
[
1
];
var
bookmarkButtonView
=
createBookmarkButtonView
(
firstActionData
.
bookmarked
);
verifyBookmarkButtonState
(
bookmarkButtonView
,
firstActionData
.
bookmarked
);
spyOn
(
bookmarkButtonView
,
firstActionData
.
handler
).
andCallThrough
();
spyOnEvent
(
bookmarkButtonView
.
$el
,
firstActionData
.
event
);
bookmarkButtonView
.
$el
.
click
();
AjaxHelpers
.
expectRequest
(
requests
,
firstActionData
.
method
,
firstActionData
.
url
,
firstActionData
.
body
);
expect
(
bookmarkButtonView
[
firstActionData
.
handler
]).
toHaveBeenCalled
();
AjaxHelpers
.
respondWithJson
(
requests
,
{});
expect
(
firstActionData
.
event
).
toHaveBeenTriggeredOn
(
bookmarkButtonView
.
$el
);
bookmarkButtonView
[
firstActionData
.
handler
].
reset
();
verifyBookmarkButtonState
(
bookmarkButtonView
,
secondActionData
.
bookmarked
);
spyOn
(
bookmarkButtonView
,
secondActionData
.
handler
).
andCallThrough
();
spyOnEvent
(
bookmarkButtonView
.
$el
,
secondActionData
.
event
);
bookmarkButtonView
.
$el
.
click
();
AjaxHelpers
.
expectRequest
(
requests
,
secondActionData
.
method
,
secondActionData
.
url
,
secondActionData
.
body
);
expect
(
bookmarkButtonView
[
secondActionData
.
handler
]).
toHaveBeenCalled
();
AjaxHelpers
.
respondWithJson
(
requests
,
{});
expect
(
secondActionData
.
event
).
toHaveBeenTriggeredOn
(
bookmarkButtonView
.
$el
);
verifyBookmarkButtonState
(
bookmarkButtonView
,
firstActionData
.
bookmarked
);
});
});
it
(
"shows an error message for HTTP 500"
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
),
$messageBanner
=
$
(
'.coursewide-message-banner'
),
bookmarkButtonView
=
createBookmarkButtonView
(
false
);
bookmarkButtonView
.
$el
.
click
();
AjaxHelpers
.
respondWithError
(
requests
);
expect
(
$messageBanner
.
text
().
trim
()).
toBe
(
bookmarkButtonView
.
errorMessage
);
// For bookmarked button.
bookmarkButtonView
=
createBookmarkButtonView
(
true
);
bookmarkButtonView
.
$el
.
click
();
AjaxHelpers
.
respondWithError
(
requests
);
expect
(
$messageBanner
.
text
().
trim
()).
toBe
(
bookmarkButtonView
.
errorMessage
);
});
});
});
lms/static/js/spec/bookmarks/bookmarks_spec.js
→
lms/static/js/spec/bookmarks/bookmarks_
list_view_
spec.js
View file @
3cbbb8f3
define
([
'backbone'
,
'jquery'
,
'underscore'
,
'js/common_helpers/ajax_helpers'
,
'js/common_helpers/template_helpers'
,
'js/bookmarks/views/bookmarks_button'
'js/bookmarks/views/bookmarks_
list_
button'
],
function
(
Backbone
,
$
,
_
,
AjaxHelpers
,
TemplateHelpers
,
BookmarksButtonView
)
{
function
(
Backbone
,
$
,
_
,
AjaxHelpers
,
TemplateHelpers
,
Bookmarks
List
ButtonView
)
{
'use strict'
;
describe
(
"lms.courseware.bookmarks"
,
function
()
{
var
bookmarksButtonView
;
var
BOOKMARKS_API_URL
=
'/api/bookmarks/v
0
/bookmarks/'
;
var
BOOKMARKS_API_URL
=
'/api/bookmarks/v
1
/bookmarks/'
;
beforeEach
(
function
()
{
loadFixtures
(
'js/fixtures/bookmarks/bookmarks.html'
);
...
...
@@ -18,7 +18,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
]
);
bookmarksButtonView
=
new
BookmarksButtonView
();
bookmarksButtonView
=
new
Bookmarks
List
ButtonView
();
this
.
addMatchers
({
toHaveBeenCalledWithUrl
:
function
(
expectedUrl
)
{
...
...
@@ -124,7 +124,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
it
(
"has rendered bookmarked list correctly"
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
);
var
url
=
BOOKMARKS_API_URL
+
'?course_id=COURSE_ID&fields=display_name%2Cpath'
;
var
url
=
BOOKMARKS_API_URL
+
'?course_id=COURSE_ID&
page_size=500&
fields=display_name%2Cpath'
;
var
expectedData
=
createBookmarksData
(
3
);
spyOn
(
bookmarksButtonView
.
bookmarksListView
,
'courseId'
).
andReturn
(
'COURSE_ID'
);
...
...
lms/static/js/spec/main.js
View file @
3cbbb8f3
...
...
@@ -87,6 +87,13 @@
// Discussion classes loaded explicitly until they are converted to use RequireJS
'DiscussionModuleView'
:
'xmodule_js/common_static/coffee/src/discussion/discussion_module_view'
,
'js/bookmarks/collections/bookmarks'
:
'js/bookmarks/collections/bookmarks'
,
'js/bookmarks/models/bookmark'
:
'js/bookmarks/models/bookmark'
,
'js/bookmarks/views/bookmarks_list_button'
:
'js/bookmarks/views/bookmarks_list_button'
,
'js/bookmarks/views/bookmarks_list'
:
'js/bookmarks/views/bookmarks_list'
,
'js/bookmarks/views/bookmark_button'
:
'js/bookmarks/views/bookmark_button'
,
'js/views/message'
:
'js/views/message'
,
// edxnotes
'annotator_1.2.9'
:
'xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min'
,
...
...
@@ -738,7 +745,8 @@
'lms/include/teams/js/spec/views/team_join_spec.js'
'lms/include/js/spec/discovery/discovery_spec.js'
,
'lms/include/js/spec/ccx/schedule_spec.js'
,
'lms/include/js/spec/bookmarks/bookmarks_spec.js'
'lms/include/js/spec/bookmarks/bookmarks_list_view_spec.js'
,
'lms/include/js/spec/bookmarks/bookmark_button_view_spec.js'
]);
}).
call
(
this
,
requirejs
,
define
);
lms/static/sass/course/courseware/_courseware.scss
View file @
3cbbb8f3
...
...
@@ -403,6 +403,10 @@ div.course-wrapper {
}
}
.sequence
.path
{
margin-bottom
:
(
$baseline
/
2
);
}
div
#seq_content
{
h1
{
background
:
none
;
...
...
lms/static/sass/views/_bookmarks.scss
View file @
3cbbb8f3
...
...
@@ -67,8 +67,8 @@
margin-bottom
:
$baseline
;
&
:hover
{
border-color
:
$
link-color
;
color
:
$
link-color
;
border-color
:
$
m-blue
;
color
:
$
m-blue
;
}
}
...
...
@@ -87,6 +87,7 @@
position
:
relative
;
top
:
-7px
;
font-family
:
FontAwesome
;
color
:
$m-blue
;
}
.list-item-content
{
...
...
@@ -124,4 +125,59 @@
.bookmarks-empty-detail
{
@extend
%t-copy-sub1
;
}
\ No newline at end of file
}
// Rules for bookmark icon shown on each sequence nav item
i
.bookmarked
{
top
:
-3px
;
position
:
absolute
;
left
:
(
$baseline
/
4
);
}
// Rules for bookmark button's different styles
.bookmark-button-wrapper
{
text-align
:
right
;
margin-bottom
:
10px
;
}
@mixin
base-style
(
$border-color
,
$content-color
)
{
background
:
none
;
border
:
1px
solid
$border-color
;
border-radius
:
(
$baseline
/
4
);
color
:
$content-color
;
&
:focus
,
&
:active
{
box-shadow
:
none
;
}
}
@mixin
icon-style
(
$content
,
$color
)
{
&
:before
{
content
:
$content
;
font-family
:
FontAwesome
;
color
:
$color
;
}
}
@mixin
hover-style
(
$border-color
,
$content-color
,
$icon-content
)
{
&
:hover
{
background
:
none
;
border
:
1px
solid
$border-color
;
color
:
$content-color
;
@include
icon-style
(
$icon-content
,
$content-color
);
}
}
.bookmark-button.bookmarked
{
@include
base-style
(
$m-blue
,
$m-blue
);
@include
icon-style
(
"\f02e"
,
$m-blue
);
@include
hover-style
(
$light-gray
,
$black
,
"\f097"
);
}
.bookmark-button
:not
(
.bookmarked
)
{
@include
base-style
(
$light-gray
,
$black
);
@include
icon-style
(
"\f097"
,
$black
);
@include
hover-style
(
$m-blue
,
$m-blue
,
"\f02e"
);
}
lms/templates/bookmark_button.html
0 → 100644
View file @
3cbbb8f3
<
%!
from
django
.
utils
.
translation
import
ugettext
as
_
%
>
<
%
page
args=
"bookmark_id, is_bookmarked"
/>
<div
class=
"bookmark-button-wrapper"
>
<button
class=
"btn bookmark-button ${"
bookmarked
"
if
is_bookmarked
else
""}"
aria-pressed=
"${"
true
"
if
is_bookmarked
else
"
false
"}"
data-bookmark-id=
"${bookmark_id}"
>
<span
class=
"sr bookmark-sr"
>
${_("Click to remove") if is_bookmarked else _("Click to add")}
</span>
${_("Bookmark")}
</button>
</div>
lms/templates/courseware/courseware.html
View file @
3cbbb8f3
...
...
@@ -24,6 +24,13 @@ ${page_title_breadcrumbs(course_name())}
</title></
%
block>
<
%
block
name=
"header_extras"
>
% for template_name in ["message_banner"]:
<script
type=
"text/template"
id=
"${template_name}-tpl"
>
<%
static
:
include
path
=
"fields/${template_name}.underscore"
/>
</script>
% endfor
% for template_name in ["image-modal"]:
<script
type=
"text/template"
id=
"${template_name}-tpl"
>
<%
static
:
include
path
=
"common/templates/${template_name}.underscore"
/>
...
...
@@ -124,6 +131,8 @@ ${fragment.foot_html()}
</
%
block>
<div
class=
"coursewide-message-banner"
aria-live=
"polite"
></div>
% if default_tab:
<
%
include
file=
"/courseware/course_navigation.html"
/>
% else:
...
...
@@ -149,7 +158,7 @@ ${fragment.foot_html()}
<div
class=
"wrapper-course-modes"
>
<div
class=
"courseware-bookmarks-button"
>
<div
class=
"courseware-bookmarks-button"
data-bookmarks-api-url=
"${bookmarks_api_url}"
>
<button
type=
"button"
class=
"bookmarks-list-button is-inactive"
aria-pressed=
"false"
>
${_('Bookmarks')}
</button>
...
...
lms/templates/seq_module.html
View file @
3cbbb8f3
<
%!
from
django
.
utils
.
translation
import
ugettext
as
_
%
>
<div
id=
"sequence_${element_id}"
class=
"sequence"
data-id=
"${item_id}"
data-position=
"${position}"
data-ajax-url=
"${ajax_url}"
>
<div
class=
"path"
></div>
<div
class=
"sequence-nav"
>
<button
class=
"sequence-nav-button button-previous"
>
<span
class=
"icon fa fa-chevron-prev"
aria-hidden=
"true"
></span><span
class=
"sr"
>
${_('Previous')}
</span>
...
...
@@ -13,16 +14,18 @@
## implementation note: will need to figure out how to handle combining detail
## statuses of multiple modules in js.
<li>
<a
class=
"seq_${item['type']} inactive progress-${item['progress_status']}"
<a
class=
"seq_${item['type']} inactive progress-${item['progress_status']}
nav-item
"
data-id=
"${item['id']}"
data-element=
"${idx+1}"
href=
"javascript:void(0);"
data-page-title=
"${item['page_title']|h}"
data-path=
"${item['path']}"
aria-controls=
"seq_contents_${idx}"
id=
"tab_${idx}"
tabindex=
"0"
>
<i
class=
"icon fa seq_${item['type']}"
aria-hidden=
"true"
></i>
<p><span
class=
"sr"
>
${item['type']}
</span>
${item['title']}
</p>
<i
class=
"fa fa-fw fa-bookmark bookmark-icon ${"
is-hidden
"
if
not
item
['
bookmarked
']
else
"
bookmarked
"}"
aria-hidden=
"true"
></i>
<p><span
class=
"sr"
>
${item['type']}
</span>
${item['title']}
<span
class=
"sr bookmark-icon-sr"
>
${_("Bookmarked") if item['bookmarked'] else ""}
</span></p>
</a>
</li>
% endfor
...
...
lms/templates/vert_module.html
View file @
3cbbb8f3
% if show_bookmark_button:
<
%
include
file=
'bookmark_button.html'
args=
"bookmark_id=bookmark_id, is_bookmarked=bookmarked"
/>
% endif
<div
class=
"vert-mod"
>
% for idx, item in enumerate(items):
<div
class=
"vert vert-${idx}"
data-id=
"${item['id']}"
>
...
...
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