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
8c8367b0
Commit
8c8367b0
authored
Apr 09, 2014
by
Greg Price
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #3183 from edx/gprice/inline-response-pagination
Add repsonse pagination to inline discussions
parents
8609345b
16847d85
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
424 additions
and
221 deletions
+424
-221
common/djangoapps/terrain/stubs/comments.py
+12
-0
common/static/coffee/spec/discussion/view/discussion_thread_view_inline_spec.coffee
+89
-0
common/static/coffee/spec/discussion/view/discussion_thread_view_spec.coffee
+4
-12
common/static/coffee/spec/discussion/view/discussion_view_spec_helper.coffee
+7
-1
common/static/coffee/src/discussion/views/discussion_thread_view.coffee
+10
-5
common/static/coffee/src/discussion/views/discussion_thread_view_inline.coffee
+14
-78
common/static/js_test.yml
+1
-0
common/test/acceptance/pages/lms/courseware.py
+16
-0
common/test/acceptance/pages/lms/discussion.py
+100
-31
common/test/acceptance/tests/test_discussion.py
+157
-80
lms/static/sass/_discussion.scss
+0
-4
lms/templates/discussion/mustache/_inline_thread.mustache
+7
-5
lms/templates/discussion/mustache/_inline_thread_cohorted.mustache
+7
-5
No files found.
common/djangoapps/terrain/stubs/comments.py
View file @
8c8367b0
...
...
@@ -14,6 +14,7 @@ class StubCommentsServiceHandler(StubHttpRequestHandler):
"/api/v1/threads$"
:
self
.
do_threads
,
"/api/v1/threads/(?P<thread_id>
\\
w+)$"
:
self
.
do_thread
,
"/api/v1/comments/(?P<comment_id>
\\
w+)$"
:
self
.
do_comment
,
"/api/v1/(?P<commentable_id>
\\
w+)/threads$"
:
self
.
do_commentable
,
}
path
=
urlparse
.
urlparse
(
self
.
path
)
.
path
for
pattern
in
pattern_handlers
:
...
...
@@ -63,6 +64,17 @@ class StubCommentsServiceHandler(StubHttpRequestHandler):
comment
=
self
.
server
.
config
[
'comments'
][
comment_id
]
self
.
send_json_response
(
comment
)
def
do_commentable
(
self
,
commentable_id
):
self
.
send_json_response
({
"collection"
:
[
thread
for
thread
in
self
.
server
.
config
.
get
(
'threads'
,
{})
.
values
()
if
thread
.
get
(
'commentable_id'
)
==
commentable_id
],
"page"
:
1
,
"num_pages"
:
1
,
})
class
StubCommentsService
(
StubHttpService
):
HANDLER_CLASS
=
StubCommentsServiceHandler
common/static/coffee/spec/discussion/view/discussion_thread_view_inline_spec.coffee
0 → 100644
View file @
8c8367b0
describe
"DiscussionThreadInlineView"
,
->
beforeEach
->
setFixtures
(
"""
<script type="text/template" id="_inline_thread">
<article class="discussion-article">
<div class="non-cohorted-indicator"/>
<div class="post-extended-content">
<div class="response-count"/>
<ol class="responses"/>
<div class="response-pagination"/>
</div>
<div class="post-tools">
<a href="javascript:void(0)" class="expand-post">Expand</a>
<a href="javascript:void(0)" class="collapse-post">Collapse</a>
</div>
</article>
</script>
<script type="text/template" id="_inline_thread_cohorted">
<article class="discussion-article">
<div class="cohorted-indicator"/>
<div class="post-extended-content">
<div class="response-count"/>
<ol class="responses"/>
<div class="response-pagination"/>
</div>
<div class="post-tools">
<a href="javascript:void(0)" class="expand-post">Expand</a>
<a href="javascript:void(0)" class="collapse-post">Collapse</a>
</div>
</article>
</script>
<div class="thread-fixture"/>
"""
)
@
threadData
=
{
id
:
"dummy"
,
body
:
"dummy body"
,
abuse_flaggers
:
[],
votes
:
{
up_count
:
"42"
}
}
@
thread
=
new
Thread
(
@
threadData
)
@
view
=
new
DiscussionThreadInlineView
({
model
:
@
thread
})
@
view
.
setElement
(
$
(
".thread-fixture"
))
spyOn
(
$
,
"ajax"
)
# Avoid unnecessary boilerplate
spyOn
(
@
view
.
showView
,
"render"
)
spyOn
(
@
view
,
"makeWmdEditor"
)
spyOn
(
DiscussionThreadView
.
prototype
,
"renderResponse"
)
assertContentVisible
=
(
view
,
selector
,
visible
)
->
content
=
view
.
$el
.
find
(
selector
)
expect
(
content
.
length
).
toEqual
(
1
)
expect
(
content
.
is
(
":visible"
)).
toEqual
(
visible
)
assertExpandedContentVisible
=
(
view
,
expanded
)
->
expect
(
view
.
$el
.
hasClass
(
"expanded"
)).
toEqual
(
expanded
)
assertContentVisible
(
view
,
".post-extended-content"
,
expanded
)
assertContentVisible
(
view
,
".expand-post"
,
not
expanded
)
assertContentVisible
(
view
,
".collapse-post"
,
expanded
)
describe
"render"
,
->
it
"uses the cohorted template if cohorted"
,
->
@
view
.
model
.
set
({
group_id
:
1
})
@
view
.
render
()
expect
(
@
view
.
$el
.
find
(
".cohorted-indicator"
).
length
).
toEqual
(
1
)
it
"uses the non-cohorted template if not cohorted"
,
->
@
view
.
render
()
expect
(
@
view
.
$el
.
find
(
".non-cohorted-indicator"
).
length
).
toEqual
(
1
)
it
"shows content that should be visible when collapsed"
,
->
@
view
.
render
()
assertExpandedContentVisible
(
@
view
,
false
)
it
"does not render any responses by default"
,
->
@
view
.
render
()
expect
(
$
.
ajax
).
not
.
toHaveBeenCalled
()
expect
(
@
view
.
$el
.
find
(
".responses li"
).
length
).
toEqual
(
0
)
describe
"expand/collapse"
,
->
it
"shows/hides appropriate content"
,
->
DiscussionViewSpecHelper
.
setNextResponseContent
({
resp_total
:
0
,
children
:
[]})
@
view
.
render
()
@
view
.
expandPost
()
assertExpandedContentVisible
(
@
view
,
true
)
@
view
.
collapsePost
()
assertExpandedContentVisible
(
@
view
,
false
)
common/static/coffee/spec/discussion/view/discussion_thread_view_spec.coffee
View file @
8c8367b0
...
...
@@ -27,16 +27,8 @@ describe "DiscussionThreadView", ->
spyOn
(
DiscussionThreadView
.
prototype
,
"renderResponse"
)
describe
"response count and pagination"
,
->
setNextResponseContent
=
(
content
)
->
$
.
ajax
.
andCallFake
(
(
params
)
=>
params
.
success
({
"content"
:
content
})
{
always
:
->
}
)
renderWithContent
=
(
view
,
content
)
->
setNextResponseContent
(
content
)
DiscussionViewSpecHelper
.
setNextResponseContent
(
content
)
view
.
render
()
jasmine
.
Clock
.
tick
(
100
)
...
...
@@ -73,16 +65,16 @@ describe "DiscussionThreadView", ->
assertRenderedCorrectly
(
@
view
,
"5 responses"
,
"Showing first response"
,
"Load all responses"
)
it
"correctly re-render when all threads have loaded"
,
->
setNextResponseContent
({
resp_total
:
5
,
children
:
[{},
{},
{},
{}]})
DiscussionViewSpecHelper
.
setNextResponseContent
({
resp_total
:
5
,
children
:
[{},
{},
{},
{}]})
@
view
.
$el
.
find
(
".load-response-button"
).
click
()
assertRenderedCorrectly
(
@
view
,
"5 responses"
,
"Showing all responses"
,
null
)
it
"correctly re-render when one page remains"
,
->
setNextResponseContent
({
resp_total
:
42
,
children
:
[{},
{}]})
DiscussionViewSpecHelper
.
setNextResponseContent
({
resp_total
:
42
,
children
:
[{},
{}]})
@
view
.
$el
.
find
(
".load-response-button"
).
click
()
assertRenderedCorrectly
(
@
view
,
"42 responses"
,
"Showing first 3 responses"
,
"Load all responses"
)
it
"correctly re-render when multiple pages remain"
,
->
setNextResponseContent
({
resp_total
:
111
,
children
:
[{},
{}]})
DiscussionViewSpecHelper
.
setNextResponseContent
({
resp_total
:
111
,
children
:
[{},
{}]})
@
view
.
$el
.
find
(
".load-response-button"
).
click
()
assertRenderedCorrectly
(
@
view
,
"111 responses"
,
"Showing first 3 responses"
,
"Load next 100 responses"
)
common/static/coffee/spec/discussion/view/discussion_view_spec_helper.coffee
View file @
8c8367b0
...
...
@@ -110,6 +110,12 @@ class @DiscussionViewSpecHelper
button
.
trigger
(
$
.
Event
(
"keydown"
,
{
which
:
32
}))
expect
(
spy
).
toHaveBeenCalled
()
@
checkVoteButtonEvents
=
(
view
)
->
@
checkButtonEvents
(
view
,
"toggleVote"
,
".vote-btn"
)
@
setNextResponseContent
=
(
content
)
->
$
.
ajax
.
andCallFake
(
(
params
)
=>
params
.
success
({
"content"
:
content
})
{
always
:
->
}
)
common/static/coffee/src/discussion/views/discussion_thread_view.coffee
View file @
8c8367b0
...
...
@@ -22,6 +22,7 @@ if Backbone?
render
:
->
@
$el
.
html
(
@
renderTemplate
())
@
initLocal
()
@
delegateEvents
()
@
renderShowView
()
...
...
@@ -33,10 +34,7 @@ if Backbone?
@
responses
.
on
(
"add"
,
@
renderResponse
)
# Without a delay, jQuery doesn't add the loading extension defined in
# utils.coffee before safeAjax is invoked, which results in an error
setTimeout
(
=>
@
loadResponses
(
INITIAL_RESPONSE_PAGE_SIZE
,
@
$el
.
find
(
".responses"
),
true
),
100
)
setTimeout
((
=>
@
loadInitialResponses
()),
100
)
@
cleanup
:
->
...
...
@@ -71,6 +69,9 @@ if Backbone?
gettext
(
"We had some trouble loading more responses. Please try again."
)
)
loadInitialResponses
:
()
->
@
loadResponses
(
INITIAL_RESPONSE_PAGE_SIZE
,
@
$el
.
find
(
".responses"
),
true
)
renderResponseCountAndPagination
:
(
responseTotal
)
=>
@
$el
.
find
(
".response-count"
).
html
(
interpolate
(
...
...
@@ -226,6 +227,9 @@ if Backbone?
renderEditView
:
()
->
@
renderSubView
(
@
editView
)
getShowViewClass
:
()
->
return
DiscussionThreadShowView
createShowView
:
()
->
if
@
editView
?
...
...
@@ -233,7 +237,8 @@ if Backbone?
@
editView
.
$el
.
empty
()
@
editView
=
null
@
showView
=
new
DiscussionThreadShowView
(
model
:
@
model
)
showViewClass
=
@
getShowViewClass
()
@
showView
=
new
showViewClass
(
model
:
@
model
)
@
showView
.
bind
"thread:_delete"
,
@
_delete
@
showView
.
bind
"thread:edit"
,
@
edit
...
...
common/static/coffee/src/discussion/views/discussion_thread_view_inline.coffee
View file @
8c8367b0
...
...
@@ -16,7 +16,7 @@ if Backbone?
@
$local
=
@
$el
@
$delegateElement
=
@
$local
render
:
->
render
Template
:
()
->
if
@
model
.
has
(
'group_id'
)
@
template
=
DiscussionUtil
.
getTemplate
(
"_inline_thread_cohorted"
)
else
...
...
@@ -25,107 +25,43 @@ if Backbone?
if
not
@
model
.
has
(
'abbreviatedBody'
)
@
abbreviateBody
()
params
=
@
model
.
toJSON
()
@
$el
.
html
(
Mustache
.
render
(
@
template
,
params
))
#@createShowView()
Mustache
.
render
(
@
template
,
params
)
@
initLocal
()
@
delegateEvents
()
@
renderShowView
()
@
renderAttrs
()
@
$
(
"span.timeago"
).
timeago
()
render
:
()
->
super
()
@
$el
.
find
(
'.post-extended-content'
).
hide
()
if
@
expanded
@
makeWmdEditor
"reply-body"
@
renderAddResponseButton
()
@
renderResponses
()
@
createShowView
:
()
->
if
@
editView
?
@
editView
.
undelegateEvents
()
@
editView
.
$el
.
empty
()
@
editView
=
null
@
showView
=
new
DiscussionThreadInlineShowView
(
model
:
@
model
)
@
showView
.
bind
"thread:_delete"
,
@
_delete
@
showView
.
bind
"thread:edit"
,
@
edit
renderResponses
:
->
#TODO: threadview
DiscussionUtil
.
safeAjax
url
:
"/courses/
#{
$$course_id
}
/discussion/forum/
#{
@
model
.
get
(
'commentable_id'
)
}
/threads/
#{
@
model
.
id
}
"
$loading
:
@
$el
success
:
(
data
,
textStatus
,
xhr
)
=>
# @$el.find(".loading").remove()
Content
.
loadContentInfos
(
data
[
'annotated_content_info'
])
comments
=
new
Comments
(
data
[
'content'
][
'children'
])
comments
.
each
@
renderResponse
@
trigger
"thread:responses:rendered"
@
$
(
'.loading'
).
remove
()
@
$el
.
find
(
'.collapse-post'
).
hide
()
toggleClosed
:
(
event
)
->
#TODO: showview
$elem
=
$
(
event
.
target
)
url
=
@
model
.
urlFor
(
'close'
)
closed
=
@
model
.
get
(
'closed'
)
data
=
{
closed
:
not
closed
}
DiscussionUtil
.
safeAjax
$elem
:
$elem
url
:
url
data
:
data
type
:
"POST"
success
:
(
response
,
textStatus
)
=>
@
model
.
set
(
'closed'
,
not
closed
)
@
model
.
set
(
'ability'
,
response
.
ability
)
getShowViewClass
:
()
->
return
DiscussionThreadInlineShowView
toggleEndorse
:
(
event
)
->
#TODO: showview
$elem
=
$
(
event
.
target
)
url
=
@
model
.
urlFor
(
'endorse'
)
endorsed
=
@
model
.
get
(
'endorsed'
)
data
=
{
endorsed
:
not
endorsed
}
DiscussionUtil
.
safeAjax
$elem
:
$elem
url
:
url
data
:
data
type
:
"POST"
success
:
(
response
,
textStatus
)
=>
@
model
.
set
(
'endorsed'
,
not
endorsed
)
loadInitialResponses
:
()
->
if
@
expanded
super
()
abbreviateBody
:
->
abbreviated
=
DiscussionUtil
.
abbreviateString
@
model
.
get
(
'body'
),
140
@
model
.
set
(
'abbreviatedBody'
,
abbreviated
)
expandPost
:
(
event
)
=>
@
expanded
=
true
@
$el
.
addClass
(
'expanded'
)
@
$el
.
find
(
'.post-body'
).
html
(
@
model
.
get
(
'body'
))
@
showView
.
convertMath
()
@
$el
.
find
(
'.expand-post'
).
css
(
'display'
,
'none'
)
@
$el
.
find
(
'.collapse-post'
).
css
(
'display'
,
'block'
)
@
$el
.
find
(
'.post-extended-content'
).
show
()
@
makeWmdEditor
"reply-body"
@
renderAttrs
()
if
@
$el
.
find
(
'.loading'
).
length
@
renderAddResponseButton
()
@
renderResponses
()
if
not
@
expanded
@
expanded
=
true
@
loadInitialResponses
()
collapsePost
:
(
event
)
->
curScroll
=
$
(
window
).
scrollTop
()
postTop
=
@
$el
.
offset
().
top
if
postTop
<
curScroll
$
(
'html, body'
).
animate
({
scrollTop
:
postTop
})
@
expanded
=
false
@
$el
.
removeClass
(
'expanded'
)
@
$el
.
find
(
'.post-body'
).
html
(
@
model
.
get
(
'abbreviatedBody'
))
@
showView
.
convertMath
()
@
$el
.
find
(
'.expand-post'
).
css
(
'display'
,
'block'
)
@
$el
.
find
(
'.collapse-post'
).
css
(
'display'
,
'none'
)
@
$el
.
find
(
'.post-extended-content'
).
hide
()
@
$el
.
find
(
'.expand-post'
).
css
(
'display'
,
'block'
)
createEditView
:
()
->
super
()
@
editView
.
bind
"thread:update"
,
@
expandPost
@
editView
.
bind
"thread:update"
,
@
abbreviateBody
@
editView
.
bind
"thread:cancel_edit"
,
@
expandPost
common/static/js_test.yml
View file @
8c8367b0
...
...
@@ -31,6 +31,7 @@ lib_paths:
-
js/vendor/jquery.min.js
-
js/vendor/jasmine-jquery.js
-
js/vendor/jasmine-imagediff.js
-
js/vendor/mustache.js
-
js/vendor/underscore-min.js
-
js/vendor/backbone-min.js
-
js/vendor/jquery.timeago.js
...
...
common/test/acceptance/pages/lms/courseware.py
0 → 100644
View file @
8c8367b0
"""
Courseware page.
"""
from
.course_page
import
CoursePage
class
CoursewarePage
(
CoursePage
):
"""
Course info.
"""
url_path
=
"courseware"
def
is_browser_on_page
(
self
):
return
self
.
q
(
css
=
'body.courseware'
)
.
present
common/test/acceptance/pages/lms/discussion
_single_thread
.py
→
common/test/acceptance/pages/lms/discussion.py
View file @
8c8367b0
from
bok_choy.page_object
import
unguarded
from
bok_choy.page_object
import
PageObject
from
bok_choy.promise
import
EmptyPromise
from
.course_page
import
CoursePage
class
DiscussionSingleThreadPage
(
CoursePage
):
def
__init__
(
self
,
browser
,
course_id
,
thread_id
):
super
(
DiscussionSingleThreadPage
,
self
)
.
__init__
(
browser
,
course_id
)
self
.
thread_id
=
thread_id
class
DiscussionThreadPage
(
PageObject
):
url
=
None
def
is_browser_on_page
(
self
):
return
self
.
q
(
css
=
"body.discussion .discussion-article[data-id='{thread_id}']"
.
format
(
thread_id
=
self
.
thread_id
)
)
.
present
def
__init__
(
self
,
browser
,
thread_selector
):
super
(
DiscussionThreadPage
,
self
)
.
__init__
(
browser
)
self
.
thread_selector
=
thread_selector
def
_find_within
(
self
,
selector
):
"""
Returns a query corresponding to the given CSS selector within the scope
of this thread page
"""
return
self
.
q
(
css
=
self
.
thread_selector
+
" "
+
selector
)
@property
@unguarded
def
url_path
(
self
):
return
"discussion/forum/dummy/threads/"
+
self
.
thread_id
def
is_browser_on_page
(
self
):
return
self
.
q
(
css
=
self
.
thread_selector
)
.
present
def
_get_element_text
(
self
,
selector
):
"""
Returns the text of the first element matching the given selector, or
None if no such element exists
"""
text_list
=
self
.
q
(
css
=
selector
)
.
text
text_list
=
self
.
_find_within
(
selector
)
.
text
return
text_list
[
0
]
if
text_list
else
None
def
_is_element_visible
(
self
,
selector
):
query
=
self
.
_find_within
(
selector
)
return
query
.
present
and
query
.
visible
def
get_response_total_text
(
self
):
"""Returns the response count text, or None if not present"""
return
self
.
_get_element_text
(
".response-count"
)
def
get_num_displayed_responses
(
self
):
"""Returns the number of responses actually rendered"""
return
len
(
self
.
q
(
css
=
".discussion-response"
)
.
results
)
return
len
(
self
.
_find_within
(
".discussion-response"
)
)
def
get_shown_responses_text
(
self
):
"""Returns the shown response count text, or None if not present"""
...
...
@@ -45,7 +51,7 @@ class DiscussionSingleThreadPage(CoursePage):
def
load_more_responses
(
self
):
"""Clicks the load more responses button and waits for responses to load"""
self
.
q
(
css
=
".load-response-button"
)
.
first
.
click
()
self
.
_find_within
(
".load-response-button"
)
.
click
()
def
_is_ajax_finished
():
return
self
.
browser
.
execute_script
(
"return jQuery.active"
)
==
0
...
...
@@ -64,25 +70,19 @@ class DiscussionSingleThreadPage(CoursePage):
Clicks the add response button and ensures that the response text
field receives focus
"""
self
.
q
(
css
=
".add-response-btn"
)
.
first
.
click
()
self
.
_find_within
(
".add-response-btn"
)
.
first
.
click
()
EmptyPromise
(
lambda
:
self
.
q
(
css
=
"#wmd-input-reply-body-{thread_id}:focus"
.
format
(
thread_id
=
self
.
thread_id
))
,
lambda
:
self
.
_find_within
(
".discussion-reply-new textarea:focus"
)
.
present
,
"Response field received focus"
)
.
fulfill
()
def
_is_element_visible
(
self
,
selector
):
return
(
self
.
q
(
css
=
selector
)
.
present
and
self
.
q
(
css
=
selector
)
.
visible
)
def
is_response_editor_visible
(
self
,
response_id
):
"""Returns true if the response editor is present, false otherwise"""
return
self
.
_is_element_visible
(
".response_{} .edit-post-body"
.
format
(
response_id
))
def
start_response_edit
(
self
,
response_id
):
"""Click the edit button for the response, loading the editing view"""
self
.
q
(
css
=
".response_{} .discussion-response .action-edit"
.
format
(
response_id
))
.
first
.
click
()
self
.
_find_within
(
".response_{} .discussion-response .action-edit"
.
format
(
response_id
))
.
first
.
click
()
EmptyPromise
(
lambda
:
self
.
is_response_editor_visible
(
response_id
),
"Response edit started"
...
...
@@ -105,7 +105,7 @@ class DiscussionSingleThreadPage(CoursePage):
def
delete_comment
(
self
,
comment_id
):
with
self
.
handle_alert
():
self
.
q
(
css
=
"#comment_{} div.action-delete"
.
format
(
comment_id
))
.
first
.
click
()
self
.
_find_within
(
"#comment_{} div.action-delete"
.
format
(
comment_id
))
.
first
.
click
()
EmptyPromise
(
lambda
:
not
self
.
is_comment_visible
(
comment_id
),
"Deleted comment was removed"
...
...
@@ -120,12 +120,12 @@ class DiscussionSingleThreadPage(CoursePage):
return
self
.
_is_element_visible
(
".edit-comment-body[data-id='{}']"
.
format
(
comment_id
))
def
_get_comment_editor_value
(
self
,
comment_id
):
return
self
.
q
(
css
=
"#wmd-input-edit-comment-body-{}"
.
format
(
comment_id
))
.
text
[
0
]
return
self
.
_find_within
(
"#wmd-input-edit-comment-body-{}"
.
format
(
comment_id
))
.
text
[
0
]
def
start_comment_edit
(
self
,
comment_id
):
"""Click the edit button for the comment, loading the editing view"""
old_body
=
self
.
get_comment_body
(
comment_id
)
self
.
q
(
css
=
"#comment_{} .action-edit"
.
format
(
comment_id
))
.
first
.
click
()
self
.
_find_within
(
"#comment_{} .action-edit"
.
format
(
comment_id
))
.
first
.
click
()
EmptyPromise
(
lambda
:
(
self
.
is_comment_editor_visible
(
comment_id
)
and
...
...
@@ -137,11 +137,11 @@ class DiscussionSingleThreadPage(CoursePage):
def
set_comment_editor_value
(
self
,
comment_id
,
new_body
):
"""Replace the contents of the comment editor"""
self
.
q
(
css
=
"#comment_{} .wmd-input"
.
format
(
comment_id
))
.
fill
(
new_body
)
self
.
_find_within
(
"#comment_{} .wmd-input"
.
format
(
comment_id
))
.
fill
(
new_body
)
def
submit_comment_edit
(
self
,
comment_id
,
new_comment_body
):
"""Click the submit button on the comment editor"""
self
.
q
(
css
=
"#comment_{} .post-update"
.
format
(
comment_id
))
.
first
.
click
()
self
.
_find_within
(
"#comment_{} .post-update"
.
format
(
comment_id
))
.
first
.
click
()
EmptyPromise
(
lambda
:
(
not
self
.
is_comment_editor_visible
(
comment_id
)
and
...
...
@@ -153,7 +153,7 @@ class DiscussionSingleThreadPage(CoursePage):
def
cancel_comment_edit
(
self
,
comment_id
,
original_body
):
"""Click the cancel button on the comment editor"""
self
.
q
(
css
=
"#comment_{} .post-cancel"
.
format
(
comment_id
))
.
first
.
click
()
self
.
_find_within
(
"#comment_{} .post-cancel"
.
format
(
comment_id
))
.
first
.
click
()
EmptyPromise
(
lambda
:
(
not
self
.
is_comment_editor_visible
(
comment_id
)
and
...
...
@@ -162,3 +162,72 @@ class DiscussionSingleThreadPage(CoursePage):
),
"Comment edit was canceled"
)
.
fulfill
()
class
DiscussionTabSingleThreadPage
(
CoursePage
):
def
__init__
(
self
,
browser
,
course_id
,
thread_id
):
super
(
DiscussionTabSingleThreadPage
,
self
)
.
__init__
(
browser
,
course_id
)
self
.
thread_page
=
DiscussionThreadPage
(
browser
,
"body.discussion .discussion-article[data-id='{thread_id}']"
.
format
(
thread_id
=
thread_id
)
)
self
.
url_path
=
"discussion/forum/dummy/threads/"
+
thread_id
def
is_browser_on_page
(
self
):
return
self
.
thread_page
.
is_browser_on_page
()
def
__getattr__
(
self
,
name
):
return
getattr
(
self
.
thread_page
,
name
)
class
InlineDiscussionPage
(
PageObject
):
url
=
None
def
__init__
(
self
,
browser
,
discussion_id
):
super
(
InlineDiscussionPage
,
self
)
.
__init__
(
browser
)
self
.
_discussion_selector
=
(
"body.courseware .discussion-module[data-discussion-id='{discussion_id}'] "
.
format
(
discussion_id
=
discussion_id
)
)
def
_find_within
(
self
,
selector
):
"""
Returns a query corresponding to the given CSS selector within the scope
of this discussion page
"""
return
self
.
q
(
css
=
self
.
_discussion_selector
+
" "
+
selector
)
def
is_browser_on_page
(
self
):
return
self
.
q
(
css
=
self
.
_discussion_selector
)
.
present
def
is_discussion_expanded
(
self
):
return
self
.
_find_within
(
".discussion"
)
.
present
def
expand_discussion
(
self
):
"""Click the link to expand the discussion"""
self
.
_find_within
(
".discussion-show"
)
.
first
.
click
()
EmptyPromise
(
self
.
is_discussion_expanded
,
"Discussion expanded"
)
.
fulfill
()
def
get_num_displayed_threads
(
self
):
return
len
(
self
.
_find_within
(
".discussion-thread"
))
class
InlineDiscussionThreadPage
(
DiscussionThreadPage
):
def
__init__
(
self
,
browser
,
thread_id
):
super
(
InlineDiscussionThreadPage
,
self
)
.
__init__
(
browser
,
"body.courseware .discussion-module #thread_{thread_id}"
.
format
(
thread_id
=
thread_id
)
)
def
expand
(
self
):
"""Clicks the link to expand the thread"""
self
.
_find_within
(
".expand-post"
)
.
first
.
click
()
EmptyPromise
(
lambda
:
bool
(
self
.
get_response_total_text
()),
"Thread expanded"
)
.
fulfill
()
common/test/acceptance/tests/test_discussion.py
View file @
8c8367b0
...
...
@@ -2,96 +2,132 @@
Tests for discussion pages
"""
from
uuid
import
uuid4
from
.helpers
import
UniqueCourseTest
from
..pages.studio.auto_auth
import
AutoAuthPage
from
..pages.lms.discussion_single_thread
import
DiscussionSingleThreadPage
from
..fixtures.course
import
CourseFixture
from
..pages.lms.courseware
import
CoursewarePage
from
..pages.lms.discussion
import
(
DiscussionTabSingleThreadPage
,
InlineDiscussionPage
,
InlineDiscussionThreadPage
)
from
..fixtures.course
import
CourseFixture
,
XBlockFixtureDesc
from
..fixtures.discussion
import
SingleThreadViewFixture
,
Thread
,
Response
,
Comment
class
Discussion
SingleThreadTest
(
UniqueCourseTes
t
):
class
Discussion
ResponsePaginationTestMixin
(
objec
t
):
"""
Tests for the discussion page displaying a single thread
A mixin containing tests for response pagination for use by both inline
discussion and the discussion tab
"""
def
setUp
(
self
):
super
(
DiscussionSingleThreadTest
,
self
)
.
setUp
()
def
setup_thread
(
self
,
num_responses
,
**
thread_kwargs
):
"""
Create a test thread with the given number of responses, passing all
keyword arguments through to the Thread fixture, then invoke
setup_thread_page.
"""
thread_id
=
"test_thread_{}"
.
format
(
uuid4
()
.
hex
)
thread_fixture
=
SingleThreadViewFixture
(
Thread
(
id
=
thread_id
,
commentable_id
=
self
.
discussion_id
,
**
thread_kwargs
)
)
for
i
in
range
(
num_responses
):
thread_fixture
.
addResponse
(
Response
(
id
=
str
(
i
),
body
=
str
(
i
)))
thread_fixture
.
push
()
self
.
setup_thread_page
(
thread_id
)
# Create a course to register for
CourseFixture
(
**
self
.
course_info
)
.
install
()
def
assert_response_display_correct
(
self
,
response_total
,
displayed_responses
):
"""
Assert that various aspects of the display of responses are all correct:
* Text indicating total number of responses
* Presence of "Add a response" button
* Number of responses actually displayed
* Presence and text of indicator of how many responses are shown
* Presence and text of button to load more responses
"""
self
.
assertEqual
(
self
.
thread_page
.
get_response_total_text
(),
str
(
response_total
)
+
" responses"
)
self
.
assertEqual
(
self
.
thread_page
.
has_add_response_button
(),
response_total
!=
0
)
self
.
assertEqual
(
self
.
thread_page
.
get_num_displayed_responses
(),
displayed_responses
)
self
.
assertEqual
(
self
.
thread_page
.
get_shown_responses_text
(),
(
None
if
response_total
==
0
else
"Showing all responses"
if
response_total
==
displayed_responses
else
"Showing first {} responses"
.
format
(
displayed_responses
)
)
)
self
.
assertEqual
(
self
.
thread_page
.
get_load_responses_button_text
(),
(
None
if
response_total
==
displayed_responses
else
"Load all responses"
if
response_total
-
displayed_responses
<
100
else
"Load next 100 responses"
)
)
def
test_pagination_no_responses
(
self
):
self
.
setup_thread
(
0
)
self
.
assert_response_display_correct
(
0
,
0
)
def
test_pagination_few_responses
(
self
):
self
.
setup_thread
(
5
)
self
.
assert_response_display_correct
(
5
,
5
)
def
test_pagination_two_response_pages
(
self
):
self
.
setup_thread
(
50
)
self
.
assert_response_display_correct
(
50
,
25
)
self
.
thread_page
.
load_more_responses
()
self
.
assert_response_display_correct
(
50
,
50
)
def
test_pagination_exactly_two_response_pages
(
self
):
self
.
setup_thread
(
125
)
self
.
assert_response_display_correct
(
125
,
25
)
self
.
thread_page
.
load_more_responses
()
self
.
assert_response_display_correct
(
125
,
125
)
def
test_pagination_three_response_pages
(
self
):
self
.
setup_thread
(
150
)
self
.
assert_response_display_correct
(
150
,
25
)
self
.
thread_page
.
load_more_responses
()
self
.
assert_response_display_correct
(
150
,
125
)
self
.
thread_page
.
load_more_responses
()
self
.
assert_response_display_correct
(
150
,
150
)
self
.
user_id
=
AutoAuthPage
(
self
.
browser
,
course_id
=
self
.
course_id
)
.
visit
()
.
get_user_id
()
def
test_add_response_button
(
self
):
self
.
setup_thread
(
5
)
self
.
assertTrue
(
self
.
thread_page
.
has_add_response_button
())
self
.
thread_page
.
click_add_response_button
()
def
setup_thread
(
self
,
thread
,
num_responses
):
view
=
SingleThreadViewFixture
(
thread
=
thread
)
for
i
in
range
(
num_responses
):
view
.
addResponse
(
Response
(
id
=
str
(
i
),
body
=
str
(
i
)))
view
.
push
()
def
test_add_response_button_closed_thread
(
self
):
self
.
setup_thread
(
5
,
closed
=
True
)
self
.
assertFalse
(
self
.
thread_page
.
has_add_response_button
())
def
test_no_responses
(
self
):
self
.
setup_thread
(
Thread
(
id
=
"0_responses"
),
0
)
page
=
DiscussionSingleThreadPage
(
self
.
browser
,
self
.
course_id
,
"0_responses"
)
page
.
visit
()
self
.
assertEqual
(
page
.
get_response_total_text
(),
"0 responses"
)
self
.
assertFalse
(
page
.
has_add_response_button
())
self
.
assertEqual
(
page
.
get_num_displayed_responses
(),
0
)
self
.
assertEqual
(
page
.
get_shown_responses_text
(),
None
)
self
.
assertIsNone
(
page
.
get_load_responses_button_text
())
def
test_few_responses
(
self
):
self
.
setup_thread
(
Thread
(
id
=
"5_responses"
),
5
)
page
=
DiscussionSingleThreadPage
(
self
.
browser
,
self
.
course_id
,
"5_responses"
)
page
.
visit
()
self
.
assertEqual
(
page
.
get_response_total_text
(),
"5 responses"
)
self
.
assertEqual
(
page
.
get_num_displayed_responses
(),
5
)
self
.
assertEqual
(
page
.
get_shown_responses_text
(),
"Showing all responses"
)
self
.
assertIsNone
(
page
.
get_load_responses_button_text
())
def
test_two_response_pages
(
self
):
self
.
setup_thread
(
Thread
(
id
=
"50_responses"
),
50
)
page
=
DiscussionSingleThreadPage
(
self
.
browser
,
self
.
course_id
,
"50_responses"
)
page
.
visit
()
self
.
assertEqual
(
page
.
get_response_total_text
(),
"50 responses"
)
self
.
assertEqual
(
page
.
get_num_displayed_responses
(),
25
)
self
.
assertEqual
(
page
.
get_shown_responses_text
(),
"Showing first 25 responses"
)
self
.
assertEqual
(
page
.
get_load_responses_button_text
(),
"Load all responses"
)
page
.
load_more_responses
()
self
.
assertEqual
(
page
.
get_num_displayed_responses
(),
50
)
self
.
assertEqual
(
page
.
get_shown_responses_text
(),
"Showing all responses"
)
self
.
assertEqual
(
page
.
get_load_responses_button_text
(),
None
)
def
test_three_response_pages
(
self
):
self
.
setup_thread
(
Thread
(
id
=
"150_responses"
),
150
)
page
=
DiscussionSingleThreadPage
(
self
.
browser
,
self
.
course_id
,
"150_responses"
)
page
.
visit
()
self
.
assertEqual
(
page
.
get_response_total_text
(),
"150 responses"
)
self
.
assertEqual
(
page
.
get_num_displayed_responses
(),
25
)
self
.
assertEqual
(
page
.
get_shown_responses_text
(),
"Showing first 25 responses"
)
self
.
assertEqual
(
page
.
get_load_responses_button_text
(),
"Load next 100 responses"
)
page
.
load_more_responses
()
self
.
assertEqual
(
page
.
get_num_displayed_responses
(),
125
)
self
.
assertEqual
(
page
.
get_shown_responses_text
(),
"Showing first 125 responses"
)
self
.
assertEqual
(
page
.
get_load_responses_button_text
(),
"Load all responses"
)
class
DiscussionTabSingleThreadTest
(
UniqueCourseTest
,
DiscussionResponsePaginationTestMixin
):
"""
Tests for the discussion page displaying a single thread
"""
page
.
load_more_responses
()
self
.
assertEqual
(
page
.
get_num_displayed_responses
(),
150
)
self
.
assertEqual
(
page
.
get_shown_responses_text
(),
"Showing all responses"
)
self
.
assertEqual
(
page
.
get_load_responses_button_text
(),
None
)
def
setUp
(
self
):
super
(
DiscussionTabSingleThreadTest
,
self
)
.
setUp
()
self
.
discussion_id
=
"test_discussion_{}"
.
format
(
uuid4
()
.
hex
)
def
test_add_response_button
(
self
):
self
.
setup_thread
(
Thread
(
id
=
"5_responses"
),
5
)
page
=
DiscussionSingleThreadPage
(
self
.
browser
,
self
.
course_id
,
"5_responses"
)
page
.
visit
()
self
.
assertTrue
(
page
.
has_add_response_button
())
page
.
click_add_response_button
()
# Create a course to register for
CourseFixture
(
**
self
.
course_info
)
.
install
()
def
test_add_response_button_closed_thread
(
self
):
self
.
setup_thread
(
Thread
(
id
=
"5_responses_closed"
,
closed
=
True
),
5
)
page
=
DiscussionSingleThreadPage
(
self
.
browser
,
self
.
course_id
,
"5_responses_closed"
)
page
.
visit
()
self
.
assertFalse
(
page
.
has_add_response_button
()
)
AutoAuthPage
(
self
.
browser
,
course_id
=
self
.
course_id
)
.
visit
()
def
setup_thread_page
(
self
,
thread_id
):
self
.
thread_page
=
DiscussionTabSingleThreadPage
(
self
.
browser
,
self
.
course_id
,
thread_id
)
# pylint:disable=W0201
self
.
thread_page
.
visit
(
)
class
DiscussionCommentDeletionTest
(
UniqueCourseTest
):
...
...
@@ -119,7 +155,7 @@ class DiscussionCommentDeletionTest(UniqueCourseTest):
def
test_comment_deletion_as_student
(
self
):
self
.
setup_user
()
self
.
setup_view
()
page
=
DiscussionSingleThreadPage
(
self
.
browser
,
self
.
course_id
,
"comment_deletion_test_thread"
)
page
=
Discussion
Tab
SingleThreadPage
(
self
.
browser
,
self
.
course_id
,
"comment_deletion_test_thread"
)
page
.
visit
()
self
.
assertTrue
(
page
.
is_comment_deletable
(
"comment_self_author"
))
self
.
assertTrue
(
page
.
is_comment_visible
(
"comment_other_author"
))
...
...
@@ -129,7 +165,7 @@ class DiscussionCommentDeletionTest(UniqueCourseTest):
def
test_comment_deletion_as_moderator
(
self
):
self
.
setup_user
(
roles
=
[
'Moderator'
])
self
.
setup_view
()
page
=
DiscussionSingleThreadPage
(
self
.
browser
,
self
.
course_id
,
"comment_deletion_test_thread"
)
page
=
Discussion
Tab
SingleThreadPage
(
self
.
browser
,
self
.
course_id
,
"comment_deletion_test_thread"
)
page
.
visit
()
self
.
assertTrue
(
page
.
is_comment_deletable
(
"comment_self_author"
))
self
.
assertTrue
(
page
.
is_comment_deletable
(
"comment_other_author"
))
...
...
@@ -168,7 +204,7 @@ class DiscussionCommentEditTest(UniqueCourseTest):
def
test_edit_comment_as_student
(
self
):
self
.
setup_user
()
self
.
setup_view
()
page
=
DiscussionSingleThreadPage
(
self
.
browser
,
self
.
course_id
,
"comment_edit_test_thread"
)
page
=
Discussion
Tab
SingleThreadPage
(
self
.
browser
,
self
.
course_id
,
"comment_edit_test_thread"
)
page
.
visit
()
self
.
assertTrue
(
page
.
is_comment_editable
(
"comment_self_author"
))
self
.
assertTrue
(
page
.
is_comment_visible
(
"comment_other_author"
))
...
...
@@ -178,7 +214,7 @@ class DiscussionCommentEditTest(UniqueCourseTest):
def
test_edit_comment_as_moderator
(
self
):
self
.
setup_user
(
roles
=
[
"Moderator"
])
self
.
setup_view
()
page
=
DiscussionSingleThreadPage
(
self
.
browser
,
self
.
course_id
,
"comment_edit_test_thread"
)
page
=
Discussion
Tab
SingleThreadPage
(
self
.
browser
,
self
.
course_id
,
"comment_edit_test_thread"
)
page
.
visit
()
self
.
assertTrue
(
page
.
is_comment_editable
(
"comment_self_author"
))
self
.
assertTrue
(
page
.
is_comment_editable
(
"comment_other_author"
))
...
...
@@ -188,7 +224,7 @@ class DiscussionCommentEditTest(UniqueCourseTest):
def
test_cancel_comment_edit
(
self
):
self
.
setup_user
()
self
.
setup_view
()
page
=
DiscussionSingleThreadPage
(
self
.
browser
,
self
.
course_id
,
"comment_edit_test_thread"
)
page
=
Discussion
Tab
SingleThreadPage
(
self
.
browser
,
self
.
course_id
,
"comment_edit_test_thread"
)
page
.
visit
()
self
.
assertTrue
(
page
.
is_comment_editable
(
"comment_self_author"
))
original_body
=
page
.
get_comment_body
(
"comment_self_author"
)
...
...
@@ -200,7 +236,7 @@ class DiscussionCommentEditTest(UniqueCourseTest):
"""Only one editor should be visible at a time within a single response"""
self
.
setup_user
(
roles
=
[
"Moderator"
])
self
.
setup_view
()
page
=
DiscussionSingleThreadPage
(
self
.
browser
,
self
.
course_id
,
"comment_edit_test_thread"
)
page
=
Discussion
Tab
SingleThreadPage
(
self
.
browser
,
self
.
course_id
,
"comment_edit_test_thread"
)
page
.
visit
()
self
.
assertTrue
(
page
.
is_comment_editable
(
"comment_self_author"
))
self
.
assertTrue
(
page
.
is_comment_editable
(
"comment_other_author"
))
...
...
@@ -224,3 +260,44 @@ class DiscussionCommentEditTest(UniqueCourseTest):
page
.
cancel_comment_edit
(
"comment_self_author"
,
original_body
)
self
.
assertFalse
(
page
.
is_comment_editor_visible
(
"comment_self_author"
))
self
.
assertTrue
(
page
.
is_add_comment_visible
(
"response1"
))
class
InlineDiscussionTest
(
UniqueCourseTest
,
DiscussionResponsePaginationTestMixin
):
"""
Tests for inline discussions
"""
def
setUp
(
self
):
super
(
InlineDiscussionTest
,
self
)
.
setUp
()
self
.
discussion_id
=
"test_discussion_{}"
.
format
(
uuid4
()
.
hex
)
CourseFixture
(
**
self
.
course_info
)
.
add_children
(
XBlockFixtureDesc
(
"chapter"
,
"Test Section"
)
.
add_children
(
XBlockFixtureDesc
(
"sequential"
,
"Test Subsection"
)
.
add_children
(
XBlockFixtureDesc
(
"vertical"
,
"Test Unit"
)
.
add_children
(
XBlockFixtureDesc
(
"discussion"
,
"Test Discussion"
,
metadata
=
{
"discussion_id"
:
self
.
discussion_id
}
)
)
)
)
)
.
install
()
AutoAuthPage
(
self
.
browser
,
course_id
=
self
.
course_id
)
.
visit
()
CoursewarePage
(
self
.
browser
,
self
.
course_id
)
.
visit
()
self
.
discussion_page
=
InlineDiscussionPage
(
self
.
browser
,
self
.
discussion_id
)
def
setup_thread_page
(
self
,
thread_id
):
self
.
discussion_page
.
expand_discussion
()
self
.
assertEqual
(
self
.
discussion_page
.
get_num_displayed_threads
(),
1
)
self
.
thread_page
=
InlineDiscussionThreadPage
(
self
.
browser
,
thread_id
)
# pylint:disable=W0201
self
.
thread_page
.
expand
()
def
test_initial_render
(
self
):
self
.
assertFalse
(
self
.
discussion_page
.
is_discussion_expanded
())
def
test_expand_discussion_empty
(
self
):
self
.
discussion_page
.
expand_discussion
()
self
.
assertEqual
(
self
.
discussion_page
.
get_num_displayed_threads
(),
0
)
lms/static/sass/_discussion.scss
View file @
8c8367b0
...
...
@@ -2069,10 +2069,6 @@ body.discussion {
font-size
:
12px
;
line-height
:
30px
;
&
.collapse-post
{
display
:
none
;
}
.icon
{
color
:
$link-color
;
margin-right
:
(
$baseline
*
0
.25
);
...
...
lms/templates/discussion/mustache/_inline_thread.mustache
View file @
8c8367b0
...
...
@@ -3,16 +3,17 @@
<article
class=
"discussion-article"
data-id=
"
{{
id
}}
"
>
<div
class=
"thread-wrapper"
>
<div
class=
"thread-content-wrapper"
></div>
<div
class=
"add-response post-extended-content"
>
<div
class=
"post-extended-content"
>
<div
class=
"response-count"
/>
<div
class=
"add-response"
>
<button
class=
"button add-response-btn"
>
<i
class=
"icon icon-reply"
></i>
<span
class=
"add-response-btn-text"
>
${_('Add A Response')}
</span>
</button>
</div>
<ol
class=
"responses post-extended-content"
>
<li
class=
"loading"
><div
class=
"loading-animation"
><span
class=
"sr"
>
${_("Loading content")}
</span></div></li>
</ol>
<form
class=
"local discussion-reply-new post-extended-content"
data-id=
"
{{
id
}}
"
>
<ol
class=
"responses"
/>
<div
class=
"response-pagination"
/>
<form
class=
"local discussion-reply-new"
data-id=
"
{{
id
}}
"
>
<h4>
${_("Post a response:")}
</h4>
<ul
class=
"discussion-errors"
></ul>
<div
class=
"reply-body"
data-id=
"
{{
id
}}
"
></div>
...
...
@@ -21,6 +22,7 @@
</div>
</form>
</div>
</div>
<div
class=
"local post-tools"
>
<a
href=
"javascript:void(0)"
class=
"expand-post"
><span
class=
"icon icon-plus"
/>
${_("Expand discussion")}
</a>
...
...
lms/templates/discussion/mustache/_inline_thread_cohorted.mustache
View file @
8c8367b0
...
...
@@ -4,16 +4,17 @@
<div
class=
"thread-wrapper"
>
<div
class=
"group-visibility-label"
>
{{
group_string
}}
</div>
<div
class=
"thread-content-wrapper"
></div>
<div
class=
"add-response post-extended-content"
>
<div
class=
"post-extended-content"
>
<div
class=
"response-count"
/>
<div
class=
"add-response"
>
<button
class=
"button add-response-btn"
>
<i
class=
"icon icon-reply"
></i>
<span
class=
"add-response-btn-text"
>
${_('Add A Response')}
</span>
</button>
</div>
<ol
class=
"responses post-extended-content"
>
<li
class=
"loading"
><div
class=
"loading-animation"
><span
class=
"sr"
>
${_("Loading content")}
</span></div></li>
</ol>
<form
class=
"local discussion-reply-new post-extended-content"
data-id=
"
{{
id
}}
"
>
<ol
class=
"responses"
/>
<div
class=
"response-pagination"
/>
<form
class=
"local discussion-reply-new"
data-id=
"
{{
id
}}
"
>
<h4>
${_("Post a response:")}
</h4>
<ul
class=
"discussion-errors"
></ul>
<div
class=
"reply-body"
data-id=
"
{{
id
}}
"
></div>
...
...
@@ -22,6 +23,7 @@
</div>
</form>
</div>
</div>
<div
class=
"local post-tools"
>
<a
href=
"javascript:void(0)"
class=
"expand-post"
><span
class=
"icon icon-plus"
/>
${_("Expand discussion")}
</a>
...
...
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