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
770a45b7
Commit
770a45b7
authored
Mar 10, 2016
by
Muzaffar yousaf
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #11363 from edx/notes-pagination
Notes pagination
parents
656913c7
e1a13a14
Hide whitespace changes
Inline
Side-by-side
Showing
41 changed files
with
2055 additions
and
591 deletions
+2055
-591
common/djangoapps/terrain/stubs/edxnotes.py
+61
-10
common/djangoapps/terrain/stubs/http.py
+3
-0
common/djangoapps/terrain/stubs/tests/test_edxnotes.py
+169
-13
common/test/acceptance/pages/common/paging.py
+5
-0
common/test/acceptance/pages/lms/edxnotes.py
+7
-2
common/test/acceptance/tests/lms/test_lms_edxnotes.py
+397
-4
lms/djangoapps/edxnotes/helpers.py
+144
-91
lms/djangoapps/edxnotes/plugins.py
+17
-2
lms/djangoapps/edxnotes/tests.py
+328
-186
lms/djangoapps/edxnotes/urls.py
+1
-1
lms/djangoapps/edxnotes/views.py
+98
-18
lms/envs/bok_choy.py
+2
-0
lms/envs/common.py
+3
-0
lms/envs/test.py
+2
-0
lms/static/js/dashboard/track_events.js
+5
-0
lms/static/js/edxnotes/collections/notes.js
+28
-22
lms/static/js/edxnotes/models/note.js
+0
-4
lms/static/js/edxnotes/plugins/store_error_handler.js
+33
-0
lms/static/js/edxnotes/views/note_item.js
+4
-3
lms/static/js/edxnotes/views/notes_factory.js
+2
-1
lms/static/js/edxnotes/views/notes_page.js
+25
-18
lms/static/js/edxnotes/views/page_factory.js
+13
-3
lms/static/js/edxnotes/views/search_box.js
+19
-16
lms/static/js/edxnotes/views/tab_panel.js
+19
-2
lms/static/js/edxnotes/views/tab_view.js
+9
-2
lms/static/js/edxnotes/views/tabs/search_results.js
+5
-3
lms/static/js/spec/edxnotes/collections/notes_spec.js
+13
-2
lms/static/js/spec/edxnotes/helpers.js
+139
-54
lms/static/js/spec/edxnotes/models/note_spec.js
+18
-5
lms/static/js/spec/edxnotes/plugins/store_error_handler_spec.js
+36
-0
lms/static/js/spec/edxnotes/views/note_item_spec.js
+36
-6
lms/static/js/spec/edxnotes/views/notes_page_spec.js
+8
-3
lms/static/js/spec/edxnotes/views/search_box_spec.js
+29
-17
lms/static/js/spec/edxnotes/views/tabs/course_structure_spec.js
+1
-1
lms/static/js/spec/edxnotes/views/tabs/recent_activity_spec.js
+168
-40
lms/static/js/spec/edxnotes/views/tabs/search_results_spec.js
+156
-41
lms/static/js/spec/edxnotes/views/tabs/tags_spec.js
+1
-1
lms/static/js/spec/main.js
+1
-0
lms/static/sass/course/_student-notes.scss
+23
-10
lms/templates/edxnotes/edxnotes.html
+14
-7
lms/templates/edxnotes/note-item.underscore
+13
-3
No files found.
common/djangoapps/terrain/stubs/edxnotes.py
View file @
770a45b7
...
...
@@ -7,11 +7,12 @@ import re
from
uuid
import
uuid4
from
datetime
import
datetime
from
copy
import
deepcopy
from
math
import
ceil
from
urllib
import
urlencode
from
.http
import
StubHttpRequestHandler
,
StubHttpService
# pylint: disable=invalid-name
class
StubEdxNotesServiceHandler
(
StubHttpRequestHandler
):
"""
Handler for EdxNotes requests.
...
...
@@ -165,7 +166,7 @@ class StubEdxNotesServiceHandler(StubHttpRequestHandler):
"""
Return the note by note id.
"""
notes
=
self
.
server
.
get_notes
()
notes
=
self
.
server
.
get_
all_
notes
()
result
=
self
.
server
.
filter_by_id
(
notes
,
note_id
)
if
result
:
self
.
respond
(
content
=
result
[
0
])
...
...
@@ -191,6 +192,53 @@ class StubEdxNotesServiceHandler(StubHttpRequestHandler):
else
:
self
.
respond
(
404
,
"404 Not Found"
)
@staticmethod
def
_get_next_prev_url
(
url_path
,
query_params
,
page_num
,
page_size
):
"""
makes url with the query params including pagination params
for pagination next and previous urls
"""
query_params
=
deepcopy
(
query_params
)
query_params
.
update
({
"page"
:
page_num
,
"page_size"
:
page_size
})
return
url_path
+
"?"
+
urlencode
(
query_params
)
def
_get_paginated_response
(
self
,
notes
,
page_num
,
page_size
):
"""
Returns a paginated response of notes.
"""
start
=
(
page_num
-
1
)
*
page_size
end
=
start
+
page_size
total_notes
=
len
(
notes
)
url_path
=
"http://{server_address}:{port}{path}"
.
format
(
server_address
=
self
.
client_address
[
0
],
port
=
self
.
server
.
port
,
path
=
self
.
path_only
)
next_url
=
None
if
end
>=
total_notes
else
self
.
_get_next_prev_url
(
url_path
,
self
.
get_params
,
page_num
+
1
,
page_size
)
prev_url
=
None
if
page_num
==
1
else
self
.
_get_next_prev_url
(
url_path
,
self
.
get_params
,
page_num
-
1
,
page_size
)
# Get notes from range
notes
=
deepcopy
(
notes
[
start
:
end
])
paginated_response
=
{
'total'
:
total_notes
,
'num_pages'
:
int
(
ceil
(
float
(
total_notes
)
/
page_size
)),
'current_page'
:
page_num
,
'rows'
:
notes
,
'next'
:
next_url
,
'start'
:
start
,
'previous'
:
prev_url
}
return
paginated_response
def
_search
(
self
):
"""
Search for a notes by user id, course_id and usage_id.
...
...
@@ -199,32 +247,35 @@ class StubEdxNotesServiceHandler(StubHttpRequestHandler):
usage_id
=
self
.
get_params
.
get
(
"usage_id"
,
None
)
course_id
=
self
.
get_params
.
get
(
"course_id"
,
None
)
text
=
self
.
get_params
.
get
(
"text"
,
None
)
page
=
int
(
self
.
get_params
.
get
(
"page"
,
1
))
page_size
=
int
(
self
.
get_params
.
get
(
"page_size"
,
2
))
if
user
is
None
:
self
.
respond
(
400
,
"Bad Request"
)
return
notes
=
self
.
server
.
get_notes
()
notes
=
self
.
server
.
get_
all_
notes
()
if
course_id
is
not
None
:
notes
=
self
.
server
.
filter_by_course_id
(
notes
,
course_id
)
if
usage_id
is
not
None
:
notes
=
self
.
server
.
filter_by_usage_id
(
notes
,
usage_id
)
if
text
:
notes
=
self
.
server
.
search
(
notes
,
text
)
self
.
respond
(
content
=
{
"total"
:
len
(
notes
),
"rows"
:
notes
,
})
self
.
respond
(
content
=
self
.
_get_paginated_response
(
notes
,
page
,
page_size
))
def
_collection
(
self
):
"""
Return all notes for the user.
"""
user
=
self
.
get_params
.
get
(
"user"
,
None
)
page
=
int
(
self
.
get_params
.
get
(
"page"
,
1
))
page_size
=
int
(
self
.
get_params
.
get
(
"page_size"
,
2
))
notes
=
self
.
server
.
get_all_notes
()
if
user
is
None
:
self
.
send_response
(
400
,
content
=
"Bad Request"
)
return
notes
=
self
.
server
.
get_notes
(
)
notes
=
self
.
_get_paginated_response
(
notes
,
page
,
page_size
)
self
.
respond
(
content
=
notes
)
def
_cleanup
(
self
):
...
...
@@ -245,9 +296,9 @@ class StubEdxNotesService(StubHttpService):
super
(
StubEdxNotesService
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
self
.
notes
=
list
()
def
get_notes
(
self
):
def
get_
all_
notes
(
self
):
"""
Returns a list of all notes
.
Returns a list of all notes
without pagination
"""
notes
=
deepcopy
(
self
.
notes
)
notes
.
reverse
()
...
...
common/djangoapps/terrain/stubs/http.py
View file @
770a45b7
...
...
@@ -3,6 +3,7 @@ Stub implementation of an HTTP service.
"""
from
BaseHTTPServer
import
HTTPServer
,
BaseHTTPRequestHandler
import
urllib
import
urlparse
import
threading
import
json
...
...
@@ -217,6 +218,8 @@ class StubHttpRequestHandler(BaseHTTPRequestHandler, object):
`format_str` is a string with old-style Python format escaping;
`args` is an array of values to fill into the string.
"""
if
not
args
:
format_str
=
urllib
.
unquote
(
format_str
)
return
u"{0} - - [{1}] {2}
\n
"
.
format
(
self
.
client_address
[
0
],
self
.
log_date_time_string
(),
...
...
common/djangoapps/terrain/stubs/tests/test_edxnotes.py
View file @
770a45b7
"""
Unit tests for stub EdxNotes implementation.
"""
import
urlparse
import
json
import
unittest
import
requests
...
...
@@ -19,7 +19,7 @@ class StubEdxNotesServiceTest(unittest.TestCase):
"""
super
(
StubEdxNotesServiceTest
,
self
)
.
setUp
()
self
.
server
=
StubEdxNotesService
()
dummy_notes
=
self
.
_get_dummy_notes
(
count
=
2
)
dummy_notes
=
self
.
_get_dummy_notes
(
count
=
5
)
self
.
server
.
add_notes
(
dummy_notes
)
self
.
addCleanup
(
self
.
server
.
shutdown
)
...
...
@@ -99,17 +99,48 @@ class StubEdxNotesServiceTest(unittest.TestCase):
self
.
assertEqual
(
response
.
status_code
,
404
)
def
test_search
(
self
):
# Without user
response
=
requests
.
get
(
self
.
_get_url
(
"api/v1/search"
))
self
.
assertEqual
(
response
.
status_code
,
400
)
# get response with default page and page size
response
=
requests
.
get
(
self
.
_get_url
(
"api/v1/search"
),
params
=
{
"user"
:
"dummy-user-id"
,
"usage_id"
:
"dummy-usage-id"
,
"course_id"
:
"dummy-course-id"
,
})
notes
=
self
.
_get_notes
()
self
.
assertTrue
(
response
.
ok
)
self
.
assertDictEqual
({
"total"
:
2
,
"rows"
:
notes
},
response
.
json
())
self
.
_verify_pagination_info
(
response
=
response
.
json
(),
total_notes
=
5
,
num_pages
=
3
,
notes_per_page
=
2
,
start
=
0
,
current_page
=
1
,
next_page
=
2
,
previous_page
=
None
)
response
=
requests
.
get
(
self
.
_get_url
(
"api/v1/search"
))
self
.
assertEqual
(
response
.
status_code
,
400
)
# search notes with text that don't exist
response
=
requests
.
get
(
self
.
_get_url
(
"api/v1/search"
),
params
=
{
"user"
:
"dummy-user-id"
,
"usage_id"
:
"dummy-usage-id"
,
"course_id"
:
"dummy-course-id"
,
"text"
:
"world war 2"
})
self
.
assertTrue
(
response
.
ok
)
self
.
_verify_pagination_info
(
response
=
response
.
json
(),
total_notes
=
0
,
num_pages
=
0
,
notes_per_page
=
0
,
start
=
0
,
current_page
=
1
,
next_page
=
None
,
previous_page
=
None
)
def
test_delete
(
self
):
notes
=
self
.
_get_notes
()
...
...
@@ -119,7 +150,7 @@ class StubEdxNotesServiceTest(unittest.TestCase):
for
note
in
notes
:
response
=
requests
.
delete
(
self
.
_get_url
(
"api/v1/annotations/"
+
note
[
"id"
]))
self
.
assertEqual
(
response
.
status_code
,
204
)
remaining_notes
=
self
.
server
.
get_notes
()
remaining_notes
=
self
.
server
.
get_
all_
notes
()
self
.
assertNotIn
(
note
[
"id"
],
[
note
[
"id"
]
for
note
in
remaining_notes
])
self
.
assertEqual
(
len
(
remaining_notes
),
0
)
...
...
@@ -139,24 +170,149 @@ class StubEdxNotesServiceTest(unittest.TestCase):
response
=
requests
.
get
(
self
.
_get_url
(
"api/v1/annotations/does_not_exist"
))
self
.
assertEqual
(
response
.
status_code
,
404
)
# pylint: disable=too-many-arguments
def
_verify_pagination_info
(
self
,
response
,
total_notes
,
num_pages
,
notes_per_page
,
current_page
,
previous_page
,
next_page
,
start
):
"""
Verify the pagination information.
Argument:
response: response from api
total_notes: total notes in the response
num_pages: total number of pages in response
notes_per_page: number of notes in the response
current_page: current page number
previous_page: previous page number
next_page: next page number
start: start of the current page
"""
def
get_page_value
(
url
):
"""
Return page value extracted from url.
"""
if
url
is
None
:
return
None
parsed
=
urlparse
.
urlparse
(
url
)
query_params
=
urlparse
.
parse_qs
(
parsed
.
query
)
page
=
query_params
[
"page"
][
0
]
return
page
if
page
is
None
else
int
(
page
)
self
.
assertEqual
(
response
[
"total"
],
total_notes
)
self
.
assertEqual
(
response
[
"num_pages"
],
num_pages
)
self
.
assertEqual
(
len
(
response
[
"rows"
]),
notes_per_page
)
self
.
assertEqual
(
response
[
"current_page"
],
current_page
)
self
.
assertEqual
(
get_page_value
(
response
[
"previous"
]),
previous_page
)
self
.
assertEqual
(
get_page_value
(
response
[
"next"
]),
next_page
)
self
.
assertEqual
(
response
[
"start"
],
start
)
def
test_notes_collection
(
self
):
response
=
requests
.
get
(
self
.
_get_url
(
"api/v1/annotations"
),
params
=
{
"user"
:
"dummy-user-id"
})
self
.
assertTrue
(
response
.
ok
)
self
.
assertEqual
(
len
(
response
.
json
()),
2
)
"""
Test paginated response of notes api
"""
# Without user
response
=
requests
.
get
(
self
.
_get_url
(
"api/v1/annotations"
))
self
.
assertEqual
(
response
.
status_code
,
400
)
# Without any pagination parameters
response
=
requests
.
get
(
self
.
_get_url
(
"api/v1/annotations"
),
params
=
{
"user"
:
"dummy-user-id"
})
self
.
assertTrue
(
response
.
ok
)
self
.
_verify_pagination_info
(
response
=
response
.
json
(),
total_notes
=
5
,
num_pages
=
3
,
notes_per_page
=
2
,
start
=
0
,
current_page
=
1
,
next_page
=
2
,
previous_page
=
None
)
# With pagination parameters
response
=
requests
.
get
(
self
.
_get_url
(
"api/v1/annotations"
),
params
=
{
"user"
:
"dummy-user-id"
,
"page"
:
2
,
"page_size"
:
3
})
self
.
assertTrue
(
response
.
ok
)
self
.
_verify_pagination_info
(
response
=
response
.
json
(),
total_notes
=
5
,
num_pages
=
2
,
notes_per_page
=
2
,
start
=
3
,
current_page
=
2
,
next_page
=
None
,
previous_page
=
1
)
def
test_notes_collection_next_previous_with_one_page
(
self
):
"""
Test next and previous urls of paginated response of notes api
when number of pages are 1
"""
response
=
requests
.
get
(
self
.
_get_url
(
"api/v1/annotations"
),
params
=
{
"user"
:
"dummy-user-id"
,
"page_size"
:
10
})
self
.
assertTrue
(
response
.
ok
)
self
.
_verify_pagination_info
(
response
=
response
.
json
(),
total_notes
=
5
,
num_pages
=
1
,
notes_per_page
=
5
,
start
=
0
,
current_page
=
1
,
next_page
=
None
,
previous_page
=
None
)
def
test_notes_collection_when_no_notes
(
self
):
"""
Test paginated response of notes api when there's no note present
"""
# Delete all notes
self
.
test_cleanup
()
# Get default page
response
=
requests
.
get
(
self
.
_get_url
(
"api/v1/annotations"
),
params
=
{
"user"
:
"dummy-user-id"
})
self
.
assertTrue
(
response
.
ok
)
self
.
_verify_pagination_info
(
response
=
response
.
json
(),
total_notes
=
0
,
num_pages
=
0
,
notes_per_page
=
0
,
start
=
0
,
current_page
=
1
,
next_page
=
None
,
previous_page
=
None
)
def
test_cleanup
(
self
):
response
=
requests
.
put
(
self
.
_get_url
(
"cleanup"
))
self
.
assertTrue
(
response
.
ok
)
self
.
assertEqual
(
len
(
self
.
server
.
get_notes
()),
0
)
self
.
assertEqual
(
len
(
self
.
server
.
get_
all_
notes
()),
0
)
def
test_create_notes
(
self
):
dummy_notes
=
self
.
_get_dummy_notes
(
count
=
2
)
response
=
requests
.
post
(
self
.
_get_url
(
"create_notes"
),
data
=
json
.
dumps
(
dummy_notes
))
self
.
assertTrue
(
response
.
ok
)
self
.
assertEqual
(
len
(
self
.
_get_notes
()),
4
)
self
.
assertEqual
(
len
(
self
.
_get_notes
()),
7
)
response
=
requests
.
post
(
self
.
_get_url
(
"create_notes"
))
self
.
assertEqual
(
response
.
status_code
,
400
)
...
...
@@ -177,7 +333,7 @@ class StubEdxNotesServiceTest(unittest.TestCase):
"""
Return a list of notes from the stub EdxNotes service.
"""
notes
=
self
.
server
.
get_notes
()
notes
=
self
.
server
.
get_
all_
notes
()
self
.
assertGreater
(
len
(
notes
),
0
,
"Notes are empty."
)
return
notes
...
...
common/test/acceptance/pages/common/paging.py
View file @
770a45b7
...
...
@@ -63,3 +63,8 @@ class PaginatedUIMixin(object):
def
is_enabled
(
self
,
css
):
"""Return whether the given element is not disabled."""
return
'is-disabled'
not
in
self
.
q
(
css
=
css
)
.
attrs
(
'class'
)[
0
]
@property
def
footer_visible
(
self
):
""" Return True if footer is visible else False"""
return
self
.
q
(
css
=
'.pagination.bottom'
)
.
visible
common/test/acceptance/pages/lms/edxnotes.py
View file @
770a45b7
from
bok_choy.page_object
import
PageObject
,
PageLoadError
,
unguarded
from
bok_choy.promise
import
BrokenPromise
,
EmptyPromise
from
.course_page
import
CoursePage
from
..common.paging
import
PaginatedUIMixin
from
...tests.helpers
import
disable_animations
from
selenium.webdriver.common.action_chains
import
ActionChains
...
...
@@ -114,7 +115,7 @@ class EdxNotesPageItem(NoteChild):
"""
BODY_SELECTOR
=
".note"
UNIT_LINK_SELECTOR
=
"a.reference-unit-link"
TAG_SELECTOR
=
"
a
.reference-tags"
TAG_SELECTOR
=
"
span
.reference-tags"
def
go_to_unit
(
self
,
unit_page
=
None
):
self
.
q
(
css
=
self
.
_bounded_selector
(
self
.
UNIT_LINK_SELECTOR
))
.
click
()
...
...
@@ -242,7 +243,7 @@ class SearchResultsView(EdxNotesPageView):
TAB_SELECTOR
=
".tab#view-search-results"
class
EdxNotesPage
(
CoursePage
):
class
EdxNotesPage
(
CoursePage
,
PaginatedUIMixin
):
"""
EdxNotes page.
"""
...
...
@@ -348,6 +349,10 @@ class EdxNotesPage(CoursePage):
children
=
self
.
q
(
css
=
'.note-group'
)
return
[
EdxNotesTagsGroup
(
self
.
browser
,
child
.
get_attribute
(
"id"
))
for
child
in
children
]
def
count
(
self
):
""" Returns the total number of notes in the list """
return
len
(
self
.
q
(
css
=
'div.wrapper-note-excerpts'
)
.
results
)
class
EdxNotesPageNoContent
(
CoursePage
):
"""
...
...
common/test/acceptance/tests/lms/test_lms_edxnotes.py
View file @
770a45b7
"""
Test LMS Notes
"""
from
unittest
import
skip
import
random
from
uuid
import
uuid4
from
datetime
import
datetime
from
nose.plugins.attrib
import
attr
from
..helpers
import
UniqueCourseTest
from
..helpers
import
UniqueCourseTest
,
EventsTestMixin
from
...fixtures.course
import
CourseFixture
,
XBlockFixtureDesc
from
...pages.lms.auto_auth
import
AutoAuthPage
from
...pages.lms.course_nav
import
CourseNavPage
from
...pages.lms.courseware
import
CoursewarePage
from
...pages.lms.edxnotes
import
EdxNotesUnitPage
,
EdxNotesPage
,
EdxNotesPageNoContent
from
...fixtures.edxnotes
import
EdxNotesFixture
,
Note
,
Range
from
..helpers
import
EventsTestMixin
class
EdxNotesTestMixin
(
UniqueCourseTest
):
...
...
@@ -345,9 +346,10 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
self
.
edxnotes_fixture
.
create_notes
(
notes_list
)
self
.
edxnotes_fixture
.
install
()
def
_add_default_notes
(
self
,
tags
=
None
):
def
_add_default_notes
(
self
,
tags
=
None
,
extra_notes
=
0
):
"""
Creates 5 test notes. If tags are not specified, will populate the notes with some test tag data.
Creates 5 test notes by default & number of extra_notes will be created if specified.
If tags are not specified, will populate the notes with some test tag data.
If tags are specified, they will be used for each of the 3 notes that have tags.
"""
xblocks
=
self
.
course_fixture
.
get_nested_xblocks
(
category
=
"html"
)
...
...
@@ -398,6 +400,19 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
updated
=
datetime
(
2015
,
1
,
1
,
1
,
1
,
1
,
1
)
.
isoformat
()
),
]
if
extra_notes
>
0
:
for
__
in
range
(
extra_notes
):
self
.
raw_note_list
.
append
(
Note
(
usage_id
=
xblocks
[
random
.
choice
([
0
,
1
,
2
,
3
,
4
,
5
])]
.
locator
,
user
=
self
.
username
,
course_id
=
self
.
course_fixture
.
_course_key
,
# pylint: disable=protected-access
text
=
"Fourth note"
,
quote
=
""
,
updated
=
datetime
(
2014
,
1
,
1
,
1
,
1
,
1
,
1
)
.
isoformat
(),
tags
=
[
"review"
]
if
tags
is
None
else
tags
)
)
self
.
_add_notes
(
self
.
raw_note_list
)
def
assertNoteContent
(
self
,
item
,
text
=
None
,
quote
=
None
,
unit_name
=
None
,
time_updated
=
None
,
tags
=
None
):
...
...
@@ -469,6 +484,44 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
]
self
.
assert_events_match
(
expected_events
,
actual_events
)
def
_verify_pagination_info
(
self
,
notes_count_on_current_page
,
header_text
,
previous_button_enabled
,
next_button_enabled
,
current_page_number
,
total_pages
):
"""
Verify pagination info
"""
self
.
assertEqual
(
self
.
notes_page
.
count
(),
notes_count_on_current_page
)
self
.
assertEqual
(
self
.
notes_page
.
get_pagination_header_text
(),
header_text
)
if
total_pages
>
1
:
self
.
assertEqual
(
self
.
notes_page
.
footer_visible
,
True
)
self
.
assertEqual
(
self
.
notes_page
.
is_previous_page_button_enabled
(),
previous_button_enabled
)
self
.
assertEqual
(
self
.
notes_page
.
is_next_page_button_enabled
(),
next_button_enabled
)
self
.
assertEqual
(
self
.
notes_page
.
get_current_page_number
(),
current_page_number
)
self
.
assertEqual
(
self
.
notes_page
.
get_total_pages
,
total_pages
)
else
:
self
.
assertEqual
(
self
.
notes_page
.
footer_visible
,
False
)
def
search_and_verify
(
self
):
"""
Add, search and verify notes.
"""
self
.
_add_default_notes
(
extra_notes
=
22
)
self
.
notes_page
.
visit
()
# Run the search
self
.
notes_page
.
search
(
"note"
)
# No error message appears
self
.
assertFalse
(
self
.
notes_page
.
is_error_visible
)
self
.
assertIn
(
u"Search Results"
,
self
.
notes_page
.
tabs
)
self
.
assertEqual
(
self
.
notes_page
.
get_total_pages
,
2
)
def
test_no_content
(
self
):
"""
Scenario: User can see `No content` message.
...
...
@@ -482,6 +535,57 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
"You have not made any notes in this course yet. Other students in this course are using notes to:"
,
notes_page_empty
.
no_content_text
)
def
test_notes_works_correctly_with_xss
(
self
):
"""
Scenario: Note text & tags should be HTML and JS escaped
Given I am enrolled in a course with notes enabled
When I visit the Notes page, with a Notes text and tag containing HTML characters like < and >
Then the text and tags appear as expected due to having been properly escaped
"""
xblocks
=
self
.
course_fixture
.
get_nested_xblocks
(
category
=
"html"
)
self
.
_add_notes
([
Note
(
usage_id
=
xblocks
[
0
]
.
locator
,
user
=
self
.
username
,
course_id
=
self
.
course_fixture
.
_course_key
,
# pylint: disable=protected-access
text
=
'<script>alert("XSS")</script>'
,
quote
=
"quote"
,
updated
=
datetime
(
2014
,
1
,
1
,
1
,
1
,
1
,
1
)
.
isoformat
(),
tags
=
[
'<script>alert("XSS")</script>'
]
),
Note
(
usage_id
=
xblocks
[
1
]
.
locator
,
user
=
self
.
username
,
course_id
=
self
.
course_fixture
.
_course_key
,
# pylint: disable=protected-access
text
=
'<b>bold</b>'
,
quote
=
"quote"
,
updated
=
datetime
(
2014
,
2
,
1
,
1
,
1
,
1
,
1
)
.
isoformat
(),
tags
=
[
'<i>bold</i>'
]
)
])
self
.
notes_page
.
visit
()
notes
=
self
.
notes_page
.
notes
self
.
assertEqual
(
len
(
notes
),
2
)
self
.
assertNoteContent
(
notes
[
0
],
quote
=
u"quote"
,
text
=
'<b>bold</b>'
,
unit_name
=
"Test Unit 1"
,
time_updated
=
"Feb 01, 2014 at 01:01 UTC"
,
tags
=
[
'<i>bold</i>'
]
)
self
.
assertNoteContent
(
notes
[
1
],
quote
=
u"quote"
,
text
=
'<script>alert("XSS")</script>'
,
unit_name
=
"Test Unit 1"
,
time_updated
=
"Jan 01, 2014 at 01:01 UTC"
,
tags
=
[
'<script>alert("XSS")</script>'
]
)
def
test_recent_activity_view
(
self
):
"""
Scenario: User can view all notes by recent activity.
...
...
@@ -850,6 +954,7 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
self
.
assert_viewed_event
(
'Search Results'
)
self
.
assert_search_event
(
'note'
,
4
)
@skip
(
"scroll to tag functionality is disabled"
)
def
test_scroll_to_tag_recent_activity
(
self
):
"""
Scenario: Can scroll to a tag group from the Recent Activity view (default view)
...
...
@@ -861,6 +966,7 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
self
.
notes_page
.
visit
()
self
.
_scroll_to_tag_and_verify
(
"pear"
,
3
)
@skip
(
"scroll to tag functionality is disabled"
)
def
test_scroll_to_tag_course_structure
(
self
):
"""
Scenario: Can scroll to a tag group from the Course Structure view
...
...
@@ -872,6 +978,7 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
self
.
notes_page
.
visit
()
.
switch_to_tab
(
"structure"
)
self
.
_scroll_to_tag_and_verify
(
"squash"
,
5
)
@skip
(
"scroll to tag functionality is disabled"
)
def
test_scroll_to_tag_search
(
self
):
"""
Scenario: Can scroll to a tag group from the Search Results view
...
...
@@ -884,6 +991,7 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
self
.
notes_page
.
visit
()
.
search
(
"note"
)
self
.
_scroll_to_tag_and_verify
(
"pumpkin"
,
4
)
@skip
(
"scroll to tag functionality is disabled"
)
def
test_scroll_to_tag_from_tag_view
(
self
):
"""
Scenario: Can scroll to a tag group from the Tags view
...
...
@@ -1005,6 +1113,291 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
note
=
self
.
note_unit_page
.
notes
[
0
]
self
.
assertFalse
(
note
.
is_visible
)
def
test_page_size_limit
(
self
):
"""
Scenario: Verify that we can't get notes more than default page size.
Given that I am a registered user
And I have a course with 11 notes
When I open Notes page
Then I can see notes list contains 10 items
And I should see paging header and footer with correct data
And I should see disabled previous button
And I should also see enabled next button
"""
self
.
_add_default_notes
(
extra_notes
=
21
)
self
.
notes_page
.
visit
()
self
.
_verify_pagination_info
(
notes_count_on_current_page
=
25
,
header_text
=
'Showing 1-25 out of 26 total'
,
previous_button_enabled
=
False
,
next_button_enabled
=
True
,
current_page_number
=
1
,
total_pages
=
2
)
def
test_pagination_with_single_page
(
self
):
"""
Scenario: Notes list pagination works as expected for single page
Given that I am a registered user
And I have a course with 5 notes
When I open Notes page
Then I can see notes list contains 5 items
And I should see paging header and footer with correct data
And I should see disabled previous and next buttons
"""
self
.
_add_default_notes
()
self
.
notes_page
.
visit
()
self
.
_verify_pagination_info
(
notes_count_on_current_page
=
5
,
header_text
=
'Showing 1-5 out of 5 total'
,
previous_button_enabled
=
False
,
next_button_enabled
=
False
,
current_page_number
=
1
,
total_pages
=
1
)
def
test_next_and_previous_page_button
(
self
):
"""
Scenario: Next & Previous buttons are working as expected for notes list pagination
Given that I am a registered user
And I have a course with 26 notes
When I open Notes page
Then I can see notes list contains 25 items
And I should see paging header and footer with correct data
And I should see disabled previous button
And I should see enabled next button
When I click on next page button in footer
Then I should be navigated to second page
And I should see a list with 1 item
And I should see paging header and footer with correct info
And I should see enabled previous button
And I should also see disabled next button
When I click on previous page button in footer
Then I should be navigated to first page
And I should see a list with 25 items
And I should see paging header and footer with correct info
And I should see disabled previous button
And I should also see enabled next button
"""
self
.
_add_default_notes
(
extra_notes
=
21
)
self
.
notes_page
.
visit
()
self
.
_verify_pagination_info
(
notes_count_on_current_page
=
25
,
header_text
=
'Showing 1-25 out of 26 total'
,
previous_button_enabled
=
False
,
next_button_enabled
=
True
,
current_page_number
=
1
,
total_pages
=
2
)
self
.
notes_page
.
press_next_page_button
()
self
.
_verify_pagination_info
(
notes_count_on_current_page
=
1
,
header_text
=
'Showing 26-26 out of 26 total'
,
previous_button_enabled
=
True
,
next_button_enabled
=
False
,
current_page_number
=
2
,
total_pages
=
2
)
self
.
notes_page
.
press_previous_page_button
()
self
.
_verify_pagination_info
(
notes_count_on_current_page
=
25
,
header_text
=
'Showing 1-25 out of 26 total'
,
previous_button_enabled
=
False
,
next_button_enabled
=
True
,
current_page_number
=
1
,
total_pages
=
2
)
def
test_pagination_with_valid_and_invalid_page_number
(
self
):
"""
Scenario: Notes list pagination works as expected for valid & invalid page number
Given that I am a registered user
And I have a course with 26 notes
When I open Notes page
Then I can see notes list contains 25 items
And I should see paging header and footer with correct data
And I should see total page value is 2
When I enter 2 in the page number input
Then I should be navigated to page 2
When I enter 3 in the page number input
Then I should not be navigated away from page 2
"""
self
.
_add_default_notes
(
extra_notes
=
21
)
self
.
notes_page
.
visit
()
self
.
assertEqual
(
self
.
notes_page
.
get_total_pages
,
2
)
# test pagination with valid page number
self
.
notes_page
.
go_to_page
(
2
)
self
.
_verify_pagination_info
(
notes_count_on_current_page
=
1
,
header_text
=
'Showing 26-26 out of 26 total'
,
previous_button_enabled
=
True
,
next_button_enabled
=
False
,
current_page_number
=
2
,
total_pages
=
2
)
# test pagination with invalid page number
self
.
notes_page
.
go_to_page
(
3
)
self
.
_verify_pagination_info
(
notes_count_on_current_page
=
1
,
header_text
=
'Showing 26-26 out of 26 total'
,
previous_button_enabled
=
True
,
next_button_enabled
=
False
,
current_page_number
=
2
,
total_pages
=
2
)
def
test_search_behaves_correctly_with_pagination
(
self
):
"""
Scenario: Searching behaves correctly with pagination.
Given that I am a registered user
And I have a course with 27 notes
When I open Notes page
Then I can see notes list with 25 items
And I should see paging header and footer with correct data
And previous button is disabled
And next button is enabled
When I run the search with "note" query
Then I see no error message
And I see that "Search Results" tab appears with 26 notes found
And an event has fired indicating that the Search Results view was selected
And an event has fired recording the search that was performed
"""
self
.
search_and_verify
()
self
.
_verify_pagination_info
(
notes_count_on_current_page
=
25
,
header_text
=
'Showing 1-25 out of 26 total'
,
previous_button_enabled
=
False
,
next_button_enabled
=
True
,
current_page_number
=
1
,
total_pages
=
2
)
self
.
assert_viewed_event
(
'Search Results'
)
self
.
assert_search_event
(
'note'
,
26
)
def
test_search_with_next_and_prev_page_button
(
self
):
"""
Scenario: Next & Previous buttons are working as expected for search
Given that I am a registered user
And I have a course with 27 notes
When I open Notes page
Then I can see notes list with 25 items
And I should see paging header and footer with correct data
And previous button is disabled
And next button is enabled
When I run the search with "note" query
Then I see that "Search Results" tab appears with 26 notes found
And an event has fired indicating that the Search Results view was selected
And an event has fired recording the search that was performed
When I click on next page button in footer
Then I should be navigated to second page
And I should see a list with 1 item
And I should see paging header and footer with correct info
And I should see enabled previous button
And I should also see disabled next button
When I click on previous page button in footer
Then I should be navigated to first page
And I should see a list with 25 items
And I should see paging header and footer with correct info
And I should see disabled previous button
And I should also see enabled next button
"""
self
.
search_and_verify
()
self
.
_verify_pagination_info
(
notes_count_on_current_page
=
25
,
header_text
=
'Showing 1-25 out of 26 total'
,
previous_button_enabled
=
False
,
next_button_enabled
=
True
,
current_page_number
=
1
,
total_pages
=
2
)
self
.
assert_viewed_event
(
'Search Results'
)
self
.
assert_search_event
(
'note'
,
26
)
self
.
notes_page
.
press_next_page_button
()
self
.
_verify_pagination_info
(
notes_count_on_current_page
=
1
,
header_text
=
'Showing 26-26 out of 26 total'
,
previous_button_enabled
=
True
,
next_button_enabled
=
False
,
current_page_number
=
2
,
total_pages
=
2
)
self
.
notes_page
.
press_previous_page_button
()
self
.
_verify_pagination_info
(
notes_count_on_current_page
=
25
,
header_text
=
'Showing 1-25 out of 26 total'
,
previous_button_enabled
=
False
,
next_button_enabled
=
True
,
current_page_number
=
1
,
total_pages
=
2
)
def
test_search_with_valid_and_invalid_page_number
(
self
):
"""
Scenario: Notes list pagination works as expected for valid & invalid page number
Given that I am a registered user
And I have a course with 27 notes
When I open Notes page
Then I can see notes list contains 25 items
And I should see paging header and footer with correct data
And I should see total page value is 2
When I run the search with "note" query
Then I see that "Search Results" tab appears with 26 notes found
And an event has fired indicating that the Search Results view was selected
And an event has fired recording the search that was performed
When I enter 2 in the page number input
Then I should be navigated to page 2
When I enter 3 in the page number input
Then I should not be navigated away from page 2
"""
self
.
search_and_verify
()
# test pagination with valid page number
self
.
notes_page
.
go_to_page
(
2
)
self
.
_verify_pagination_info
(
notes_count_on_current_page
=
1
,
header_text
=
'Showing 26-26 out of 26 total'
,
previous_button_enabled
=
True
,
next_button_enabled
=
False
,
current_page_number
=
2
,
total_pages
=
2
)
# test pagination with invalid page number
self
.
notes_page
.
go_to_page
(
3
)
self
.
_verify_pagination_info
(
notes_count_on_current_page
=
1
,
header_text
=
'Showing 26-26 out of 26 total'
,
previous_button_enabled
=
True
,
next_button_enabled
=
False
,
current_page_number
=
2
,
total_pages
=
2
)
@attr
(
'shard_4'
)
class
EdxNotesToggleSingleNoteTest
(
EdxNotesTestMixin
):
...
...
lms/djangoapps/edxnotes/helpers.py
View file @
770a45b7
"""
Helper methods related to EdxNotes.
"""
import
json
import
logging
from
json
import
JSONEncoder
from
uuid
import
uuid4
import
urlparse
from
urllib
import
urlencode
import
requests
from
datetime
import
datetime
...
...
@@ -19,7 +20,7 @@ from django.core.urlresolvers import reverse
from
django.utils.translation
import
ugettext
as
_
from
edxnotes.exceptions
import
EdxNotesParseError
,
EdxNotesServiceUnavailable
from
capa.util
import
sanitize_html
from
edxnotes.plugins
import
EdxNotesTab
from
courseware.views
import
get_current_child
from
courseware.access
import
has_access
from
openedx.core.lib.token_utils
import
get_id_token
...
...
@@ -30,10 +31,10 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
log
=
logging
.
getLogger
(
__name__
)
HIGHLIGHT_TAG
=
"span"
HIGHLIGHT_CLASS
=
"note-highlight"
# OAuth2 Client name for edxnotes
CLIENT_NAME
=
"edx-notes"
DEFAULT_PAGE
=
1
DEFAULT_PAGE_SIZE
=
25
class
NoteJSONEncoder
(
JSONEncoder
):
...
...
@@ -63,22 +64,33 @@ def get_token_url(course_id):
})
def
send_request
(
user
,
course_id
,
pa
th
=
""
,
query_string
=
None
):
def
send_request
(
user
,
course_id
,
pa
ge
,
page_size
,
path
=
""
,
text
=
None
):
"""
Sends a request with appropriate parameters and headers.
Sends a request to notes api with appropriate parameters and headers.
Arguments:
user: Current logged in user
course_id: Course id
page: requested or default page number
page_size: requested or default page size
path: `search` or `annotations`. This is used to calculate notes api endpoint.
text: text to search.
Returns:
Response received from notes api
"""
url
=
get_internal_endpoint
(
path
)
params
=
{
"user"
:
anonymous_id_for_user
(
user
,
None
),
"course_id"
:
unicode
(
course_id
)
.
encode
(
"utf-8"
),
"page"
:
page
,
"page_size"
:
page_size
,
}
if
query_string
:
if
text
:
params
.
update
({
"text"
:
query_string
,
"highlight"
:
True
,
"highlight_tag"
:
HIGHLIGHT_TAG
,
"highlight_class"
:
HIGHLIGHT_CLASS
,
"text"
:
text
,
"highlight"
:
True
})
try
:
...
...
@@ -90,6 +102,7 @@ def send_request(user, course_id, path="", query_string=None):
params
=
params
)
except
RequestException
:
log
.
error
(
"Failed to connect to edx-notes-api: url=
%
s, params=
%
s"
,
url
,
str
(
params
))
raise
EdxNotesServiceUnavailable
(
_
(
"EdxNotes Service is unavailable. Please try again in a few minutes."
))
return
response
...
...
@@ -125,15 +138,13 @@ def preprocess_collection(user, course, collection):
store
=
modulestore
()
filtered_collection
=
list
()
cache
=
{}
include_path_info
=
(
'course_structure'
not
in
settings
.
NOTES_DISABLED_TABS
)
with
store
.
bulk_operations
(
course
.
id
):
for
model
in
collection
:
update
=
{
u"text"
:
sanitize_html
(
model
[
"text"
]),
u"quote"
:
sanitize_html
(
model
[
"quote"
]),
u"updated"
:
dateutil_parse
(
model
[
"updated"
]),
}
if
"tags"
in
model
:
update
[
u"tags"
]
=
[
sanitize_html
(
tag
)
for
tag
in
model
[
"tags"
]]
model
.
update
(
update
)
usage_id
=
model
[
"usage_id"
]
if
usage_id
in
cache
:
...
...
@@ -160,42 +171,46 @@ def preprocess_collection(user, course, collection):
log
.
debug
(
"Unit not found:
%
s"
,
usage_key
)
continue
section
=
unit
.
get_parent
()
if
not
section
:
log
.
debug
(
"Section not found:
%
s"
,
usage_key
)
continue
if
section
in
cache
:
usage_context
=
cache
[
section
]
usage_context
.
update
({
"unit"
:
get_module_context
(
course
,
unit
),
})
model
.
update
(
usage_context
)
cache
[
usage_id
]
=
cache
[
unit
]
=
usage_context
filtered_collection
.
append
(
model
)
continue
chapter
=
section
.
get_parent
()
if
not
chapter
:
log
.
debug
(
"Chapter not found:
%
s"
,
usage_key
)
continue
if
chapter
in
cache
:
usage_context
=
cache
[
chapter
]
usage_context
.
update
({
"unit"
:
get_module_context
(
course
,
unit
),
"section"
:
get_module_context
(
course
,
section
),
})
model
.
update
(
usage_context
)
cache
[
usage_id
]
=
cache
[
unit
]
=
cache
[
section
]
=
usage_context
filtered_collection
.
append
(
model
)
continue
if
include_path_info
:
section
=
unit
.
get_parent
()
if
not
section
:
log
.
debug
(
"Section not found:
%
s"
,
usage_key
)
continue
if
section
in
cache
:
usage_context
=
cache
[
section
]
usage_context
.
update
({
"unit"
:
get_module_context
(
course
,
unit
),
})
model
.
update
(
usage_context
)
cache
[
usage_id
]
=
cache
[
unit
]
=
usage_context
filtered_collection
.
append
(
model
)
continue
chapter
=
section
.
get_parent
()
if
not
chapter
:
log
.
debug
(
"Chapter not found:
%
s"
,
usage_key
)
continue
if
chapter
in
cache
:
usage_context
=
cache
[
chapter
]
usage_context
.
update
({
"unit"
:
get_module_context
(
course
,
unit
),
"section"
:
get_module_context
(
course
,
section
),
})
model
.
update
(
usage_context
)
cache
[
usage_id
]
=
cache
[
unit
]
=
cache
[
section
]
=
usage_context
filtered_collection
.
append
(
model
)
continue
usage_context
=
{
"unit"
:
get_module_context
(
course
,
unit
),
"section"
:
get_module_context
(
course
,
section
),
"chapter"
:
get_module_context
(
course
,
chapter
),
"section"
:
get_module_context
(
course
,
section
)
if
include_path_info
else
{}
,
"chapter"
:
get_module_context
(
course
,
chapter
)
if
include_path_info
else
{}
,
}
model
.
update
(
usage_context
)
cache
[
usage_id
]
=
cache
[
unit
]
=
cache
[
section
]
=
cache
[
chapter
]
=
usage_context
if
include_path_info
:
cache
[
section
]
=
cache
[
chapter
]
=
usage_context
cache
[
usage_id
]
=
cache
[
unit
]
=
usage_context
filtered_collection
.
append
(
model
)
return
filtered_collection
...
...
@@ -239,39 +254,97 @@ def get_index(usage_key, children):
return
children
.
index
(
usage_key
)
def
search
(
user
,
course
,
query_string
):
def
construct_pagination_urls
(
request
,
course_id
,
api_next_url
,
api_previous_url
):
"""
Returns search results for the `query_string(str)`.
Construct next and previous urls for LMS. `api_next_url` and `api_previous_url`
are returned from notes api but we need to transform them according to LMS notes
views by removing and replacing extra information.
Arguments:
request: HTTP request object
course_id: course id
api_next_url: notes api next url
api_previous_url: notes api previous url
Returns:
next_url: lms notes next url
previous_url: lms notes previous url
"""
response
=
send_request
(
user
,
course
.
id
,
"search"
,
query_string
)
try
:
content
=
json
.
loads
(
response
.
content
)
collection
=
content
[
"rows"
]
except
(
ValueError
,
KeyError
):
log
.
warning
(
"invalid JSON:
%
s"
,
response
.
content
)
raise
EdxNotesParseError
(
_
(
"Server error. Please try again in a few minutes."
))
content
.
update
({
"rows"
:
preprocess_collection
(
user
,
course
,
collection
)
})
def
lms_url
(
url
):
"""
Create lms url from api url.
"""
if
url
is
None
:
return
None
keys
=
(
'page'
,
'page_size'
,
'text'
)
parsed
=
urlparse
.
urlparse
(
url
)
query_params
=
urlparse
.
parse_qs
(
parsed
.
query
)
encoded_query_params
=
urlencode
({
key
:
query_params
.
get
(
key
)[
0
]
for
key
in
keys
if
key
in
query_params
})
return
"{}?{}"
.
format
(
request
.
build_absolute_uri
(
base_url
),
encoded_query_params
)
return
json
.
dumps
(
content
,
cls
=
NoteJSONEncoder
)
base_url
=
reverse
(
"notes"
,
kwargs
=
{
"course_id"
:
course_id
})
next_url
=
lms_url
(
api_next_url
)
previous_url
=
lms_url
(
api_previous_url
)
return
next_url
,
previous_url
def
get_notes
(
user
,
course
):
def
get_notes
(
request
,
course
,
page
=
DEFAULT_PAGE
,
page_size
=
DEFAULT_PAGE_SIZE
,
text
=
None
):
"""
Returns all notes for the user.
Returns paginated list of notes for the user.
Arguments:
request: HTTP request object
course: Course descriptor
page: requested or default page number
page_size: requested or default page size
text: text to search. If None then return all results for the current logged in user.
Returns:
Paginated dictionary with these key:
start: start of the current page
current_page: current page number
next: url for next page
previous: url for previous page
count: total number of notes available for the sent query
num_pages: number of pages available
results: list with notes info dictionary. each item in this list will be a dict
"""
response
=
send_request
(
user
,
course
.
id
,
"annotations"
)
path
=
'search'
if
text
else
'annotations'
response
=
send_request
(
request
.
user
,
course
.
id
,
page
,
page_size
,
path
,
text
)
try
:
collection
=
json
.
loads
(
response
.
content
)
except
ValueError
:
return
None
if
not
collection
:
return
None
return
json
.
dumps
(
preprocess_collection
(
user
,
course
,
collection
),
cls
=
NoteJSONEncoder
)
log
.
error
(
"Invalid JSON response received from notes api: response_content=
%
s"
,
response
.
content
)
raise
EdxNotesParseError
(
_
(
"Invalid JSON response received from notes api."
))
# Verify response dict structure
expected_keys
=
[
'total'
,
'rows'
,
'num_pages'
,
'start'
,
'next'
,
'previous'
,
'current_page'
]
keys
=
collection
.
keys
()
if
not
keys
or
not
all
(
key
in
expected_keys
for
key
in
keys
):
log
.
error
(
"Incorrect data received from notes api: collection_data=
%
s"
,
str
(
collection
))
raise
EdxNotesParseError
(
_
(
"Incorrect data received from notes api."
))
filtered_results
=
preprocess_collection
(
request
.
user
,
course
,
collection
[
'rows'
])
# Notes API is called from:
# 1. The annotatorjs in courseware. It expects these attributes to be named "total" and "rows".
# 2. The Notes tab Javascript proxied through LMS. It expects these attributes to be called "count" and "results".
collection
[
'count'
]
=
collection
[
'total'
]
del
collection
[
'total'
]
collection
[
'results'
]
=
filtered_results
del
collection
[
'rows'
]
collection
[
'next'
],
collection
[
'previous'
]
=
construct_pagination_urls
(
request
,
course
.
id
,
collection
[
'next'
],
collection
[
'previous'
]
)
return
collection
def
get_endpoint
(
api_url
,
path
=
""
):
...
...
@@ -354,26 +427,6 @@ def generate_uid():
def
is_feature_enabled
(
course
):
"""
Returns True if Student Notes feature is enabled for the course,
False otherwise.
In order for the application to be enabled it must be:
1) enabled globally via FEATURES.
2) present in the course tab configuration.
3) Harvard Annotation Tool must be disabled for the course.
"""
return
(
settings
.
FEATURES
.
get
(
"ENABLE_EDXNOTES"
)
and
[
t
for
t
in
course
.
tabs
if
t
[
"type"
]
==
"edxnotes"
]
# tab found
and
not
is_harvard_notes_enabled
(
course
))
def
is_harvard_notes_enabled
(
course
):
"""
Returns True if Harvard Annotation Tool is enabled for the course,
False otherwise.
Checks for 'textannotation', 'imageannotation', 'videoannotation' in the list
of advanced modules of the course.
Returns True if Student Notes feature is enabled for the course, False otherwise.
"""
modules
=
set
([
'textannotation'
,
'imageannotation'
,
'videoannotation'
])
return
bool
(
modules
.
intersection
(
course
.
advanced_modules
))
return
EdxNotesTab
.
is_enabled
(
course
)
lms/djangoapps/edxnotes/plugins.py
View file @
770a45b7
"""
Registers the "edX Notes" feature for the edX platform.
"""
from
django.conf
import
settings
from
django.utils.translation
import
ugettext_noop
from
courseware.tabs
import
EnrolledTab
...
...
@@ -27,4 +26,20 @@ class EdxNotesTab(EnrolledTab):
"""
if
not
super
(
EdxNotesTab
,
cls
)
.
is_enabled
(
course
,
user
=
user
):
return
False
if
not
settings
.
FEATURES
.
get
(
"ENABLE_EDXNOTES"
)
or
is_harvard_notes_enabled
(
course
):
return
False
return
course
.
edxnotes
def
is_harvard_notes_enabled
(
course
):
"""
Returns True if Harvard Annotation Tool is enabled for the course,
False otherwise.
Checks for 'textannotation', 'imageannotation', 'videoannotation' in the list
of advanced modules of the course.
"""
modules
=
set
([
'textannotation'
,
'imageannotation'
,
'videoannotation'
])
return
bool
(
modules
.
intersection
(
course
.
advanced_modules
))
lms/djangoapps/edxnotes/tests.py
View file @
770a45b7
...
...
@@ -8,11 +8,13 @@ import jwt
from
mock
import
patch
,
MagicMock
from
unittest
import
skipUnless
from
datetime
import
datetime
import
urlparse
from
edxmako.shortcuts
import
render_to_string
from
edxnotes
import
helpers
from
edxnotes.decorators
import
edxnotes
from
edxnotes.exceptions
import
EdxNotesParseError
,
EdxNotesServiceUnavailable
from
edxnotes.plugins
import
EdxNotesTab
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
django.core.exceptions
import
ImproperlyConfigured
...
...
@@ -31,6 +33,29 @@ from courseware.tabs import get_course_tab_list
from
student.tests.factories
import
UserFactory
,
CourseEnrollmentFactory
FEATURES
=
settings
.
FEATURES
.
copy
()
NOTES_API_EMPTY_RESPONSE
=
{
"total"
:
0
,
"rows"
:
[],
"current_page"
:
1
,
"start"
:
0
,
"next"
:
None
,
"previous"
:
None
,
"num_pages"
:
0
,
}
NOTES_VIEW_EMPTY_RESPONSE
=
{
"count"
:
0
,
"results"
:
[],
"current_page"
:
1
,
"start"
:
0
,
"next"
:
None
,
"previous"
:
None
,
"num_pages"
:
0
,
}
def
enable_edxnotes_for_the_course
(
course
,
user_id
):
"""
Enable EdxNotes for the course.
...
...
@@ -118,6 +143,7 @@ class EdxNotesDecoratorTest(ModuleStoreTestCase):
Tests that get_html is wrapped when feature flag is on, but edxnotes are
disabled for the course.
"""
self
.
course
.
edxnotes
=
False
self
.
assertEqual
(
"original_get_html"
,
self
.
problem
.
get_html
())
@patch.dict
(
"django.conf.settings.FEATURES"
,
{
"ENABLE_EDXNOTES"
:
False
})
...
...
@@ -191,6 +217,9 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
self
.
user
=
UserFactory
.
create
(
username
=
"Joe"
,
email
=
"joe@example.com"
,
password
=
"edx"
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
"edx"
)
self
.
request
=
RequestFactory
()
.
request
()
self
.
request
.
user
=
self
.
user
def
_get_unit_url
(
self
,
course
,
chapter
,
section
,
position
=
1
):
"""
Returns `jump_to_id` url for the `vertical`.
...
...
@@ -202,14 +231,6 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
'position'
:
position
,
})
def
test_edxnotes_not_enabled
(
self
):
"""
Tests that edxnotes are disabled when the course tab configuration does NOT
contain a tab with type "edxnotes."
"""
self
.
course
.
tabs
=
[]
self
.
assertFalse
(
helpers
.
is_feature_enabled
(
self
.
course
))
def
test_edxnotes_harvard_notes_enabled
(
self
):
"""
Tests that edxnotes are disabled when Harvard Annotation Tool is enabled.
...
...
@@ -226,15 +247,17 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
self
.
course
.
advanced_modules
=
[
"textannotation"
,
"videoannotation"
,
"imageannotation"
]
self
.
assertFalse
(
helpers
.
is_feature_enabled
(
self
.
course
))
def
test_edxnotes_enabled
(
self
):
@ddt.unpack
@ddt.data
(
{
'_edxnotes'
:
True
},
{
'_edxnotes'
:
False
}
)
def
test_is_feature_enabled
(
self
,
_edxnotes
):
"""
Tests that edxnotes are enabled when the course tab configuration contains
a tab with type "edxnotes."
Tests that is_feature_enabled shows correct behavior.
"""
self
.
course
.
tabs
=
[{
"type"
:
"foo"
},
{
"name"
:
"Notes"
,
"type"
:
"edxnotes"
},
{
"type"
:
"bar"
}]
self
.
assertTrue
(
helpers
.
is_feature_enabled
(
self
.
course
))
self
.
course
.
edxnotes
=
_edxnotes
self
.
assertEqual
(
helpers
.
is_feature_enabled
(
self
.
course
),
_edxnotes
)
@ddt.data
(
helpers
.
get_public_endpoint
,
...
...
@@ -280,71 +303,91 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
"""
Tests the result if correct data is received.
"""
mock_get
.
return_value
.
content
=
json
.
dumps
([
{
u"quote"
:
u"quote text"
,
u"text"
:
u"text"
,
u"usage_id"
:
unicode
(
self
.
html_module_1
.
location
),
u"updated"
:
datetime
(
2014
,
11
,
19
,
8
,
5
,
16
,
00000
)
.
isoformat
(),
},
mock_get
.
return_value
.
content
=
json
.
dumps
(
{
u"quote"
:
u"quote text"
,
u"text"
:
u"text"
,
u"usage_id"
:
unicode
(
self
.
html_module_2
.
location
),
u"updated"
:
datetime
(
2014
,
11
,
19
,
8
,
6
,
16
,
00000
)
.
isoformat
(),
"total"
:
2
,
"current_page"
:
1
,
"start"
:
0
,
"next"
:
None
,
"previous"
:
None
,
"num_pages"
:
1
,
"rows"
:
[
{
u"quote"
:
u"quote text"
,
u"text"
:
u"text"
,
u"usage_id"
:
unicode
(
self
.
html_module_1
.
location
),
u"updated"
:
datetime
(
2014
,
11
,
19
,
8
,
5
,
16
,
00000
)
.
isoformat
(),
},
{
u"quote"
:
u"quote text"
,
u"text"
:
u"text"
,
u"usage_id"
:
unicode
(
self
.
html_module_2
.
location
),
u"updated"
:
datetime
(
2014
,
11
,
19
,
8
,
6
,
16
,
00000
)
.
isoformat
(),
}
]
}
]
)
)
self
.
assertItemsEqual
(
[
{
u"quote"
:
u"quote text"
,
u"text"
:
u"text"
,
u"chapter"
:
{
u"display_name"
:
self
.
chapter
.
display_name_with_default_escaped
,
u"index"
:
0
,
u"location"
:
unicode
(
self
.
chapter
.
location
),
u"children"
:
[
unicode
(
self
.
sequential
.
location
)]
},
u"section"
:
{
u"display_name"
:
self
.
sequential
.
display_name_with_default_escaped
,
u"location"
:
unicode
(
self
.
sequential
.
location
),
u"children"
:
[
unicode
(
self
.
vertical
.
location
),
unicode
(
self
.
vertical_with_container
.
location
)]
},
u"unit"
:
{
u"url"
:
self
.
_get_unit_url
(
self
.
course
,
self
.
chapter
,
self
.
sequential
),
u"display_name"
:
self
.
vertical
.
display_name_with_default_escaped
,
u"location"
:
unicode
(
self
.
vertical
.
location
),
},
u"usage_id"
:
unicode
(
self
.
html_module_2
.
location
),
u"updated"
:
"Nov 19, 2014 at 08:06 UTC"
,
},
{
u"quote"
:
u"quote text"
,
u"text"
:
u"text"
,
u"chapter"
:
{
u"display_name"
:
self
.
chapter
.
display_name_with_default_escaped
,
u"index"
:
0
,
u"location"
:
unicode
(
self
.
chapter
.
location
),
u"children"
:
[
unicode
(
self
.
sequential
.
location
)]
},
u"section"
:
{
u"display_name"
:
self
.
sequential
.
display_name_with_default_escaped
,
u"location"
:
unicode
(
self
.
sequential
.
location
),
u"children"
:
[
unicode
(
self
.
vertical
.
location
),
unicode
(
self
.
vertical_with_container
.
location
)]
{
"count"
:
2
,
"current_page"
:
1
,
"start"
:
0
,
"next"
:
None
,
"previous"
:
None
,
"num_pages"
:
1
,
"results"
:
[
{
u"quote"
:
u"quote text"
,
u"text"
:
u"text"
,
u"chapter"
:
{
u"display_name"
:
self
.
chapter
.
display_name_with_default_escaped
,
u"index"
:
0
,
u"location"
:
unicode
(
self
.
chapter
.
location
),
u"children"
:
[
unicode
(
self
.
sequential
.
location
)]
},
u"section"
:
{
u"display_name"
:
self
.
sequential
.
display_name_with_default_escaped
,
u"location"
:
unicode
(
self
.
sequential
.
location
),
u"children"
:
[
unicode
(
self
.
vertical
.
location
),
unicode
(
self
.
vertical_with_container
.
location
)
]
},
u"unit"
:
{
u"url"
:
self
.
_get_unit_url
(
self
.
course
,
self
.
chapter
,
self
.
sequential
),
u"display_name"
:
self
.
vertical
.
display_name_with_default_escaped
,
u"location"
:
unicode
(
self
.
vertical
.
location
),
},
u"usage_id"
:
unicode
(
self
.
html_module_2
.
location
),
u"updated"
:
"Nov 19, 2014 at 08:06 UTC"
,
},
u"unit"
:
{
u"url"
:
self
.
_get_unit_url
(
self
.
course
,
self
.
chapter
,
self
.
sequential
),
u"display_name"
:
self
.
vertical
.
display_name_with_default_escaped
,
u"location"
:
unicode
(
self
.
vertical
.
location
),
{
u"quote"
:
u"quote text"
,
u"text"
:
u"text"
,
u"chapter"
:
{
u"display_name"
:
self
.
chapter
.
display_name_with_default_escaped
,
u"index"
:
0
,
u"location"
:
unicode
(
self
.
chapter
.
location
),
u"children"
:
[
unicode
(
self
.
sequential
.
location
)]
},
u"section"
:
{
u"display_name"
:
self
.
sequential
.
display_name_with_default_escaped
,
u"location"
:
unicode
(
self
.
sequential
.
location
),
u"children"
:
[
unicode
(
self
.
vertical
.
location
),
unicode
(
self
.
vertical_with_container
.
location
)]
},
u"unit"
:
{
u"url"
:
self
.
_get_unit_url
(
self
.
course
,
self
.
chapter
,
self
.
sequential
),
u"display_name"
:
self
.
vertical
.
display_name_with_default_escaped
,
u"location"
:
unicode
(
self
.
vertical
.
location
),
},
u"usage_id"
:
unicode
(
self
.
html_module_1
.
location
),
u"updated"
:
"Nov 19, 2014 at 08:05 UTC"
,
},
u"usage_id"
:
unicode
(
self
.
html_module_1
.
location
),
u"updated"
:
"Nov 19, 2014 at 08:05 UTC"
,
},
],
json
.
loads
(
helpers
.
get_notes
(
self
.
user
,
self
.
course
))
]
},
helpers
.
get_notes
(
self
.
request
,
self
.
course
)
)
@patch
(
"edxnotes.helpers.requests.get"
,
autospec
=
True
)
...
...
@@ -353,15 +396,15 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
Tests the result if incorrect json is received.
"""
mock_get
.
return_value
.
content
=
"Error"
self
.
assert
IsNone
(
helpers
.
get_notes
(
self
.
user
,
self
.
course
)
)
self
.
assert
Raises
(
EdxNotesParseError
,
helpers
.
get_notes
,
self
.
request
,
self
.
course
)
@patch
(
"edxnotes.helpers.requests.get"
,
autospec
=
True
)
def
test_get_notes_empty_collection
(
self
,
mock_get
):
"""
Tests the result if an empty
collection
is received.
Tests the result if an empty
response
is received.
"""
mock_get
.
return_value
.
content
=
json
.
dumps
(
[]
)
self
.
assert
IsNone
(
helpers
.
get_notes
(
self
.
user
,
self
.
course
)
)
mock_get
.
return_value
.
content
=
json
.
dumps
(
{}
)
self
.
assert
Raises
(
EdxNotesParseError
,
helpers
.
get_notes
,
self
.
request
,
self
.
course
)
@patch
(
"edxnotes.helpers.requests.get"
,
autospec
=
True
)
def
test_search_correct_data
(
self
,
mock_get
):
...
...
@@ -370,6 +413,11 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
"""
mock_get
.
return_value
.
content
=
json
.
dumps
({
"total"
:
2
,
"current_page"
:
1
,
"start"
:
0
,
"next"
:
None
,
"previous"
:
None
,
"num_pages"
:
1
,
"rows"
:
[
{
u"quote"
:
u"quote text"
,
...
...
@@ -388,8 +436,13 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
self
.
assertItemsEqual
(
{
"total"
:
2
,
"rows"
:
[
"count"
:
2
,
"current_page"
:
1
,
"start"
:
0
,
"next"
:
None
,
"previous"
:
None
,
"num_pages"
:
1
,
"results"
:
[
{
u"quote"
:
u"quote text"
,
u"text"
:
u"text"
,
...
...
@@ -440,7 +493,7 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
},
]
},
json
.
loads
(
helpers
.
search
(
self
.
user
,
self
.
course
,
"test"
)
)
helpers
.
get_notes
(
self
.
request
,
self
.
course
)
)
@patch
(
"edxnotes.helpers.requests.get"
,
autospec
=
True
)
...
...
@@ -449,7 +502,7 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
Tests the result if incorrect json is received.
"""
mock_get
.
return_value
.
content
=
"Error"
self
.
assertRaises
(
EdxNotesParseError
,
helpers
.
search
,
self
.
user
,
self
.
course
,
"test"
)
self
.
assertRaises
(
EdxNotesParseError
,
helpers
.
get_notes
,
self
.
request
,
self
.
course
)
@patch
(
"edxnotes.helpers.requests.get"
,
autospec
=
True
)
def
test_search_wrong_data_format
(
self
,
mock_get
):
...
...
@@ -457,60 +510,17 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
Tests the result if incorrect data structure is received.
"""
mock_get
.
return_value
.
content
=
json
.
dumps
({
"1"
:
2
})
self
.
assertRaises
(
EdxNotesParseError
,
helpers
.
search
,
self
.
user
,
self
.
course
,
"test"
)
self
.
assertRaises
(
EdxNotesParseError
,
helpers
.
get_notes
,
self
.
request
,
self
.
course
)
@patch
(
"edxnotes.helpers.requests.get"
,
autospec
=
True
)
def
test_search_empty_collection
(
self
,
mock_get
):
"""
Tests no results.
"""
mock_get
.
return_value
.
content
=
json
.
dumps
({
"total"
:
0
,
"rows"
:
[]
})
mock_get
.
return_value
.
content
=
json
.
dumps
(
NOTES_API_EMPTY_RESPONSE
)
self
.
assertItemsEqual
(
{
"total"
:
0
,
"rows"
:
[]
},
json
.
loads
(
helpers
.
search
(
self
.
user
,
self
.
course
,
"test"
))
)
def
test_preprocess_collection_escaping
(
self
):
"""
Tests the result if appropriate module is not found.
"""
initial_collection
=
[{
u"quote"
:
u"test <script>alert('test')</script>"
,
u"text"
:
u"text
\"
<>&'"
,
u"usage_id"
:
unicode
(
self
.
html_module_1
.
location
),
u"updated"
:
datetime
(
2014
,
11
,
19
,
8
,
5
,
16
,
00000
)
.
isoformat
()
}]
self
.
assertItemsEqual
(
[{
u"quote"
:
u"test <script>alert('test')</script>"
,
u"text"
:
u'text "<>&
\'
'
,
u"chapter"
:
{
u"display_name"
:
self
.
chapter
.
display_name_with_default_escaped
,
u"index"
:
0
,
u"location"
:
unicode
(
self
.
chapter
.
location
),
u"children"
:
[
unicode
(
self
.
sequential
.
location
)]
},
u"section"
:
{
u"display_name"
:
self
.
sequential
.
display_name_with_default_escaped
,
u"location"
:
unicode
(
self
.
sequential
.
location
),
u"children"
:
[
unicode
(
self
.
vertical
.
location
),
unicode
(
self
.
vertical_with_container
.
location
)]
},
u"unit"
:
{
u"url"
:
self
.
_get_unit_url
(
self
.
course
,
self
.
chapter
,
self
.
sequential
),
u"display_name"
:
self
.
vertical
.
display_name_with_default_escaped
,
u"location"
:
unicode
(
self
.
vertical
.
location
),
},
u"usage_id"
:
unicode
(
self
.
html_module_1
.
location
),
u"updated"
:
datetime
(
2014
,
11
,
19
,
8
,
5
,
16
,
00000
),
}],
helpers
.
preprocess_collection
(
self
.
user
,
self
.
course
,
initial_collection
)
NOTES_VIEW_EMPTY_RESPONSE
,
helpers
.
get_notes
(
self
.
request
,
self
.
course
)
)
def
test_preprocess_collection_no_item
(
self
):
...
...
@@ -625,6 +635,59 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
[],
helpers
.
preprocess_collection
(
self
.
user
,
self
.
course
,
initial_collection
)
)
@override_settings
(
NOTES_DISABLED_TABS
=
[
'course_structure'
,
'tags'
])
def
test_preprocess_collection_with_disabled_tabs
(
self
,
):
"""
Tests that preprocess collection returns correct data if `course_structure` and `tags` are disabled.
"""
initial_collection
=
[
{
u"quote"
:
u"quote text1"
,
u"text"
:
u"text1"
,
u"usage_id"
:
unicode
(
self
.
html_module_1
.
location
),
u"updated"
:
datetime
(
2016
,
01
,
26
,
8
,
5
,
16
,
00000
)
.
isoformat
(),
},
{
u"quote"
:
u"quote text2"
,
u"text"
:
u"text2"
,
u"usage_id"
:
unicode
(
self
.
html_module_2
.
location
),
u"updated"
:
datetime
(
2016
,
01
,
26
,
9
,
6
,
17
,
00000
)
.
isoformat
(),
},
]
self
.
assertItemsEqual
(
[
{
'section'
:
{},
'chapter'
:
{},
"unit"
:
{
u"url"
:
self
.
_get_unit_url
(
self
.
course
,
self
.
chapter
,
self
.
sequential
),
u"display_name"
:
self
.
vertical
.
display_name_with_default_escaped
,
u"location"
:
unicode
(
self
.
vertical
.
location
),
},
u'text'
:
u'text1'
,
u'quote'
:
u'quote text1'
,
u'usage_id'
:
unicode
(
self
.
html_module_1
.
location
),
u'updated'
:
datetime
(
2016
,
01
,
26
,
8
,
5
,
16
)
},
{
'section'
:
{},
'chapter'
:
{},
"unit"
:
{
u"url"
:
self
.
_get_unit_url
(
self
.
course
,
self
.
chapter
,
self
.
sequential
),
u"display_name"
:
self
.
vertical
.
display_name_with_default_escaped
,
u"location"
:
unicode
(
self
.
vertical
.
location
),
},
u'text'
:
u'text2'
,
u'quote'
:
u'quote text2'
,
u'usage_id'
:
unicode
(
self
.
html_module_2
.
location
),
u'updated'
:
datetime
(
2016
,
01
,
26
,
9
,
6
,
17
)
}
],
helpers
.
preprocess_collection
(
self
.
user
,
self
.
course
,
initial_collection
)
)
def
test_get_parent_unit
(
self
):
"""
Tests `get_parent_unit` method for the successful result.
...
...
@@ -693,14 +756,19 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
@patch
(
"edxnotes.helpers.anonymous_id_for_user"
,
autospec
=
True
)
@patch
(
"edxnotes.helpers.get_edxnotes_id_token"
,
autospec
=
True
)
@patch
(
"edxnotes.helpers.requests.get"
,
autospec
=
True
)
def
test_send_request_with_
query_string
(
self
,
mock_get
,
mock_get_id_token
,
mock_anonymous_id_for_user
):
def
test_send_request_with_
text_param
(
self
,
mock_get
,
mock_get_id_token
,
mock_anonymous_id_for_user
):
"""
Tests that requests are send with correct information.
"""
mock_get_id_token
.
return_value
=
"test_token"
mock_anonymous_id_for_user
.
return_value
=
"anonymous_id"
helpers
.
send_request
(
self
.
user
,
self
.
course
.
id
,
path
=
"test"
,
query_string
=
"text"
self
.
user
,
self
.
course
.
id
,
path
=
"test"
,
text
=
"text"
,
page
=
helpers
.
DEFAULT_PAGE
,
page_size
=
helpers
.
DEFAULT_PAGE_SIZE
)
mock_get
.
assert_called_with
(
"http://example.com/test/"
,
...
...
@@ -712,8 +780,8 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
"course_id"
:
unicode
(
self
.
course
.
id
),
"text"
:
"text"
,
"highlight"
:
True
,
"highlight_tag"
:
"span"
,
"highlight_class"
:
"note-highlight"
,
'page'
:
1
,
'page_size'
:
25
,
}
)
...
...
@@ -722,14 +790,14 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
@patch
(
"edxnotes.helpers.anonymous_id_for_user"
,
autospec
=
True
)
@patch
(
"edxnotes.helpers.get_edxnotes_id_token"
,
autospec
=
True
)
@patch
(
"edxnotes.helpers.requests.get"
,
autospec
=
True
)
def
test_send_request_without_
query_string
(
self
,
mock_get
,
mock_get_id_token
,
mock_anonymous_id_for_user
):
def
test_send_request_without_
text_param
(
self
,
mock_get
,
mock_get_id_token
,
mock_anonymous_id_for_user
):
"""
Tests that requests are send with correct information.
"""
mock_get_id_token
.
return_value
=
"test_token"
mock_anonymous_id_for_user
.
return_value
=
"anonymous_id"
helpers
.
send_request
(
self
.
user
,
self
.
course
.
id
,
path
=
"test"
self
.
user
,
self
.
course
.
id
,
path
=
"test"
,
page
=
1
,
page_size
=
25
)
mock_get
.
assert_called_with
(
"http://example.com/test/"
,
...
...
@@ -739,6 +807,8 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
params
=
{
"user"
:
"anonymous_id"
,
"course_id"
:
unicode
(
self
.
course
.
id
),
'page'
:
helpers
.
DEFAULT_PAGE
,
'page_size'
:
helpers
.
DEFAULT_PAGE_SIZE
,
}
)
...
...
@@ -808,8 +878,69 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
self
.
assertEqual
(
0
,
helpers
.
get_index
(
unicode
(
self
.
vertical
.
location
),
children
))
self
.
assertEqual
(
1
,
helpers
.
get_index
(
unicode
(
self
.
vertical_with_container
.
location
),
children
))
@ddt.unpack
@ddt.data
(
{
'previous_api_url'
:
None
,
'next_api_url'
:
None
},
{
'previous_api_url'
:
None
,
'next_api_url'
:
'edxnotes/?course_id=abc&page=2&page_size=10&user=123'
},
{
'previous_api_url'
:
'edxnotes.org/?course_id=abc&page=2&page_size=10&user=123'
,
'next_api_url'
:
None
},
{
'previous_api_url'
:
'edxnotes.org/?course_id=abc&page_size=10&user=123'
,
'next_api_url'
:
'edxnotes.org/?course_id=abc&page=3&page_size=10&user=123'
},
{
'previous_api_url'
:
'edxnotes.org/?course_id=abc&page=2&page_size=10&text=wow&user=123'
,
'next_api_url'
:
'edxnotes.org/?course_id=abc&page=4&page_size=10&text=wow&user=123'
},
)
def
test_construct_url
(
self
,
previous_api_url
,
next_api_url
):
"""
Verify that `construct_url` works correctly.
"""
# make absolute url
# pylint: disable=no-member
if
self
.
request
.
is_secure
():
host
=
'https://'
+
self
.
request
.
get_host
()
else
:
host
=
'http://'
+
self
.
request
.
get_host
()
notes_url
=
host
+
reverse
(
"notes"
,
args
=
[
unicode
(
self
.
course
.
id
)])
def
verify_url
(
constructed
,
expected
):
"""
Verify that constructed url is correct.
"""
# if api url is None then constructed url should also be None
if
expected
is
None
:
self
.
assertEqual
(
expected
,
constructed
)
else
:
# constructed url should startswith notes view url instead of api view url
self
.
assertTrue
(
constructed
.
startswith
(
notes_url
))
# constructed url should not contain extra params
self
.
assertNotIn
(
'user'
,
constructed
)
# constructed url should only has these params if present in api url
allowed_params
=
(
'page'
,
'page_size'
,
'text'
)
# extract query params from constructed url
parsed
=
urlparse
.
urlparse
(
constructed
)
params
=
urlparse
.
parse_qs
(
parsed
.
query
)
# verify that constructed url has only correct params and params have correct values
for
param
,
value
in
params
.
items
():
self
.
assertIn
(
param
,
allowed_params
)
self
.
assertIn
(
'{}={}'
.
format
(
param
,
value
[
0
]),
expected
)
next_url
,
previous_url
=
helpers
.
construct_pagination_urls
(
self
.
request
,
self
.
course
.
id
,
next_api_url
,
previous_api_url
)
verify_url
(
next_url
,
next_api_url
)
verify_url
(
previous_url
,
previous_api_url
)
@skipUnless
(
settings
.
FEATURES
[
"ENABLE_EDXNOTES"
],
"EdxNotes feature needs to be enabled."
)
@ddt.ddt
class
EdxNotesViewsTest
(
ModuleStoreTestCase
):
"""
Tests for EdxNotes views.
...
...
@@ -822,7 +953,7 @@ class EdxNotesViewsTest(ModuleStoreTestCase):
CourseEnrollmentFactory
.
create
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
"edx"
)
self
.
notes_page_url
=
reverse
(
"edxnotes"
,
args
=
[
unicode
(
self
.
course
.
id
)])
self
.
search_url
=
reverse
(
"search_
notes"
,
args
=
[
unicode
(
self
.
course
.
id
)])
self
.
notes_url
=
reverse
(
"
notes"
,
args
=
[
unicode
(
self
.
course
.
id
)])
self
.
get_token_url
=
reverse
(
"get_token"
,
args
=
[
unicode
(
self
.
course
.
id
)])
self
.
visibility_url
=
reverse
(
"edxnotes_visibility"
,
args
=
[
unicode
(
self
.
course
.
id
)])
...
...
@@ -858,14 +989,14 @@ class EdxNotesViewsTest(ModuleStoreTestCase):
# pylint: disable=unused-argument
@patch.dict
(
"django.conf.settings.FEATURES"
,
{
"ENABLE_EDXNOTES"
:
True
})
@patch
(
"edxnotes.views.get_notes"
,
return_value
=
[]
)
@patch
(
"edxnotes.views.get_notes"
,
return_value
=
{
'results'
:
[]}
)
def
test_edxnotes_view_is_enabled
(
self
,
mock_get_notes
):
"""
Tests that appropriate view is received if EdxNotes feature is enabled.
"""
enable_edxnotes_for_the_course
(
self
.
course
,
self
.
user
.
id
)
response
=
self
.
client
.
get
(
self
.
notes_page_url
)
self
.
assertContains
(
response
,
'Highlights and notes you
\'
ve made in course content'
)
self
.
assertContains
(
response
,
'Highlights and notes you
'
ve made in course content'
)
@patch.dict
(
"django.conf.settings.FEATURES"
,
{
"ENABLE_EDXNOTES"
:
False
})
def
test_edxnotes_view_is_disabled
(
self
):
...
...
@@ -877,74 +1008,38 @@ class EdxNotesViewsTest(ModuleStoreTestCase):
@patch.dict
(
"django.conf.settings.FEATURES"
,
{
"ENABLE_EDXNOTES"
:
True
})
@patch
(
"edxnotes.views.get_notes"
,
autospec
=
True
)
def
test_edxnotes_view_404_service_unavailable
(
self
,
mock_get_notes
):
"""
Tests that 404 status code is received if EdxNotes service is unavailable.
"""
mock_get_notes
.
side_effect
=
EdxNotesServiceUnavailable
enable_edxnotes_for_the_course
(
self
.
course
,
self
.
user
.
id
)
response
=
self
.
client
.
get
(
self
.
notes_page_url
)
self
.
assertEqual
(
response
.
status_code
,
404
)
@patch.dict
(
"django.conf.settings.FEATURES"
,
{
"ENABLE_EDXNOTES"
:
True
})
@patch
(
"edxnotes.views.search"
,
autospec
=
True
)
def
test_search_notes_successfully_respond
(
self
,
mock_search
):
"""
Tests that
`search_notes`
successfully respond if EdxNotes feature is enabled.
Tests that
search notes
successfully respond if EdxNotes feature is enabled.
"""
mock_search
.
return_value
=
json
.
dumps
({
"total"
:
0
,
"rows"
:
[],
})
mock_search
.
return_value
=
NOTES_VIEW_EMPTY_RESPONSE
enable_edxnotes_for_the_course
(
self
.
course
,
self
.
user
.
id
)
response
=
self
.
client
.
get
(
self
.
search_url
,
{
"text"
:
"test"
})
self
.
assertEqual
(
json
.
loads
(
response
.
content
),
{
"total"
:
0
,
"rows"
:
[],
})
response
=
self
.
client
.
get
(
self
.
notes_url
,
{
"text"
:
"test"
})
self
.
assertEqual
(
json
.
loads
(
response
.
content
),
NOTES_VIEW_EMPTY_RESPONSE
)
self
.
assertEqual
(
response
.
status_code
,
200
)
@patch.dict
(
"django.conf.settings.FEATURES"
,
{
"ENABLE_EDXNOTES"
:
False
})
@patch
(
"edxnotes.views.search"
,
autospec
=
True
)
def
test_search_notes_is_disabled
(
self
,
mock_search
):
def
test_search_notes_is_disabled
(
self
):
"""
Tests that 404 status code is received if EdxNotes feature is disabled.
"""
mock_search
.
return_value
=
json
.
dumps
({
"total"
:
0
,
"rows"
:
[],
})
response
=
self
.
client
.
get
(
self
.
search_url
,
{
"text"
:
"test"
})
response
=
self
.
client
.
get
(
self
.
notes_url
,
{
"text"
:
"test"
})
self
.
assertEqual
(
response
.
status_code
,
404
)
@patch.dict
(
"django.conf.settings.FEATURES"
,
{
"ENABLE_EDXNOTES"
:
True
})
@patch
(
"edxnotes.views.
search
"
,
autospec
=
True
)
def
test_search_
404
_service_unavailable
(
self
,
mock_search
):
@patch
(
"edxnotes.views.
get_notes
"
,
autospec
=
True
)
def
test_search_
500
_service_unavailable
(
self
,
mock_search
):
"""
Tests that
404
status code is received if EdxNotes service is unavailable.
Tests that
500
status code is received if EdxNotes service is unavailable.
"""
mock_search
.
side_effect
=
EdxNotesServiceUnavailable
enable_edxnotes_for_the_course
(
self
.
course
,
self
.
user
.
id
)
response
=
self
.
client
.
get
(
self
.
search
_url
,
{
"text"
:
"test"
})
response
=
self
.
client
.
get
(
self
.
notes
_url
,
{
"text"
:
"test"
})
self
.
assertEqual
(
response
.
status_code
,
500
)
self
.
assertIn
(
"error"
,
response
.
content
)
@patch.dict
(
"django.conf.settings.FEATURES"
,
{
"ENABLE_EDXNOTES"
:
True
})
@patch
(
"edxnotes.views.search"
,
autospec
=
True
)
def
test_search_notes_without_required_parameters
(
self
,
mock_search
):
"""
Tests that 400 status code is received if the required parameters were not sent.
"""
mock_search
.
return_value
=
json
.
dumps
({
"total"
:
0
,
"rows"
:
[],
})
enable_edxnotes_for_the_course
(
self
.
course
,
self
.
user
.
id
)
response
=
self
.
client
.
get
(
self
.
search_url
)
self
.
assertEqual
(
response
.
status_code
,
400
)
@patch.dict
(
"django.conf.settings.FEATURES"
,
{
"ENABLE_EDXNOTES"
:
True
})
@patch
(
"edxnotes.views.search"
,
autospec
=
True
)
@patch
(
"edxnotes.views.get_notes"
,
autospec
=
True
)
def
test_search_notes_exception
(
self
,
mock_search
):
"""
Tests that 500 status code is received if invalid data was received from
...
...
@@ -952,7 +1047,7 @@ class EdxNotesViewsTest(ModuleStoreTestCase):
"""
mock_search
.
side_effect
=
EdxNotesParseError
enable_edxnotes_for_the_course
(
self
.
course
,
self
.
user
.
id
)
response
=
self
.
client
.
get
(
self
.
search
_url
,
{
"text"
:
"test"
})
response
=
self
.
client
.
get
(
self
.
notes
_url
,
{
"text"
:
"test"
})
self
.
assertEqual
(
response
.
status_code
,
500
)
self
.
assertIn
(
"error"
,
response
.
content
)
...
...
@@ -1022,3 +1117,50 @@ class EdxNotesViewsTest(ModuleStoreTestCase):
content_type
=
"application/json"
,
)
self
.
assertEqual
(
response
.
status_code
,
400
)
@skipUnless
(
settings
.
FEATURES
[
"ENABLE_EDXNOTES"
],
"EdxNotes feature needs to be enabled."
)
@ddt.ddt
class
EdxNotesPluginTest
(
ModuleStoreTestCase
):
"""
EdxNotesTab tests.
"""
def
setUp
(
self
):
super
(
EdxNotesPluginTest
,
self
)
.
setUp
()
self
.
course
=
CourseFactory
.
create
(
edxnotes
=
True
)
self
.
user
=
UserFactory
.
create
(
username
=
"ma"
,
email
=
"ma@ma.info"
,
password
=
"edx"
)
CourseEnrollmentFactory
.
create
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
)
def
test_edxnotes_tab_with_unauthorized_user
(
self
):
"""
Verify EdxNotesTab visibility when user is unauthroized.
"""
user
=
UserFactory
.
create
(
username
=
"ma1"
,
email
=
"ma1@ma1.info"
,
password
=
"edx"
)
self
.
assertFalse
(
EdxNotesTab
.
is_enabled
(
self
.
course
,
user
=
user
))
@ddt.unpack
@ddt.data
(
{
'enable_edxnotes'
:
False
},
{
'enable_edxnotes'
:
True
}
)
def
test_edxnotes_tab_with_feature_flag
(
self
,
enable_edxnotes
):
"""
Verify EdxNotesTab visibility when ENABLE_EDXNOTES feature flag is enabled/disabled.
"""
FEATURES
[
'ENABLE_EDXNOTES'
]
=
enable_edxnotes
with
override_settings
(
FEATURES
=
FEATURES
):
self
.
assertEqual
(
EdxNotesTab
.
is_enabled
(
self
.
course
),
enable_edxnotes
)
@ddt.unpack
@ddt.data
(
{
'harvard_notes_enabled'
:
False
},
{
'harvard_notes_enabled'
:
True
}
)
def
test_edxnotes_tab_with_harvard_notes
(
self
,
harvard_notes_enabled
):
"""
Verify EdxNotesTab visibility when harvard notes feature is enabled/disabled.
"""
with
patch
(
"edxnotes.plugins.is_harvard_notes_enabled"
)
as
mock_harvard_notes_enabled
:
mock_harvard_notes_enabled
.
return_value
=
harvard_notes_enabled
self
.
assertEqual
(
EdxNotesTab
.
is_enabled
(
self
.
course
),
not
harvard_notes_enabled
)
lms/djangoapps/edxnotes/urls.py
View file @
770a45b7
...
...
@@ -7,7 +7,7 @@ from django.conf.urls import patterns, url
urlpatterns
=
patterns
(
"edxnotes.views"
,
url
(
r"^/$"
,
"edxnotes"
,
name
=
"edxnotes"
),
url
(
r"^/
search/$"
,
"search_notes"
,
name
=
"search_
notes"
),
url
(
r"^/
notes/$"
,
"notes"
,
name
=
"
notes"
),
url
(
r"^/token/$"
,
"get_token"
,
name
=
"get_token"
),
url
(
r"^/visibility/$"
,
"edxnotes_visibility"
,
name
=
"edxnotes_visibility"
),
)
lms/djangoapps/edxnotes/views.py
View file @
770a45b7
...
...
@@ -7,6 +7,7 @@ from django.contrib.auth.decorators import login_required
from
django.http
import
HttpResponse
,
HttpResponseBadRequest
,
Http404
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
django.views.decorators.http
import
require_GET
from
edxmako.shortcuts
import
render_to_response
from
opaque_keys.edx.keys
import
CourseKey
from
courseware.courses
import
get_course_with_access
...
...
@@ -18,8 +19,10 @@ from edxnotes.helpers import (
get_edxnotes_id_token
,
get_notes
,
is_feature_enabled
,
search
,
get_course_position
,
DEFAULT_PAGE
,
DEFAULT_PAGE_SIZE
,
NoteJSONEncoder
,
)
...
...
@@ -30,6 +33,13 @@ log = logging.getLogger(__name__)
def
edxnotes
(
request
,
course_id
):
"""
Displays the EdxNotes page.
Arguments:
request: HTTP request object
course_id: course id
Returns:
Rendered HTTP response.
"""
course_key
=
CourseKey
.
from_string
(
course_id
)
course
=
get_course_with_access
(
request
.
user
,
"load"
,
course_key
)
...
...
@@ -37,20 +47,20 @@ def edxnotes(request, course_id):
if
not
is_feature_enabled
(
course
):
raise
Http404
try
:
notes
=
get_notes
(
request
.
user
,
course
)
except
EdxNotesServiceUnavailable
:
raise
Http404
notes_info
=
get_notes
(
request
,
course
)
has_notes
=
(
len
(
notes_info
.
get
(
'results'
))
>
0
)
context
=
{
"course"
:
course
,
"search_endpoint"
:
reverse
(
"search_notes"
,
kwargs
=
{
"course_id"
:
course_id
}),
"notes"
:
notes
,
"debug"
:
json
.
dumps
(
settings
.
DEBUG
),
"notes_endpoint"
:
reverse
(
"notes"
,
kwargs
=
{
"course_id"
:
course_id
}),
"notes"
:
notes_info
,
"page_size"
:
DEFAULT_PAGE_SIZE
,
"debug"
:
settings
.
DEBUG
,
'position'
:
None
,
'disabled_tabs'
:
settings
.
NOTES_DISABLED_TABS
,
'has_notes'
:
has_notes
,
}
if
not
notes
:
if
not
has_
notes
:
field_data_cache
=
FieldDataCache
.
cache_for_descriptor_descendents
(
course
.
id
,
request
.
user
,
course
,
depth
=
2
)
...
...
@@ -66,27 +76,97 @@ def edxnotes(request, course_id):
return
render_to_response
(
"edxnotes/edxnotes.html"
,
context
)
@require_GET
@login_required
def
search_
notes
(
request
,
course_id
):
def
notes
(
request
,
course_id
):
"""
Handles search requests.
Notes view to handle list and search requests.
Query parameters:
page: page number to get
page_size: number of items in the page
text: text string to search. If `text` param is missing then get all the
notes for the current user for this course else get only those notes
which contain the `text` value.
Arguments:
request: HTTP request object
course_id: course id
Returns:
Paginated response as JSON. A sample response is below.
{
"count": 101,
"num_pages": 11,
"current_page": 1,
"results": [
{
"chapter": {
"index": 4,
"display_name": "About Exams and Certificates",
"location": "i4x://org/course/category/name@revision",
"children": [
"i4x://org/course/category/name@revision"
]
},
"updated": "Dec 09, 2015 at 09:31 UTC",
"tags": ["shadow","oil"],
"quote": "foo bar baz",
"section": {
"display_name": "edX Exams",
"location": "i4x://org/course/category/name@revision",
"children": [
"i4x://org/course/category/name@revision",
"i4x://org/course/category/name@revision",
]
},
"created": "2015-12-09T09:31:17.338305Z",
"ranges": [
{
"start": "/div[1]/p[1]",
"end": "/div[1]/p[1]",
"startOffset": 0,
"endOffset": 6
}
],
"user": "50cf92f9a3d8489df95e583549b919df",
"text": "first angry height hungry structure",
"course_id": "edx/DemoX/Demo",
"id": "1231",
"unit": {
"url": "/courses/edx
%2
FDemoX
%2
FDemo/courseware/1414ffd5143b4b508f739b563ab468b7/workflow/1",
"display_name": "EdX Exams",
"location": "i4x://org/course/category/name@revision"
},
"usage_id": "i4x://org/course/category/name@revision"
} ],
"next": "http://0.0.0.0:8000/courses/edx
%2
FDemoX
%2
FDemo/edxnotes/notes/?page=2&page_size=10",
"start": 0,
"previous": null
}
"""
course_key
=
CourseKey
.
from_string
(
course_id
)
course
=
get_course_with_access
(
request
.
user
,
"load"
,
course_key
)
course
=
get_course_with_access
(
request
.
user
,
'load'
,
course_key
)
if
not
is_feature_enabled
(
course
):
raise
Http404
if
"text"
not
in
request
.
GET
:
return
HttpResponseBadRequest
()
page
=
request
.
GET
.
get
(
'page'
)
or
DEFAULT_PAGE
page_size
=
request
.
GET
.
get
(
'page_size'
)
or
DEFAULT_PAGE_SIZE
text
=
request
.
GET
.
get
(
'text'
)
query_string
=
request
.
GET
[
"text"
]
try
:
search_results
=
search
(
request
.
user
,
course
,
query_string
)
notes_info
=
get_notes
(
request
,
course
,
page
=
page
,
page_size
=
page_size
,
text
=
text
)
except
(
EdxNotesParseError
,
EdxNotesServiceUnavailable
)
as
err
:
return
JsonResponseBadRequest
({
"error"
:
err
.
message
},
status
=
500
)
return
HttpResponse
(
search_results
)
return
HttpResponse
(
json
.
dumps
(
notes_info
,
cls
=
NoteJSONEncoder
),
content_type
=
"application/json"
)
# pylint: disable=unused-argument
...
...
lms/envs/bok_choy.py
View file @
770a45b7
...
...
@@ -91,6 +91,8 @@ XQUEUE_INTERFACE['url'] = 'http://localhost:8040'
EDXNOTES_PUBLIC_API
=
'http://localhost:8042/api/v1'
EDXNOTES_INTERNAL_API
=
'http://localhost:8042/api/v1'
NOTES_DISABLED_TABS
=
[]
# Silence noisy logs
import
logging
LOG_OVERRIDES
=
[
...
...
lms/envs/common.py
View file @
770a45b7
...
...
@@ -2542,6 +2542,9 @@ ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT = 60
if
FEATURES
[
'ENABLE_EDXNOTES'
]:
OAUTH_ID_TOKEN_EXPIRATION
=
60
*
60
# These tabs are currently disabled
NOTES_DISABLED_TABS
=
[
'course_structure'
,
'tags'
]
# Configuration used for generating PDF Receipts/Invoices
PDF_RECEIPT_TAX_ID
=
'add here'
PDF_RECEIPT_FOOTER_TEXT
=
'add your own specific footer text here'
...
...
lms/envs/test.py
View file @
770a45b7
...
...
@@ -516,6 +516,8 @@ MONGODB_LOG = {
'db'
:
'xlog'
,
}
NOTES_DISABLED_TABS
=
[]
# Enable EdxNotes for tests.
FEATURES
[
'ENABLE_EDXNOTES'
]
=
True
...
...
lms/static/js/dashboard/track_events.js
View file @
770a45b7
...
...
@@ -34,6 +34,7 @@ var edx = edx || {};
// Emit an event when the 'course title link' is clicked.
edx
.
dashboard
.
trackCourseTitleClicked
=
function
(
$courseTitleLink
,
properties
){
var
trackProperty
=
properties
||
edx
.
dashboard
.
generateTrackProperties
;
window
.
analytics
.
trackLink
(
$courseTitleLink
,
'edx.bi.dashboard.course_title.clicked'
,
...
...
@@ -102,6 +103,7 @@ var edx = edx || {};
};
edx
.
dashboard
.
xseriesTrackMessages
=
function
()
{
$
(
'.xseries-action .btn'
).
each
(
function
(
i
,
element
)
{
var
data
=
edx
.
dashboard
.
generateProgramProperties
(
$
(
element
));
...
...
@@ -110,6 +112,9 @@ var edx = edx || {};
};
$
(
document
).
ready
(
function
()
{
if
(
!
window
.
analytics
)
{
return
;
}
edx
.
dashboard
.
trackCourseTitleClicked
(
$
(
'.course-title > a'
));
edx
.
dashboard
.
trackCourseImageLinkClicked
(
$
(
'.cover'
));
edx
.
dashboard
.
trackEnterCourseLinkClicked
(
$
(
'.enter-course'
));
...
...
lms/static/js/edxnotes/collections/notes.js
View file @
770a45b7
;(
function
(
define
,
undefined
)
{
;(
function
(
define
)
{
'use strict'
;
define
([
'
backbone
'
,
'js/edxnotes/models/note'
],
function
(
Backbone
,
NoteModel
)
{
var
NotesCollection
=
Backbone
.
Collection
.
extend
({
'
underscore'
,
'common/js/components/collections/paging_collection
'
,
'js/edxnotes/models/note'
],
function
(
_
,
PagingCollection
,
NoteModel
)
{
return
Paging
Collection
.
extend
({
model
:
NoteModel
,
initialize
:
function
(
models
,
options
)
{
PagingCollection
.
prototype
.
initialize
.
call
(
this
);
this
.
perPage
=
options
.
perPage
;
this
.
server_api
=
_
.
pick
(
PagingCollection
.
prototype
.
server_api
,
"page"
,
"page_size"
);
if
(
options
.
text
)
{
this
.
server_api
.
text
=
options
.
text
;
}
},
/**
* Returns course structure from the list of notes.
* @return {Object}
...
...
@@ -17,30 +27,26 @@ define([
sections
=
{},
units
=
{};
if
(
!
courseStructure
)
{
this
.
each
(
function
(
note
)
{
var
chapter
=
note
.
get
(
'chapter'
),
section
=
note
.
get
(
'section'
),
unit
=
note
.
get
(
'unit'
);
this
.
each
(
function
(
note
)
{
var
chapter
=
note
.
get
(
'chapter'
),
section
=
note
.
get
(
'section'
),
unit
=
note
.
get
(
'unit'
);
chapters
[
chapter
.
location
]
=
chapter
;
sections
[
section
.
location
]
=
section
;
units
[
unit
.
location
]
=
units
[
unit
.
location
]
||
[];
units
[
unit
.
location
].
push
(
note
);
});
chapters
[
chapter
.
location
]
=
chapter
;
sections
[
section
.
location
]
=
section
;
units
[
unit
.
location
]
=
units
[
unit
.
location
]
||
[];
units
[
unit
.
location
].
push
(
note
);
});
courseStructure
=
{
chapters
:
_
.
sortBy
(
_
.
toArray
(
chapters
),
function
(
c
)
{
return
c
.
index
;}),
sections
:
sections
,
units
:
units
};
}
courseStructure
=
{
chapters
:
_
.
sortBy
(
_
.
toArray
(
chapters
),
function
(
c
)
{
return
c
.
index
;}),
sections
:
sections
,
units
:
units
};
return
courseStructure
;
};
}())
});
return
NotesCollection
;
});
}).
call
(
this
,
define
||
RequireJS
.
define
);
lms/static/js/edxnotes/models/note.js
View file @
770a45b7
...
...
@@ -51,10 +51,6 @@ define(['backbone', 'js/edxnotes/utils/utils', 'underscore.string'], function (B
}
return
message
;
},
getText
:
function
()
{
return
Utils
.
nl2br
(
this
.
get
(
'text'
));
}
});
...
...
lms/static/js/edxnotes/plugins/store_error_handler.js
0 → 100644
View file @
770a45b7
(
function
(
define
,
undefined
)
{
'use strict'
;
define
([
'annotator_1.2.9'
],
function
(
Annotator
)
{
/**
* Modifies Annotator.Plugin.Store.prototype._onError to show custom error message
* if sent by server
*/
var
originalErrorHandler
=
Annotator
.
Plugin
.
Store
.
prototype
.
_onError
;
Annotator
.
Plugin
.
Store
.
prototype
.
_onError
=
function
(
xhr
)
{
var
serverResponse
;
// Try to parse json
if
(
xhr
.
responseText
)
{
try
{
serverResponse
=
JSON
.
parse
(
xhr
.
responseText
);
}
catch
(
exception
)
{
serverResponse
=
null
;
}
}
// if response includes an error message it will take precedence
if
(
serverResponse
&&
serverResponse
.
error_msg
)
{
Annotator
.
showNotification
(
serverResponse
.
error_msg
,
Annotator
.
Notification
.
ERROR
);
return
console
.
error
(
Annotator
.
_t
(
"API request failed:"
)
+
(
" '"
+
xhr
.
status
+
"'"
));
}
// Delegate to original error handler
originalErrorHandler
(
xhr
);
};
});
}).
call
(
this
,
define
||
RequireJS
.
define
);
lms/static/js/edxnotes/views/note_item.js
View file @
770a45b7
...
...
@@ -31,8 +31,7 @@ define([
getContext
:
function
()
{
return
$
.
extend
({},
this
.
model
.
toJSON
(),
{
message
:
this
.
model
.
getQuote
(),
text
:
this
.
model
.
getText
()
message
:
this
.
model
.
getQuote
()
});
},
...
...
@@ -60,7 +59,9 @@ define([
tagHandler
:
function
(
event
)
{
event
.
preventDefault
();
this
.
options
.
scrollToTag
(
event
.
currentTarget
.
text
);
if
(
!
_
.
isUndefined
(
this
.
options
.
scrollToTag
))
{
this
.
options
.
scrollToTag
(
event
.
currentTarget
.
text
);
}
},
redirectTo
:
function
(
uri
)
{
...
...
lms/static/js/edxnotes/views/notes_factory.js
View file @
770a45b7
...
...
@@ -4,7 +4,8 @@ define([
'jquery'
,
'underscore'
,
'annotator_1.2.9'
,
'js/edxnotes/utils/logger'
,
'js/edxnotes/views/shim'
,
'js/edxnotes/plugins/scroller'
,
'js/edxnotes/plugins/events'
,
'js/edxnotes/plugins/accessibility'
,
'js/edxnotes/plugins/caret_navigation'
'js/edxnotes/plugins/caret_navigation'
,
'js/edxnotes/plugins/store_error_handler'
],
function
(
$
,
_
,
Annotator
,
NotesLogger
)
{
var
plugins
=
[
'Auth'
,
'Store'
,
'Scroller'
,
'Events'
,
'Accessibility'
,
'CaretNavigation'
,
'Tags'
],
getOptions
,
setupPlugins
,
getAnnotator
;
...
...
lms/static/js/edxnotes/views/notes_page.js
View file @
770a45b7
...
...
@@ -15,17 +15,19 @@ define([
this
.
options
=
options
;
this
.
tabsCollection
=
new
TabsCollection
();
// Must create the Tags view first to get the "scrollToTag" method.
this
.
tagsView
=
new
TagsView
({
el
:
this
.
el
,
collection
:
this
.
collection
,
tabsCollection
:
this
.
tabsCollection
});
if
(
!
_
.
contains
(
this
.
options
.
disabledTabs
,
'tags'
))
{
// Must create the Tags view first to get the "scrollToTag" method.
this
.
tagsView
=
new
TagsView
({
el
:
this
.
el
,
collection
:
this
.
collection
,
tabsCollection
:
this
.
tabsCollection
});
scrollToTag
=
this
.
tagsView
.
scrollToTag
;
scrollToTag
=
this
.
tagsView
.
scrollToTag
;
// Remove the Tags model from the tabs collection because it should not appear first.
tagsModel
=
this
.
tabsCollection
.
shift
();
// Remove the Tags model from the tabs collection because it should not appear first.
tagsModel
=
this
.
tabsCollection
.
shift
();
}
this
.
recentActivityView
=
new
RecentActivityView
({
el
:
this
.
el
,
...
...
@@ -34,20 +36,25 @@ define([
scrollToTag
:
scrollToTag
});
this
.
courseStructureView
=
new
CourseStructureView
({
el
:
this
.
el
,
collection
:
this
.
collection
,
tabsCollection
:
this
.
tabsCollection
,
scrollToTag
:
scrollToTag
});
// Add the Tags model after the Course Structure model.
this
.
tabsCollection
.
push
(
tagsModel
);
if
(
!
_
.
contains
(
this
.
options
.
disabledTabs
,
'course_structure'
))
{
this
.
courseStructureView
=
new
CourseStructureView
({
el
:
this
.
el
,
collection
:
this
.
collection
,
tabsCollection
:
this
.
tabsCollection
,
scrollToTag
:
scrollToTag
});
}
if
(
!
_
.
contains
(
this
.
options
.
disabledTabs
,
'tags'
))
{
// Add the Tags model after the Course Structure model.
this
.
tabsCollection
.
push
(
tagsModel
);
}
this
.
searchResultsView
=
new
SearchResultsView
({
el
:
this
.
el
,
tabsCollection
:
this
.
tabsCollection
,
debug
:
this
.
options
.
debug
,
perPage
:
this
.
options
.
perPage
,
createTabOnInitialization
:
false
,
scrollToTag
:
scrollToTag
});
...
...
lms/static/js/edxnotes/views/page_factory.js
View file @
770a45b7
...
...
@@ -6,19 +6,29 @@ define([
/**
* Factory method for the Notes page.
* @param {Object} params Params for the Notes page.
* @param {Array} params.notesList A list of note models.
* @param {List} params.disabledTabs Names of disabled tabs, these tabs will not be shown.
* @param {Object} params.notes Paginated notes info.
* @param {Number} params.pageSize Number of notes per page.
* @param {Boolean} params.debugMode Enable the flag to see debug information.
* @param {String} params.endpoint The endpoint of the store.
* @return {Object} An instance of NotesPageView.
*/
return
function
(
params
)
{
var
collection
=
new
NotesCollection
(
params
.
notesList
);
var
collection
=
new
NotesCollection
(
params
.
notes
,
{
url
:
params
.
notesEndpoint
,
perPage
:
params
.
pageSize
,
parse
:
true
}
);
return
new
NotesPageView
({
el
:
$
(
'.wrapper-student-notes'
).
get
(
0
),
collection
:
collection
,
debug
:
params
.
debugMode
,
endpoint
:
params
.
endpoint
perPage
:
params
.
pageSize
,
disabledTabs
:
params
.
disabledTabs
});
};
});
...
...
lms/static/js/edxnotes/views/search_box.js
View file @
770a45b7
...
...
@@ -29,9 +29,15 @@ define([
this
.
logger
=
NotesLogger
.
getLogger
(
'search_box'
,
this
.
options
.
debug
);
this
.
$el
.
removeClass
(
'is-hidden'
);
this
.
isDisabled
=
false
;
this
.
searchInput
=
this
.
$el
.
find
(
'#search-notes-input'
);
this
.
logger
.
log
(
'initialized'
);
},
clearInput
:
function
()
{
// clear the search input box
this
.
searchInput
.
val
(
''
);
},
submitHandler
:
function
(
event
)
{
event
.
preventDefault
();
this
.
search
();
...
...
@@ -43,15 +49,12 @@ define([
* @return {Array}
*/
prepareData
:
function
(
data
)
{
var
collection
;
if
(
!
(
data
&&
_
.
has
(
data
,
'total'
)
&&
_
.
has
(
data
,
'rows'
)))
{
if
(
!
(
data
&&
_
.
has
(
data
,
'count'
)
&&
_
.
has
(
data
,
'results'
)))
{
this
.
logger
.
log
(
'Wrong data'
,
data
,
this
.
searchQuery
);
return
null
;
}
collection
=
new
NotesCollection
(
data
.
rows
);
return
[
collection
,
data
.
total
,
this
.
searchQuery
];
return
[
this
.
collection
,
this
.
searchQuery
];
},
/**
...
...
@@ -99,8 +102,8 @@ define([
if
(
args
)
{
this
.
options
.
search
.
apply
(
this
,
args
);
this
.
logger
.
emit
(
'edx.course.student_notes.searched'
,
{
'number_of_results'
:
args
[
1
]
,
'search_string'
:
args
[
2
]
'number_of_results'
:
args
[
0
].
totalCount
,
'search_string'
:
args
[
1
]
});
}
else
{
this
.
options
.
error
(
this
.
errorMessage
,
this
.
searchQuery
);
...
...
@@ -144,15 +147,15 @@ define([
* @return {jQuery.Deferred}
*/
sendRequest
:
function
(
text
)
{
var
settings
=
{
url
:
this
.
el
.
action
,
type
:
this
.
el
.
method
,
dataType
:
'json'
,
data
:
{
text
:
text
}
};
this
.
logger
.
log
(
settings
);
return
$
.
ajax
(
settings
);
this
.
collection
=
new
NotesCollection
(
[]
,
{
text
:
text
,
perPage
:
this
.
options
.
perPage
,
url
:
this
.
el
.
action
}
);
return
this
.
collection
.
goTo
(
1
);
}
});
...
...
lms/static/js/edxnotes/views/tab_panel.js
View file @
770a45b7
;(
function
(
define
,
undefined
)
{
'use strict'
;
define
([
'gettext'
,
'underscore'
,
'backbone'
,
'js/edxnotes/views/note_item'
],
function
(
gettext
,
_
,
Backbone
,
NoteItemView
)
{
define
([
'gettext'
,
'underscore'
,
'backbone'
,
'js/edxnotes/views/note_item'
,
'common/js/components/views/paging_header'
,
'common/js/components/views/paging_footer'
],
function
(
gettext
,
_
,
Backbone
,
NoteItemView
,
PagingHeaderView
,
PagingFooterView
)
{
var
TabPanelView
=
Backbone
.
View
.
extend
({
tagName
:
'section'
,
className
:
'tab-panel'
,
...
...
@@ -13,14 +14,30 @@ function (gettext, _, Backbone, NoteItemView) {
initialize
:
function
()
{
this
.
children
=
[];
if
(
this
.
options
.
createHeaderFooter
)
{
this
.
pagingHeaderView
=
new
PagingHeaderView
({
collection
:
this
.
collection
});
this
.
pagingFooterView
=
new
PagingFooterView
({
collection
:
this
.
collection
,
hideWhenOnePage
:
true
});
}
if
(
this
.
hasOwnProperty
(
'collection'
))
{
this
.
listenTo
(
this
.
collection
,
'page_changed'
,
this
.
render
);
}
},
render
:
function
()
{
this
.
$el
.
html
(
this
.
getTitle
());
this
.
renderView
(
this
.
pagingHeaderView
);
this
.
renderContent
();
this
.
renderView
(
this
.
pagingFooterView
);
return
this
;
},
renderView
:
function
(
view
)
{
if
(
this
.
options
.
createHeaderFooter
&&
this
.
collection
.
models
.
length
)
{
this
.
$el
.
append
(
view
.
render
().
el
);
view
.
delegateEvents
();
}
},
renderContent
:
function
()
{
return
this
;
},
...
...
lms/static/js/edxnotes/views/tab_view.js
View file @
770a45b7
...
...
@@ -14,7 +14,8 @@ define([
initialize
:
function
(
options
)
{
_
.
bindAll
(
this
,
'showLoadingIndicator'
,
'hideLoadingIndicator'
);
this
.
options
=
_
.
defaults
(
options
||
{},
{
createTabOnInitialization
:
true
createTabOnInitialization
:
true
,
createHeaderFooter
:
true
});
if
(
this
.
options
.
createTabOnInitialization
)
{
...
...
@@ -64,7 +65,13 @@ define([
getSubView
:
function
()
{
var
collection
=
this
.
getCollection
();
return
new
this
.
PanelConstructor
({
collection
:
collection
,
scrollToTag
:
this
.
options
.
scrollToTag
});
return
new
this
.
PanelConstructor
(
{
collection
:
collection
,
scrollToTag
:
this
.
options
.
scrollToTag
,
createHeaderFooter
:
this
.
options
.
createHeaderFooter
}
);
},
destroySubView
:
function
()
{
...
...
lms/static/js/edxnotes/views/tabs/search_results.js
View file @
770a45b7
...
...
@@ -58,6 +58,7 @@ define([
this
.
searchBox
=
new
SearchBoxView
({
el
:
document
.
getElementById
(
'search-notes-form'
),
debug
:
this
.
options
.
debug
,
perPage
:
this
.
options
.
perPage
,
beforeSearchStart
:
this
.
onBeforeSearchStart
,
search
:
this
.
onSearch
,
error
:
this
.
onSearchError
...
...
@@ -81,7 +82,8 @@ define([
return
new
this
.
PanelConstructor
({
collection
:
collection
,
searchQuery
:
this
.
searchResults
.
searchQuery
,
scrollToTag
:
this
.
options
.
scrollToTag
scrollToTag
:
this
.
options
.
scrollToTag
,
createHeaderFooter
:
this
.
options
.
createHeaderFooter
});
}
else
{
return
new
this
.
NoResultsViewConstructor
({
...
...
@@ -103,6 +105,7 @@ define([
onClose
:
function
()
{
this
.
searchResults
=
null
;
this
.
searchBox
.
clearInput
();
},
onBeforeSearchStart
:
function
()
{
...
...
@@ -122,10 +125,9 @@ define([
}
},
onSearch
:
function
(
collection
,
total
,
searchQuery
)
{
onSearch
:
function
(
collection
,
searchQuery
)
{
this
.
searchResults
=
{
collection
:
collection
,
total
:
total
,
searchQuery
:
searchQuery
};
...
...
lms/static/js/spec/edxnotes/collections/notes_spec.js
View file @
770a45b7
...
...
@@ -6,7 +6,7 @@ define([
var
notes
=
Helpers
.
getDefaultNotes
();
beforeEach
(
function
()
{
this
.
collection
=
new
NotesCollection
(
notes
);
this
.
collection
=
new
NotesCollection
(
notes
,
{
perPage
:
10
,
parse
:
true
}
);
});
it
(
'can return correct course structure'
,
function
()
{
...
...
@@ -23,11 +23,22 @@ define([
'i4x://section/2'
:
Helpers
.
getSection
(
'First Section'
,
2
,
[
3
])
});
expect
(
structure
.
units
).
toEqual
({
var
compareUnits
=
function
(
structureUnits
,
collectionUnits
)
{
expect
(
structureUnits
.
length
===
collectionUnits
.
length
).
toBeTruthy
();
for
(
var
i
=
0
;
i
<
structureUnits
.
length
;
i
++
)
{
expect
(
structureUnits
[
i
].
attributes
).
toEqual
(
collectionUnits
[
i
].
attributes
);
}
};
var
units
=
{
'i4x://unit/0'
:
[
this
.
collection
.
at
(
0
),
this
.
collection
.
at
(
1
)],
'i4x://unit/1'
:
[
this
.
collection
.
at
(
2
)],
'i4x://unit/2'
:
[
this
.
collection
.
at
(
3
)],
'i4x://unit/3'
:
[
this
.
collection
.
at
(
4
)]
};
_
.
each
(
units
,
function
(
value
,
key
){
compareUnits
(
structure
.
units
[
key
],
value
);
});
});
});
...
...
lms/static/js/spec/edxnotes/helpers.js
View file @
770a45b7
define
([
'underscore'
],
function
(
_
)
{
define
([
'underscore'
,
'URI'
,
'common/js/spec_helpers/ajax_helpers'
],
function
(
_
,
URI
,
AjaxHelpers
)
{
'use strict'
;
var
B64
=
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
,
LONG_TEXT
,
PRUNED_TEXT
,
TRUNCATED_TEXT
,
SHORT_TEXT
,
base64Encode
,
makeToken
,
getChapter
,
getSection
,
getUnit
,
getDefaultNotes
;
base64Encode
,
makeToken
,
getChapter
,
getSection
,
getUnit
,
getDefaultNotes
,
verifyUrl
,
verifyRequestParams
,
createNotesData
,
respondToRequest
,
verifyPaginationInfo
,
verifyPageData
;
LONG_TEXT
=
[
'Adipisicing elit, sed do eiusmod tempor incididunt '
,
...
...
@@ -106,57 +108,134 @@ define(['underscore'], function(_) {
getDefaultNotes
=
function
()
{
// Note that the server returns notes in reverse chronological order (newest first).
return
[
{
chapter
:
getChapter
(
'Second Chapter'
,
0
,
1
,
[
1
,
'w_n'
,
0
]),
section
:
getSection
(
'Third Section'
,
0
,
[
'w_n'
,
1
,
0
]),
unit
:
getUnit
(
'Fourth Unit'
,
0
),
created
:
'December 11, 2014 at 11:12AM'
,
updated
:
'December 11, 2014 at 11:12AM'
,
text
:
'Third added model'
,
quote
:
'Note 4'
,
tags
:
[
'Pumpkin'
,
'pumpkin'
,
'yummy'
]
},
{
chapter
:
getChapter
(
'Second Chapter'
,
0
,
1
,
[
1
,
'w_n'
,
0
]),
section
:
getSection
(
'Third Section'
,
0
,
[
'w_n'
,
1
,
0
]),
unit
:
getUnit
(
'Fourth Unit'
,
0
),
created
:
'December 11, 2014 at 11:11AM'
,
updated
:
'December 11, 2014 at 11:11AM'
,
text
:
'Third added model'
,
quote
:
'Note 5'
},
{
chapter
:
getChapter
(
'Second Chapter'
,
0
,
1
,
[
1
,
'w_n'
,
0
]),
section
:
getSection
(
'Third Section'
,
0
,
[
'w_n'
,
1
,
0
]),
unit
:
getUnit
(
'Third Unit'
,
1
),
created
:
'December 11, 2014 at 11:11AM'
,
updated
:
'December 11, 2014 at 11:11AM'
,
text
:
'Second added model'
,
quote
:
'Note 3'
,
tags
:
[
'yummy'
]
},
{
chapter
:
getChapter
(
'Second Chapter'
,
0
,
1
,
[
1
,
'w_n'
,
0
]),
section
:
getSection
(
'Second Section'
,
1
,
[
2
]),
unit
:
getUnit
(
'Second Unit'
,
2
),
created
:
'December 11, 2014 at 11:10AM'
,
updated
:
'December 11, 2014 at 11:10AM'
,
text
:
'First added model'
,
quote
:
'Note 2'
,
tags
:
[
'PUMPKIN'
,
'pie'
]
},
{
chapter
:
getChapter
(
'First Chapter'
,
1
,
0
,
[
2
]),
section
:
getSection
(
'First Section'
,
2
,
[
3
]),
unit
:
getUnit
(
'First Unit'
,
3
),
created
:
'December 11, 2014 at 11:10AM'
,
updated
:
'December 11, 2014 at 11:10AM'
,
text
:
'First added model'
,
quote
:
'Note 1'
,
tags
:
[
'pie'
,
'pumpkin'
]
}
];
return
{
'count'
:
5
,
'current_page'
:
1
,
'num_pages'
:
1
,
'start'
:
0
,
'next'
:
null
,
'previous'
:
null
,
'results'
:
[
{
chapter
:
getChapter
(
'Second Chapter'
,
0
,
1
,
[
1
,
'w_n'
,
0
]),
section
:
getSection
(
'Third Section'
,
0
,
[
'w_n'
,
1
,
0
]),
unit
:
getUnit
(
'Fourth Unit'
,
0
),
created
:
'December 11, 2014 at 11:12AM'
,
updated
:
'December 11, 2014 at 11:12AM'
,
text
:
'Third added model'
,
quote
:
'Note 4'
,
tags
:
[
'Pumpkin'
,
'pumpkin'
,
'yummy'
]
},
{
chapter
:
getChapter
(
'Second Chapter'
,
0
,
1
,
[
1
,
'w_n'
,
0
]),
section
:
getSection
(
'Third Section'
,
0
,
[
'w_n'
,
1
,
0
]),
unit
:
getUnit
(
'Fourth Unit'
,
0
),
created
:
'December 11, 2014 at 11:11AM'
,
updated
:
'December 11, 2014 at 11:11AM'
,
text
:
'Third added model'
,
quote
:
'Note 5'
},
{
chapter
:
getChapter
(
'Second Chapter'
,
0
,
1
,
[
1
,
'w_n'
,
0
]),
section
:
getSection
(
'Third Section'
,
0
,
[
'w_n'
,
1
,
0
]),
unit
:
getUnit
(
'Third Unit'
,
1
),
created
:
'December 11, 2014 at 11:11AM'
,
updated
:
'December 11, 2014 at 11:11AM'
,
text
:
'Second added model'
,
quote
:
'Note 3'
,
tags
:
[
'yummy'
]
},
{
chapter
:
getChapter
(
'Second Chapter'
,
0
,
1
,
[
1
,
'w_n'
,
0
]),
section
:
getSection
(
'Second Section'
,
1
,
[
2
]),
unit
:
getUnit
(
'Second Unit'
,
2
),
created
:
'December 11, 2014 at 11:10AM'
,
updated
:
'December 11, 2014 at 11:10AM'
,
text
:
'First added model'
,
quote
:
'Note 2'
,
tags
:
[
'PUMPKIN'
,
'pie'
]
},
{
chapter
:
getChapter
(
'First Chapter'
,
1
,
0
,
[
2
]),
section
:
getSection
(
'First Section'
,
2
,
[
3
]),
unit
:
getUnit
(
'First Unit'
,
3
),
created
:
'December 11, 2014 at 11:10AM'
,
updated
:
'December 11, 2014 at 11:10AM'
,
text
:
'First added model'
,
quote
:
'Note 1'
,
tags
:
[
'pie'
,
'pumpkin'
]
}
]
};
};
verifyUrl
=
function
(
requestUrl
,
expectedUrl
,
expectedParams
)
{
expect
(
requestUrl
.
slice
(
0
,
expectedUrl
.
length
)
===
expectedUrl
).
toBeTruthy
();
verifyRequestParams
(
requestUrl
,
expectedParams
);
};
verifyRequestParams
=
function
(
requestUrl
,
expectedParams
)
{
var
urlParams
=
(
new
URI
(
requestUrl
)).
query
(
true
);
_
.
each
(
expectedParams
,
function
(
value
,
key
)
{
expect
(
urlParams
[
key
]).
toBe
(
value
);
});
};
createNotesData
=
function
(
options
)
{
var
data
=
{
count
:
options
.
count
||
0
,
num_pages
:
options
.
num_pages
||
1
,
current_page
:
options
.
current_page
||
1
,
start
:
options
.
start
||
0
,
results
:
[]
};
for
(
var
i
=
0
;
i
<
options
.
numNotesToCreate
;
i
++
)
{
var
notesInfo
=
{
chapter
:
getChapter
(
'First Chapter__'
+
i
,
1
,
0
,
[
2
]),
section
:
getSection
(
'First Section__'
+
i
,
2
,
[
3
]),
unit
:
getUnit
(
'First Unit__'
+
i
,
3
),
created
:
new
Date
().
toISOString
(),
updated
:
new
Date
().
toISOString
(),
text
:
'text__'
+
i
,
quote
:
'Note__'
+
i
,
tags
:
[
'tag__'
+
i
,
'tag__'
+
i
+
1
]
};
data
.
results
.
push
(
notesInfo
);
}
return
data
;
};
respondToRequest
=
function
(
requests
,
responseJson
,
respondToEvent
)
{
// Respond to the analytics event
if
(
respondToEvent
)
{
AjaxHelpers
.
respondWithNoContent
(
requests
);
}
// Now process the actual request
AjaxHelpers
.
respondWithJson
(
requests
,
responseJson
);
};
verifyPaginationInfo
=
function
(
view
,
headerMessage
,
footerHidden
,
currentPage
,
totalPages
)
{
expect
(
view
.
$
(
'.search-count.listing-count'
).
text
().
trim
()).
toBe
(
headerMessage
);
expect
(
view
.
$
(
'.pagination.bottom'
).
parent
().
hasClass
(
'hidden'
)).
toBe
(
footerHidden
);
if
(
!
footerHidden
)
{
expect
(
parseInt
(
view
.
$
(
'.pagination span.current-page'
).
text
().
trim
())).
toBe
(
currentPage
);
expect
(
parseInt
(
view
.
$
(
'.pagination span.total-pages'
).
text
().
trim
())).
toBe
(
totalPages
);
}
};
verifyPageData
=
function
(
view
,
tabsCollection
,
tabInfo
,
tabId
,
notes
)
{
expect
(
tabsCollection
).
toHaveLength
(
1
);
expect
(
tabsCollection
.
at
(
0
).
toJSON
()).
toEqual
(
tabInfo
);
expect
(
view
.
$
(
tabId
)).
toExist
();
expect
(
view
.
$
(
'.note'
)).
toHaveLength
(
notes
.
results
.
length
);
_
.
each
(
view
.
$
(
'.note'
),
function
(
element
,
index
)
{
expect
(
$
(
'.note-comments'
,
element
)).
toContainText
(
notes
.
results
[
index
].
text
);
expect
(
$
(
'.note-excerpt'
,
element
)).
toContainText
(
notes
.
results
[
index
].
quote
);
});
};
return
{
...
...
@@ -169,6 +248,12 @@ define(['underscore'], function(_) {
getChapter
:
getChapter
,
getSection
:
getSection
,
getUnit
:
getUnit
,
getDefaultNotes
:
getDefaultNotes
getDefaultNotes
:
getDefaultNotes
,
verifyUrl
:
verifyUrl
,
verifyRequestParams
:
verifyRequestParams
,
createNotesData
:
createNotesData
,
respondToRequest
:
respondToRequest
,
verifyPaginationInfo
:
verifyPaginationInfo
,
verifyPageData
:
verifyPageData
};
});
lms/static/js/spec/edxnotes/models/note_spec.js
View file @
770a45b7
...
...
@@ -4,10 +4,23 @@ define([
'use strict'
;
describe
(
'EdxNotes NoteModel'
,
function
()
{
beforeEach
(
function
()
{
this
.
collection
=
new
NotesCollection
([
{
quote
:
Helpers
.
LONG_TEXT
,
text
:
'text
\
n with
\
r
\
nline
\
n
\
rbreaks
\
r'
},
{
quote
:
Helpers
.
SHORT_TEXT
,
text
:
'text
\
n with
\
r
\
nline
\
n
\
rbreaks
\
r'
}
]);
this
.
collection
=
new
NotesCollection
(
{
'count'
:
2
,
'current_page'
:
1
,
'num_pages'
:
1
,
'start'
:
0
,
'next'
:
null
,
'previous'
:
null
,
'results'
:
[
{
quote
:
Helpers
.
LONG_TEXT
,
text
:
'text
\
n with
\
r
\
nline
\
n
\
rbreaks
\
r'
},
{
quote
:
Helpers
.
SHORT_TEXT
,
text
:
'text
\
n with
\
r
\
nline
\
n
\
rbreaks
\
r'
}
]
},
{
perPage
:
10
,
parse
:
true
}
);
});
it
(
'has correct values on initialization'
,
function
()
{
...
...
@@ -33,7 +46,7 @@ define([
it
(
'can return appropriate `text`'
,
function
()
{
var
model
=
this
.
collection
.
at
(
0
);
expect
(
model
.
get
Text
()).
toBe
(
'text<br> with<br>line<br>breaks <br>
'
);
expect
(
model
.
get
(
'text'
)).
toBe
(
'text
\
n with
\
r
\
nline
\
n
\
rbreaks
\
r
'
);
});
});
});
lms/static/js/spec/edxnotes/plugins/store_error_handler_spec.js
0 → 100644
View file @
770a45b7
define
([
'jquery'
,
'underscore'
,
'annotator_1.2.9'
,
'common/js/spec_helpers/ajax_helpers'
,
'js/spec/edxnotes/helpers'
,
'js/edxnotes/views/notes_factory'
],
function
(
$
,
_
,
Annotator
,
AjaxHelpers
,
Helpers
,
NotesFactory
)
{
'use strict'
;
describe
(
'Store Error Handler Custom Message'
,
function
()
{
beforeEach
(
function
()
{
spyOn
(
Annotator
,
'showNotification'
);
loadFixtures
(
'js/fixtures/edxnotes/edxnotes_wrapper.html'
);
this
.
wrapper
=
document
.
getElementById
(
'edx-notes-wrapper-123'
);
});
afterEach
(
function
()
{
_
.
invoke
(
Annotator
.
_instances
,
'destroy'
);
});
it
(
'can handle custom error if sent from server'
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
);
var
token
=
Helpers
.
makeToken
();
NotesFactory
.
factory
(
this
.
wrapper
,
{
endpoint
:
'/test_endpoint'
,
user
:
'a user'
,
usageId
:
'an usage'
,
courseId
:
'a course'
,
token
:
token
,
tokenUrl
:
'/test_token_url'
});
var
errorMsg
=
'can
\'
t create more notes'
;
AjaxHelpers
.
respondWithError
(
requests
,
400
,
{
error_msg
:
errorMsg
});
expect
(
Annotator
.
showNotification
).
toHaveBeenCalledWith
(
errorMsg
,
Annotator
.
Notification
.
ERROR
);
});
});
});
lms/static/js/spec/edxnotes/views/note_item_spec.js
View file @
770a45b7
...
...
@@ -9,14 +9,14 @@ define([
)
{
'use strict'
;
describe
(
'EdxNotes NoteItemView'
,
function
()
{
var
getView
=
function
(
model
,
scrollToTag
)
{
var
getView
=
function
(
model
,
scrollToTag
,
formattedText
)
{
model
=
new
NoteModel
(
_
.
defaults
(
model
||
{},
{
id
:
'id-123'
,
user
:
'user-123'
,
usage_id
:
'usage_id-123'
,
created
:
'December 11, 2014 at 11:12AM'
,
updated
:
'December 11, 2014 at 11:12AM'
,
text
:
'Third added model'
,
text
:
formattedText
||
'Third added model'
,
quote
:
Helpers
.
LONG_TEXT
,
unit
:
{
url
:
'http://example.com/'
...
...
@@ -67,12 +67,42 @@ define([
var
view
=
getView
({
tags
:
[
"First"
,
"Second"
]});
expect
(
view
.
$
(
'.reference-title'
).
length
).
toBe
(
3
);
expect
(
view
.
$
(
'.reference-title'
)[
2
]).
toContainText
(
'Tags:'
);
expect
(
view
.
$
(
'
a
.reference-tags'
).
length
).
toBe
(
2
);
expect
(
view
.
$
(
'
a
.reference-tags'
)[
0
]).
toContainText
(
'First'
);
expect
(
view
.
$
(
'
a
.reference-tags'
)[
1
]).
toContainText
(
'Second'
);
expect
(
view
.
$
(
'
span
.reference-tags'
).
length
).
toBe
(
2
);
expect
(
view
.
$
(
'
span
.reference-tags'
)[
0
]).
toContainText
(
'First'
);
expect
(
view
.
$
(
'
span
.reference-tags'
)[
1
]).
toContainText
(
'Second'
);
});
it
(
'should handle a click event on the tag'
,
function
()
{
it
(
'should highlight tags & text if they have elasticsearch formatter'
,
function
()
{
var
view
=
getView
({
tags
:
[
"First"
,
"{elasticsearch_highlight_start}Second{elasticsearch_highlight_end}"
]
},
{},
"{elasticsearch_highlight_start}Sample{elasticsearch_highlight_end}"
);
expect
(
view
.
$
(
'.reference-title'
).
length
).
toBe
(
3
);
expect
(
view
.
$
(
'.reference-title'
)[
2
]).
toContainText
(
'Tags:'
);
expect
(
view
.
$
(
'span.reference-tags'
).
length
).
toBe
(
2
);
expect
(
view
.
$
(
'span.reference-tags'
)[
0
]).
toContainText
(
'First'
);
// highlighted tag & text
expect
(
$
.
trim
(
$
(
view
.
$
(
'span.reference-tags'
)[
1
]).
html
())).
toBe
(
'<span class="note-highlight">Second</span>'
);
expect
(
$
.
trim
(
view
.
$
(
'.note-comment-p'
).
html
())).
toBe
(
'<span class="note-highlight">Sample</span>'
);
});
it
(
'should escape html for tags & comments'
,
function
()
{
var
view
=
getView
({
tags
:
[
"First"
,
"<b>Second</b>"
,
"ȗnicode"
]
},
{},
"<b>Sample</b>"
);
expect
(
view
.
$
(
'.reference-title'
).
length
).
toBe
(
3
);
expect
(
view
.
$
(
'.reference-title'
)[
2
]).
toContainText
(
'Tags:'
);
expect
(
view
.
$
(
'span.reference-tags'
).
length
).
toBe
(
3
);
expect
(
view
.
$
(
'span.reference-tags'
)[
0
]).
toContainText
(
'First'
);
expect
(
$
.
trim
(
$
(
view
.
$
(
'span.reference-tags'
)[
1
]).
html
())).
toBe
(
'<b>Second</b>'
);
expect
(
$
.
trim
(
$
(
view
.
$
(
'span.reference-tags'
)[
2
]).
html
())).
toBe
(
'ȗnicode'
);
expect
(
$
.
trim
(
view
.
$
(
'.note-comment-p'
).
html
())).
toBe
(
'<b>Sample</b>'
);
});
xit
(
'should handle a click event on the tag'
,
function
()
{
var
scrollToTagSpy
=
{
scrollToTag
:
function
(
tagName
){}
};
...
...
lms/static/js/spec/edxnotes/views/notes_page_spec.js
View file @
770a45b7
...
...
@@ -13,7 +13,7 @@ define([
TemplateHelpers
.
installTemplates
([
'templates/edxnotes/note-item'
,
'templates/edxnotes/tab-item'
]);
this
.
view
=
new
NotesFactory
({
notes
List
:
notes
});
this
.
view
=
new
NotesFactory
({
notes
:
notes
,
pageSize
:
10
});
});
...
...
@@ -35,8 +35,13 @@ define([
this
.
view
.
$
(
'.search-notes-input'
).
val
(
'test_query'
);
this
.
view
.
$
(
'.search-notes-submit'
).
click
();
AjaxHelpers
.
respondWithJson
(
requests
,
{
total
:
0
,
rows
:
[]
'count'
:
0
,
'current_page'
:
1
,
'num_pages'
:
1
,
'start'
:
0
,
'next'
:
null
,
'previous'
:
null
,
'results'
:
[]
});
expect
(
this
.
view
.
$
(
'#view-search-results'
)).
toHaveClass
(
'is-active'
);
expect
(
this
.
view
.
$
(
'#view-recent-activity'
)).
toExist
();
...
...
lms/static/js/spec/edxnotes/views/search_box_spec.js
View file @
770a45b7
define
([
'jquery'
,
'underscore'
,
'common/js/spec_helpers/ajax_helpers'
,
'js/edxnotes/views/search_box'
,
'js/edxnotes/collections/notes'
,
'js/spec/edxnotes/custom_matchers'
,
'jasmine-jquery'
],
function
(
$
,
_
,
AjaxHelpers
,
SearchBoxView
,
NotesCollection
,
customMatchers
)
{
'js/edxnotes/collections/notes'
,
'js/spec/edxnotes/custom_matchers'
,
'j
s/spec/edxnotes/helpers'
,
'j
asmine-jquery'
],
function
(
$
,
_
,
AjaxHelpers
,
SearchBoxView
,
NotesCollection
,
customMatchers
,
Helpers
)
{
'use strict'
;
describe
(
'EdxNotes SearchBoxView'
,
function
()
{
var
getSearchBox
,
submitForm
,
assertBoxIsEnabled
,
assertBoxIsDisabled
;
var
getSearchBox
,
submitForm
,
assertBoxIsEnabled
,
assertBoxIsDisabled
,
searchResponse
;
searchResponse
=
{
'count'
:
2
,
'current_page'
:
1
,
'num_pages'
:
1
,
'start'
:
0
,
'next'
:
null
,
'previous'
:
null
,
'results'
:
[
null
,
null
]
};
getSearchBox
=
function
(
options
)
{
options
=
_
.
defaults
(
options
||
{},
{
el
:
$
(
'#search-notes-form'
).
get
(
0
),
perPage
:
10
,
beforeSearchStart
:
jasmine
.
createSpy
(),
search
:
jasmine
.
createSpy
(),
error
:
jasmine
.
createSpy
(),
...
...
@@ -50,7 +61,11 @@ define([
submitForm
(
this
.
searchBox
,
'test_text'
);
request
=
requests
[
0
];
expect
(
request
.
method
).
toBe
(
form
.
method
.
toUpperCase
());
expect
(
request
.
url
).
toBe
(
form
.
action
+
'?'
+
$
.
param
({
text
:
'test_text'
}));
Helpers
.
verifyUrl
(
request
.
url
,
form
.
action
,
{
text
:
'test_text'
,
page
:
'1'
,
page_size
:
'10'
}
);
});
it
(
'returns success result'
,
function
()
{
...
...
@@ -60,13 +75,10 @@ define([
'test_text'
);
assertBoxIsDisabled
(
this
.
searchBox
);
AjaxHelpers
.
respondWithJson
(
requests
,
{
total
:
2
,
rows
:
[
null
,
null
]
});
AjaxHelpers
.
respondWithJson
(
requests
,
searchResponse
);
assertBoxIsEnabled
(
this
.
searchBox
);
expect
(
this
.
searchBox
.
options
.
search
).
toHaveBeenCalledWith
(
jasmine
.
any
(
NotesCollection
),
2
,
'test_text'
jasmine
.
any
(
NotesCollection
),
'test_text'
);
expect
(
this
.
searchBox
.
options
.
complete
).
toHaveBeenCalledWith
(
'test_text'
...
...
@@ -76,10 +88,7 @@ define([
it
(
'should log the edx.course.student_notes.searched event properly'
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
);
submitForm
(
this
.
searchBox
,
'test_text'
);
AjaxHelpers
.
respondWithJson
(
requests
,
{
total
:
2
,
rows
:
[
null
,
null
]
});
AjaxHelpers
.
respondWithJson
(
requests
,
searchResponse
);
expect
(
Logger
.
log
).
toHaveBeenCalledWith
(
'edx.course.student_notes.searched'
,
{
'number_of_results'
:
2
,
...
...
@@ -140,10 +149,7 @@ define([
submitForm
(
this
.
searchBox
,
'test_text'
);
assertBoxIsDisabled
(
this
.
searchBox
);
submitForm
(
this
.
searchBox
,
'another_text'
);
AjaxHelpers
.
respondWithJson
(
requests
,
{
total
:
2
,
rows
:
[
null
,
null
]
});
AjaxHelpers
.
respondWithJson
(
requests
,
searchResponse
);
assertBoxIsEnabled
(
this
.
searchBox
);
expect
(
requests
).
toHaveLength
(
1
);
});
...
...
@@ -158,5 +164,11 @@ define([
' '
);
});
it
(
'can clear its input box'
,
function
()
{
this
.
searchBox
.
$
(
'.search-notes-input'
).
val
(
'search me'
);
this
.
searchBox
.
clearInput
();
expect
(
this
.
searchBox
.
$
(
'#search-notes-input'
).
val
()).
toEqual
(
''
);
});
});
});
lms/static/js/spec/edxnotes/views/tabs/course_structure_spec.js
View file @
770a45b7
...
...
@@ -40,7 +40,7 @@ define([
'templates/edxnotes/note-item'
,
'templates/edxnotes/tab-item'
]);
this
.
collection
=
new
NotesCollection
(
notes
);
this
.
collection
=
new
NotesCollection
(
notes
,
{
perPage
:
10
,
parse
:
true
}
);
this
.
tabsCollection
=
new
TabsCollection
();
});
...
...
lms/static/js/spec/edxnotes/views/tabs/recent_activity_spec.js
View file @
770a45b7
define
([
'jquery'
,
'common/js/spec_helpers/template_helpers'
,
'
js/edxnotes/collections/note
s'
,
'js/edxnotes/collections/tabs'
,
'js/edxnotes/views/tabs/recent_activity'
,
'js/spec/edxnotes/custom_matchers'
,
'jasmine-jquery'
'jquery'
,
'common/js/spec_helpers/template_helpers'
,
'
common/js/spec_helpers/ajax_helper
s'
,
'js/edxnotes/collections/
notes'
,
'js/edxnotes/collections/
tabs'
,
'js/edxnotes/views/tabs/recent_activity'
,
'js/spec/edxnotes/custom_matchers'
,
'j
s/spec/edxnotes/helpers'
,
'j
asmine-jquery'
],
function
(
$
,
TemplateHelpers
,
NotesCollection
,
TabsCollection
,
RecentActivityView
,
customMatch
ers
$
,
TemplateHelpers
,
AjaxHelpers
,
NotesCollection
,
TabsCollection
,
RecentActivityView
,
customMatchers
,
Help
ers
)
{
'use strict'
;
describe
(
'EdxNotes RecentActivityView'
,
function
()
{
var
notes
=
[
{
created
:
'December 11, 2014 at 11:12AM'
,
updated
:
'December 11, 2014 at 11:12AM'
,
text
:
'Third added model'
,
quote
:
'Should be listed first'
},
{
created
:
'December 11, 2014 at 11:11AM'
,
updated
:
'December 11, 2014 at 11:11AM'
,
text
:
'Second added model'
,
quote
:
'Should be listed second'
},
{
created
:
'December 11, 2014 at 11:10AM'
,
updated
:
'December 11, 2014 at 11:10AM'
,
text
:
'First added model'
,
quote
:
'Should be listed third'
}
],
getView
;
var
notes
=
{
'count'
:
3
,
'current_page'
:
1
,
'num_pages'
:
1
,
'start'
:
0
,
'next'
:
null
,
'previous'
:
null
,
'results'
:
[
{
created
:
'December 11, 2014 at 11:12AM'
,
updated
:
'December 11, 2014 at 11:12AM'
,
text
:
'Third added model'
,
quote
:
'Should be listed first'
},
{
created
:
'December 11, 2014 at 11:11AM'
,
updated
:
'December 11, 2014 at 11:11AM'
,
text
:
'Second added model'
,
quote
:
'Should be listed second'
},
{
created
:
'December 11, 2014 at 11:10AM'
,
updated
:
'December 11, 2014 at 11:10AM'
,
text
:
'First added model'
,
quote
:
'Should be listed third'
}
]
},
getView
,
tabInfo
,
recentActivityTabId
;
getView
=
function
(
collection
,
tabsCollection
,
options
)
{
var
view
;
...
...
@@ -35,6 +43,7 @@ define([
el
:
$
(
'.wrapper-student-notes'
),
collection
:
collection
,
tabsCollection
:
tabsCollection
,
createHeaderFooter
:
true
});
view
=
new
RecentActivityView
(
options
);
...
...
@@ -43,6 +52,17 @@ define([
return
view
;
};
tabInfo
=
{
name
:
'Recent Activity'
,
identifier
:
'view-recent-activity'
,
icon
:
'fa fa-clock-o'
,
is_active
:
true
,
is_closable
:
false
,
view
:
'Recent Activity'
};
recentActivityTabId
=
'#recent-panel'
;
beforeEach
(
function
()
{
customMatchers
(
this
);
loadFixtures
(
'js/fixtures/edxnotes/edxnotes.html'
);
...
...
@@ -50,28 +70,136 @@ define([
'templates/edxnotes/note-item'
,
'templates/edxnotes/tab-item'
]);
this
.
collection
=
new
NotesCollection
(
notes
);
this
.
collection
=
new
NotesCollection
(
notes
,
{
perPage
:
10
,
parse
:
true
}
);
this
.
tabsCollection
=
new
TabsCollection
();
});
it
(
'displays a tab and content with proper data and order'
,
function
()
{
var
view
=
getView
(
this
.
collection
,
this
.
tabsCollection
);
Helpers
.
verifyPaginationInfo
(
view
,
"Showing 1-3 out of 3 total"
,
true
,
1
,
1
);
Helpers
.
verifyPageData
(
view
,
this
.
tabsCollection
,
tabInfo
,
recentActivityTabId
,
notes
);
});
expect
(
this
.
tabsCollection
).
toHaveLength
(
1
);
expect
(
this
.
tabsCollection
.
at
(
0
).
toJSON
()).
toEqual
({
name
:
'Recent Activity'
,
identifier
:
'view-recent-activity'
,
icon
:
'fa fa-clock-o'
,
is_active
:
true
,
is_closable
:
false
,
view
:
'Recent Activity'
});
expect
(
view
.
$
(
'#recent-panel'
)).
toExist
();
expect
(
view
.
$
(
'.note'
)).
toHaveLength
(
3
);
_
.
each
(
view
.
$
(
'.note'
),
function
(
element
,
index
)
{
expect
(
$
(
'.note-comments'
,
element
)).
toContainText
(
notes
[
index
].
text
);
expect
(
$
(
'.note-excerpt'
,
element
)).
toContainText
(
notes
[
index
].
quote
);
});
it
(
"will not render header and footer if there are no notes"
,
function
()
{
var
notes
=
{
'count'
:
0
,
'current_page'
:
1
,
'num_pages'
:
1
,
'start'
:
0
,
'next'
:
null
,
'previous'
:
null
,
'results'
:
[]
};
var
collection
=
new
NotesCollection
(
notes
,
{
perPage
:
10
,
parse
:
true
});
var
view
=
getView
(
collection
,
this
.
tabsCollection
);
expect
(
view
.
$
(
'.search-tools.listing-tools'
)).
toHaveLength
(
0
);
expect
(
view
.
$
(
'.pagination.pagination-full.bottom'
)).
toHaveLength
(
0
);
});
it
(
"can go to a page number"
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
);
var
notes
=
Helpers
.
createNotesData
(
{
numNotesToCreate
:
10
,
count
:
12
,
num_pages
:
2
,
current_page
:
1
,
start
:
0
}
);
var
collection
=
new
NotesCollection
(
notes
,
{
perPage
:
10
,
parse
:
true
});
var
view
=
getView
(
collection
,
this
.
tabsCollection
);
Helpers
.
verifyPaginationInfo
(
view
,
"Showing 1-10 out of 12 total"
,
false
,
1
,
2
);
Helpers
.
verifyPageData
(
view
,
this
.
tabsCollection
,
tabInfo
,
recentActivityTabId
,
notes
);
view
.
$
(
'input#page-number-input'
).
val
(
'2'
);
view
.
$
(
'input#page-number-input'
).
trigger
(
'change'
);
Helpers
.
verifyRequestParams
(
requests
[
requests
.
length
-
1
].
url
,
{
page
:
'2'
,
page_size
:
'10'
}
);
notes
=
Helpers
.
createNotesData
(
{
numNotesToCreate
:
2
,
count
:
12
,
num_pages
:
2
,
current_page
:
2
,
start
:
10
}
);
Helpers
.
respondToRequest
(
requests
,
notes
,
true
);
Helpers
.
verifyPaginationInfo
(
view
,
"Showing 11-12 out of 12 total"
,
false
,
2
,
2
);
Helpers
.
verifyPageData
(
view
,
this
.
tabsCollection
,
tabInfo
,
recentActivityTabId
,
notes
);
});
it
(
"can navigate forward and backward"
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
);
var
page1Notes
=
Helpers
.
createNotesData
(
{
numNotesToCreate
:
10
,
count
:
15
,
num_pages
:
2
,
current_page
:
1
,
start
:
0
}
);
var
collection
=
new
NotesCollection
(
page1Notes
,
{
perPage
:
10
,
parse
:
true
});
var
view
=
getView
(
collection
,
this
.
tabsCollection
);
Helpers
.
verifyPaginationInfo
(
view
,
"Showing 1-10 out of 15 total"
,
false
,
1
,
2
);
Helpers
.
verifyPageData
(
view
,
this
.
tabsCollection
,
tabInfo
,
recentActivityTabId
,
page1Notes
);
view
.
$
(
'.pagination .next-page-link'
).
click
();
Helpers
.
verifyRequestParams
(
requests
[
requests
.
length
-
1
].
url
,
{
page
:
'2'
,
page_size
:
'10'
}
);
var
page2Notes
=
Helpers
.
createNotesData
(
{
numNotesToCreate
:
5
,
count
:
15
,
num_pages
:
2
,
current_page
:
2
,
start
:
10
}
);
Helpers
.
respondToRequest
(
requests
,
page2Notes
,
true
);
Helpers
.
verifyPaginationInfo
(
view
,
"Showing 11-15 out of 15 total"
,
false
,
2
,
2
);
Helpers
.
verifyPageData
(
view
,
this
.
tabsCollection
,
tabInfo
,
recentActivityTabId
,
page2Notes
);
view
.
$
(
'.pagination .previous-page-link'
).
click
();
Helpers
.
verifyRequestParams
(
requests
[
requests
.
length
-
1
].
url
,
{
page
:
'1'
,
page_size
:
'10'
}
);
Helpers
.
respondToRequest
(
requests
,
page1Notes
);
Helpers
.
verifyPaginationInfo
(
view
,
"Showing 1-10 out of 15 total"
,
false
,
1
,
2
);
Helpers
.
verifyPageData
(
view
,
this
.
tabsCollection
,
tabInfo
,
recentActivityTabId
,
page1Notes
);
});
it
(
"sends correct page size value"
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
);
var
notes
=
Helpers
.
createNotesData
(
{
numNotesToCreate
:
5
,
count
:
7
,
num_pages
:
2
,
current_page
:
1
,
start
:
0
}
);
var
collection
=
new
NotesCollection
(
notes
,
{
perPage
:
5
,
parse
:
true
});
var
view
=
getView
(
collection
,
this
.
tabsCollection
);
view
.
$
(
'.pagination .next-page-link'
).
click
();
Helpers
.
verifyRequestParams
(
requests
[
requests
.
length
-
1
].
url
,
{
page
:
'2'
,
page_size
:
'5'
}
);
});
});
});
lms/static/js/spec/edxnotes/views/tabs/search_results_spec.js
View file @
770a45b7
define
([
'jquery'
,
'underscore'
,
'common/js/spec_helpers/template_helpers'
,
'common/js/spec_helpers/ajax_helpers'
,
'logger'
,
'js/edxnotes/collections/tabs'
,
'js/edxnotes/views/tabs/search_results'
,
'js/spec/edxnotes/custom_matchers'
,
'jasmine-jquery'
'js/spec/edxnotes/custom_matchers'
,
'j
s/spec/edxnotes/helpers'
,
'j
asmine-jquery'
],
function
(
$
,
_
,
TemplateHelpers
,
AjaxHelpers
,
Logger
,
TabsCollection
,
SearchResultsView
,
customMatchers
customMatchers
,
Helpers
)
{
'use strict'
;
describe
(
'EdxNotes SearchResultsView'
,
function
()
{
...
...
@@ -29,18 +29,25 @@ define([
}
],
responseJson
=
{
total
:
3
,
rows
:
notes
'count'
:
3
,
'current_page'
:
1
,
'num_pages'
:
1
,
'start'
:
0
,
'next'
:
null
,
'previous'
:
null
,
'results'
:
notes
},
getView
,
submitForm
,
respondToSearch
;
getView
,
submitForm
,
tabInfo
,
searchResultsTabId
;
getView
=
function
(
tabsCollection
,
options
)
{
getView
=
function
(
tabsCollection
,
perPage
,
options
)
{
options
=
_
.
defaults
(
options
||
{},
{
el
:
$
(
'.wrapper-student-notes'
),
tabsCollection
:
tabsCollection
,
user
:
'test_user'
,
courseId
:
'course_id'
,
createTabOnInitialization
:
false
createTabOnInitialization
:
false
,
createHeaderFooter
:
true
,
perPage
:
perPage
||
10
});
return
new
SearchResultsView
(
options
);
};
...
...
@@ -50,14 +57,17 @@ define([
searchBox
.
$
(
'.search-notes-submit'
).
click
();
};
respondToSearch
=
function
(
requests
,
responseJson
)
{
// First respond to the analytics event
AjaxHelpers
.
respondWithNoContent
(
requests
);
// Now process the search request
AjaxHelpers
.
respondWithJson
(
requests
,
responseJson
);
tabInfo
=
{
name
:
'Search Results'
,
identifier
:
'view-search-results'
,
icon
:
'fa fa-search'
,
is_active
:
true
,
is_closable
:
true
,
view
:
'Search Results'
};
searchResultsTabId
=
"#search-results-panel"
;
beforeEach
(
function
()
{
customMatchers
(
this
);
loadFixtures
(
'js/fixtures/edxnotes/edxnotes.html'
);
...
...
@@ -79,23 +89,9 @@ define([
requests
=
AjaxHelpers
.
requests
(
this
);
submitForm
(
view
.
searchBox
,
'second'
);
respondToSearch
(
requests
,
responseJson
);
expect
(
this
.
tabsCollection
).
toHaveLength
(
1
);
expect
(
this
.
tabsCollection
.
at
(
0
).
toJSON
()).
toEqual
({
name
:
'Search Results'
,
identifier
:
'view-search-results'
,
icon
:
'fa fa-search'
,
is_active
:
true
,
is_closable
:
true
,
view
:
'Search Results'
});
expect
(
view
.
$
(
'#search-results-panel'
)).
toExist
();
expect
(
view
.
$
(
'#search-results-panel'
)).
toBeFocused
();
expect
(
view
.
$
(
'.note'
)).
toHaveLength
(
3
);
view
.
searchResults
.
collection
.
each
(
function
(
model
,
index
)
{
expect
(
model
.
get
(
'text'
)).
toBe
(
notes
[
index
].
text
);
});
Helpers
.
respondToRequest
(
requests
,
responseJson
,
true
);
Helpers
.
verifyPageData
(
view
,
this
.
tabsCollection
,
tabInfo
,
searchResultsTabId
,
responseJson
);
Helpers
.
verifyPaginationInfo
(
view
,
"Showing 1-3 out of 3 total"
,
true
,
1
,
1
);
});
it
(
'displays loading indicator when search is running'
,
function
()
{
...
...
@@ -108,7 +104,7 @@ define([
expect
(
this
.
tabsCollection
).
toHaveLength
(
1
);
expect
(
view
.
searchResults
).
toBeNull
();
expect
(
view
.
$
(
'.tab-panel'
)).
not
.
toExist
();
respondToSearch
(
requests
,
responseJson
);
Helpers
.
respondToRequest
(
requests
,
responseJson
,
true
);
expect
(
view
.
$
(
'.ui-loading'
)).
toHaveClass
(
'is-hidden'
);
});
...
...
@@ -117,10 +113,7 @@ define([
requests
=
AjaxHelpers
.
requests
(
this
);
submitForm
(
view
.
searchBox
,
'some text'
);
respondToSearch
(
requests
,
{
total
:
0
,
rows
:
[]
});
Helpers
.
respondToRequest
(
requests
,
_
.
extend
(
_
.
clone
(
responseJson
),
{
count
:
0
,
results
:
[]}),
true
);
expect
(
view
.
$
(
'#search-results-panel'
)).
not
.
toExist
();
expect
(
view
.
$
(
'#no-results-panel'
)).
toBeFocused
();
...
...
@@ -153,12 +146,14 @@ define([
it
(
'can clear search results if tab is closed'
,
function
()
{
var
view
=
getView
(
this
.
tabsCollection
),
requests
=
AjaxHelpers
.
requests
(
this
);
spyOn
(
view
.
searchBox
,
'clearInput'
).
andCallThrough
();
submitForm
(
view
.
searchBox
,
'test_query'
);
respondToSearch
(
requests
,
responseJson
);
Helpers
.
respondToRequest
(
requests
,
responseJson
,
true
);
expect
(
view
.
searchResults
).
toBeDefined
();
this
.
tabsCollection
.
at
(
0
).
destroy
();
expect
(
view
.
searchResults
).
toBeNull
();
expect
(
view
.
searchBox
.
clearInput
).
toHaveBeenCalled
();
});
it
(
'can correctly show/hide error messages'
,
function
()
{
...
...
@@ -195,20 +190,140 @@ define([
}];
submitForm
(
view
.
searchBox
,
'test_query'
);
respondToSearch
(
requests
,
responseJson
);
Helpers
.
respondToRequest
(
requests
,
responseJson
,
true
);
expect
(
view
.
$
(
'.note'
)).
toHaveLength
(
3
);
submitForm
(
view
.
searchBox
,
'new_test_query'
);
respondToSearch
(
requests
,
{
total
:
1
,
rows
:
newNotes
});
Helpers
.
respondToRequest
(
requests
,
{
'count'
:
1
,
'current_page'
:
1
,
'num_pages'
:
1
,
'start'
:
0
,
'next'
:
null
,
'previous'
:
null
,
'results'
:
newNotes
},
true
);
expect
(
view
.
$
(
'.note'
).
length
).
toHaveLength
(
1
);
view
.
searchResults
.
collection
.
each
(
function
(
model
,
index
)
{
expect
(
model
.
get
(
'text'
)).
toBe
(
newNotes
[
index
].
text
);
});
});
it
(
"will not render header and footer if there are no notes"
,
function
()
{
var
view
=
getView
(
this
.
tabsCollection
),
requests
=
AjaxHelpers
.
requests
(
this
),
notes
=
{
'count'
:
0
,
'current_page'
:
1
,
'num_pages'
:
1
,
'start'
:
0
,
'next'
:
null
,
'previous'
:
null
,
'results'
:
[]
};
submitForm
(
view
.
searchBox
,
'awesome'
);
Helpers
.
respondToRequest
(
requests
,
notes
,
true
);
expect
(
view
.
$
(
'.search-tools.listing-tools'
)).
toHaveLength
(
0
);
expect
(
view
.
$
(
'.pagination.pagination-full.bottom'
)).
toHaveLength
(
0
);
});
it
(
"can go to a page number"
,
function
()
{
var
view
=
getView
(
this
.
tabsCollection
),
requests
=
AjaxHelpers
.
requests
(
this
),
notes
=
Helpers
.
createNotesData
(
{
numNotesToCreate
:
10
,
count
:
12
,
num_pages
:
2
,
current_page
:
1
,
start
:
0
}
);
submitForm
(
view
.
searchBox
,
'awesome'
);
Helpers
.
respondToRequest
(
requests
,
notes
,
true
);
Helpers
.
verifyPaginationInfo
(
view
,
"Showing 1-10 out of 12 total"
,
false
,
1
,
2
);
Helpers
.
verifyPageData
(
view
,
this
.
tabsCollection
,
tabInfo
,
searchResultsTabId
,
notes
);
view
.
$
(
'input#page-number-input'
).
val
(
'2'
);
view
.
$
(
'input#page-number-input'
).
trigger
(
'change'
);
Helpers
.
verifyRequestParams
(
requests
[
requests
.
length
-
1
].
url
,
{
page
:
'2'
,
page_size
:
'10'
}
);
notes
=
Helpers
.
createNotesData
(
{
numNotesToCreate
:
2
,
count
:
12
,
num_pages
:
2
,
current_page
:
2
,
start
:
10
}
);
Helpers
.
respondToRequest
(
requests
,
notes
,
true
);
Helpers
.
verifyPaginationInfo
(
view
,
"Showing 11-12 out of 12 total"
,
false
,
2
,
2
);
Helpers
.
verifyPageData
(
view
,
this
.
tabsCollection
,
tabInfo
,
searchResultsTabId
,
notes
);
});
it
(
"can navigate forward and backward"
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
),
page1Notes
=
Helpers
.
createNotesData
(
{
numNotesToCreate
:
10
,
count
:
15
,
num_pages
:
2
,
current_page
:
1
,
start
:
0
}
),
view
=
getView
(
this
.
tabsCollection
);
submitForm
(
view
.
searchBox
,
'awesome'
);
Helpers
.
respondToRequest
(
requests
,
page1Notes
,
true
);
Helpers
.
verifyPaginationInfo
(
view
,
"Showing 1-10 out of 15 total"
,
false
,
1
,
2
);
Helpers
.
verifyPageData
(
view
,
this
.
tabsCollection
,
tabInfo
,
searchResultsTabId
,
page1Notes
);
view
.
$
(
'.pagination .next-page-link'
).
click
();
Helpers
.
verifyRequestParams
(
requests
[
requests
.
length
-
1
].
url
,
{
page
:
'2'
,
page_size
:
'10'
}
);
var
page2Notes
=
Helpers
.
createNotesData
(
{
numNotesToCreate
:
5
,
count
:
15
,
num_pages
:
2
,
current_page
:
2
,
start
:
10
}
);
Helpers
.
respondToRequest
(
requests
,
page2Notes
,
true
);
Helpers
.
verifyPaginationInfo
(
view
,
"Showing 11-15 out of 15 total"
,
false
,
2
,
2
);
Helpers
.
verifyPageData
(
view
,
this
.
tabsCollection
,
tabInfo
,
searchResultsTabId
,
page2Notes
);
view
.
$
(
'.pagination .previous-page-link'
).
click
();
Helpers
.
verifyRequestParams
(
requests
[
requests
.
length
-
1
].
url
,
{
page
:
'1'
,
page_size
:
'10'
}
);
Helpers
.
respondToRequest
(
requests
,
page1Notes
);
Helpers
.
verifyPaginationInfo
(
view
,
"Showing 1-10 out of 15 total"
,
false
,
1
,
2
);
Helpers
.
verifyPageData
(
view
,
this
.
tabsCollection
,
tabInfo
,
searchResultsTabId
,
page1Notes
);
});
it
(
"sends correct page size value"
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
),
view
=
getView
(
this
.
tabsCollection
,
5
);
submitForm
(
view
.
searchBox
,
'awesome'
);
Helpers
.
verifyRequestParams
(
requests
[
requests
.
length
-
1
].
url
,
{
page
:
'1'
,
page_size
:
'5'
}
);
});
});
});
lms/static/js/spec/edxnotes/views/tabs/tags_spec.js
View file @
770a45b7
...
...
@@ -44,7 +44,7 @@ define([
'templates/edxnotes/note-item'
,
'templates/edxnotes/tab-item'
]);
this
.
collection
=
new
NotesCollection
(
notes
);
this
.
collection
=
new
NotesCollection
(
notes
,
{
perPage
:
10
,
parse
:
true
}
);
this
.
tabsCollection
=
new
TabsCollection
();
});
...
...
lms/static/js/spec/main.js
View file @
770a45b7
...
...
@@ -705,6 +705,7 @@
'lms/include/js/spec/edxnotes/plugins/events_spec.js'
,
'lms/include/js/spec/edxnotes/plugins/scroller_spec.js'
,
'lms/include/js/spec/edxnotes/plugins/caret_navigation_spec.js'
,
'lms/include/js/spec/edxnotes/plugins/store_error_handler_spec.js'
,
'lms/include/js/spec/edxnotes/collections/notes_spec.js'
,
'lms/include/js/spec/search/search_spec.js'
,
'lms/include/js/spec/navigation_spec.js'
,
...
...
lms/static/sass/course/_student-notes.scss
View file @
770a45b7
...
...
@@ -189,6 +189,10 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4;
background
:
transparent
;
}
.note-comment-p
{
word-wrap
:
break-word
;
}
.note-comment-ul
,
.note-comment-ol
{
padding
:
auto
;
...
...
@@ -233,29 +237,29 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4;
color
:
$m-gray-d2
;
}
// CASE: tag matches a search query
.reference-meta.reference-tags
.note-highlight
{
// needed because .note-highlight is a span, which overrides the color
@extend
%shame-link-text
;
background-color
:
$result-highlight-color-base
;
}
.reference-meta.reference-tags
{
word-wrap
:
break-word
;
// CASE: tag matches a search query
.note-highlight
{
background-color
:
$result-highlight-color-base
;
}
}
// Put commas between tags.
a
.reference-meta.reference-tags
:after
{
span
.reference-meta.reference-tags
:after
{
content
:
","
;
color
:
$m-gray-d2
;
}
// But not after the last tag.
a
.reference-meta.reference-tags
:last-child:after
{
span
.reference-meta.reference-tags
:last-child:after
{
content
:
""
;
}
// needed for poor base LMS styling scope
a
.reference-meta
{
@extend
%shame-link-text
;
@extend
%shame-link-text
;
}
}
}
...
...
@@ -285,6 +289,15 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4;
.tab-panel
,
.inline-error
,
.ui-loading
{
@extend
%no-outline
;
border-top
:
$divider-visual-primary
;
.listing-tools
{
@include
margin
(
$baseline
$baseline
(
-
$baseline
/
2
)
0
);
}
.note-group
:first-of-type
{
border-top
:
none
;
}
}
.tab-panel.note-group
{
...
...
lms/templates/edxnotes/edxnotes.html
View file @
770a45b7
<
%
page
expression_filter=
"h"
/>
<
%
inherit
file=
"/main.html"
/>
<
%
namespace
name=
'static'
file=
'/static_content.html'
/>
<
%!
from
django
.
utils
.
translation
import
ugettext
as
_
import
json
from
edxnotes
.
helpers
import
NoteJSONEncoder
from
openedx
.
core
.
djangolib
.
js_utils
import
dump_js_escaped_json
,
js_escaped_string
%
>
<
%
block
name=
"bodyclass"
>
view-student-notes is-in-course course
</
%
block>
...
...
@@ -12,6 +15,7 @@ import json
</
%
block>
<
%
include
file=
"/courseware/course_navigation.html"
args=
"active_page='edxnotes'"
/>
<section
class=
"container"
>
<div
class=
"wrapper-student-notes"
>
<div
class=
"student-notes"
>
...
...
@@ -24,9 +28,9 @@ import json
</h1>
</div>
% if notes:
% if
has_
notes:
<div
class=
"wrapper-notes-search"
>
<form
role=
"search"
action=
"${
search
_endpoint}"
method=
"GET"
id=
"search-notes-form"
class=
"is-hidden"
>
<form
role=
"search"
action=
"${
notes
_endpoint}"
method=
"GET"
id=
"search-notes-form"
class=
"is-hidden"
>
<label
for=
"search-notes-input"
class=
"sr"
>
${_('Search notes for:')}
</label>
<input
type=
"search"
class=
"search-notes-input"
id=
"search-notes-input"
name=
"note"
placeholder=
"${_('Search notes for...')}"
required
>
<button
type=
"submit"
class=
"search-notes-submit"
>
...
...
@@ -51,7 +55,7 @@ import json
<h2
id=
"tab-view"
class=
"tabs-label"
>
${_('View notes by:')}
</h2>
</div>
% if notes:
% if
has_
notes:
<div
class=
"ui-loading"
tabindex=
"-1"
>
<span
class=
"spin"
>
<i
class=
"icon fa fa-refresh"
></i>
...
...
@@ -103,12 +107,15 @@ import json
% endfor
</
%
block>
% if notes:
% if
has_
notes:
<
%
block
name=
"js_extra"
>
<
%
static:require_module
module_name=
"js/edxnotes/views/page_factory"
class_name=
"NotesPageFactory"
>
NotesPageFactory({
notesList: ${notes if notes is not None else []},
debugMode: ${debug}
disabledTabs: ${disabled_tabs | n, dump_js_escaped_json},
notes: ${dump_js_escaped_json(notes, NoteJSONEncoder) | n},
notesEndpoint: ${notes_endpoint | n, dump_js_escaped_json},
pageSize: ${page_size | n, dump_js_escaped_json},
debugMode: ${debug | n, dump_js_escaped_json}
});
</
%
static:require
_module
>
</
%
block>
...
...
lms/templates/edxnotes/note-item.underscore
View file @
770a45b7
<div class="wrapper-note-excerpts">
<% if (message) { %>
<div class="note-excerpt" role="region" aria-label="<%- gettext('Highlighted text') %>">
<p class="note-excerpt-p"><%
=
message %>
<p class="note-excerpt-p"><%
-
message %>
<% if (show_link) { %>
<% if (is_expanded) { %>
<a href="#" class="note-excerpt-more-link"><%- gettext('Less') %></a>
...
...
@@ -17,7 +17,12 @@
<ol class="note-comments" role="region" aria-label="<%- gettext('Note') %>">
<li class="note-comment">
<p class="note-comment-title"><%- gettext("You commented...") %></p>
<p class="note-comment-p"><%= text %></p>
<p class="note-comment-p">
<%= interpolate_text(_.escape(text), {
elasticsearch_highlight_start: '<span class="note-highlight">',
elasticsearch_highlight_end: '</span>'
})%>
</p>
</li>
</ol>
<% } %>
...
...
@@ -38,7 +43,12 @@
<% if (tags.length > 0) { %>
<p class="reference-title"><%- gettext("Tags:") %></p>
<% for (var i = 0; i < tags.length; i++) { %>
<a class="reference-meta reference-tags" href="#"><%= tags[i] %></a>
<span class="reference-meta reference-tags">
<%= interpolate_text(_.escape(tags[i]), {
elasticsearch_highlight_start: '<span class="note-highlight">',
elasticsearch_highlight_end: '</span>'
})%>
</span>
<% } %>
<% } %>
</div>
...
...
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