Add pagination to forum user profile page
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
describe "DiscussionUserProfileView", ->
|
||||
beforeEach ->
|
||||
setFixtures(
|
||||
"""
|
||||
<script type="text/template" id="_user_profile">
|
||||
<section class="discussion">
|
||||
{{#threads}}
|
||||
<article class="discussion-thread" id="thread_{{id}}"/>
|
||||
{{/threads}}
|
||||
</section>
|
||||
<section class="pagination"/>
|
||||
</script>
|
||||
<script type="text/template" id="_profile_thread">
|
||||
<div class="profile-thread" id="thread_{{id}}"/>
|
||||
</script>
|
||||
<script type="text/template" id="_pagination">
|
||||
<div class="discussion-paginator">
|
||||
<a href="#different-page"/>
|
||||
</div>
|
||||
<div
|
||||
class="pagination-params"
|
||||
data-leftdots="{{leftdots}}"
|
||||
data-page="{{page}}"
|
||||
data-rightdots="{{rightdots}}"
|
||||
>
|
||||
{{#previous}}
|
||||
<div class="previous" data-url="{{url}}" data-number="{{number}}"/>
|
||||
{{/previous}}
|
||||
{{#first}}
|
||||
<div class="first" data-url="{{url}}" data-number="{{number}}"/>
|
||||
{{/first}}
|
||||
{{#lowPages}}
|
||||
<div class="lowPages" data-url="{{url}}" data-number="{{number}}"/>
|
||||
{{/lowPages}}
|
||||
{{#highPages}}
|
||||
<div class="highPages" data-url="{{url}}" data-number="{{number}}"/>
|
||||
{{/highPages}}
|
||||
{{#last}}
|
||||
<div class="last" data-url="{{url}}" data-number="{{number}}"/>
|
||||
{{/last}}
|
||||
{{#next}}
|
||||
<div class="next" data-url="{{url}}" data-number="{{number}}"/>
|
||||
{{/next}}
|
||||
</div>
|
||||
</script>
|
||||
<div class="user-profile-fixture"/>
|
||||
"""
|
||||
)
|
||||
window.$$course_id = "dummy_course_id"
|
||||
spyOn(DiscussionThreadProfileView.prototype, "render")
|
||||
|
||||
makeView = (threads, page, numPages) ->
|
||||
return new DiscussionUserProfileView(
|
||||
el: $(".user-profile-fixture")
|
||||
collection: threads
|
||||
page: page
|
||||
numPages: numPages
|
||||
)
|
||||
|
||||
describe "thread rendering should be correct", ->
|
||||
checkRender = (numThreads) ->
|
||||
threads = _.map(_.range(numThreads), (i) -> {id: i.toString(), body: "dummy body"})
|
||||
view = makeView(threads, 1, 1)
|
||||
expect(view.$(".discussion").children().length).toEqual(numThreads)
|
||||
_.each(threads, (thread) -> expect(view.$("#thread_#{thread.id}").length).toEqual(1))
|
||||
|
||||
it "with no threads", ->
|
||||
checkRender(0)
|
||||
|
||||
it "with one thread", ->
|
||||
checkRender(1)
|
||||
|
||||
it "with several threads", ->
|
||||
checkRender(5)
|
||||
|
||||
describe "pagination rendering should be correct", ->
|
||||
baseUri = URI(window.location)
|
||||
|
||||
pageInfo = (page) -> {url: baseUri.clone().addSearch("page", page).toString(), number: page}
|
||||
|
||||
checkRender = (params) ->
|
||||
view = makeView([], params.page, params.numPages)
|
||||
paramsQuery = view.$(".pagination-params")
|
||||
expect(paramsQuery.length).toEqual(1)
|
||||
_.each(
|
||||
["page", "leftdots", "rightdots"],
|
||||
(param) ->
|
||||
expect(paramsQuery.data(param)).toEqual(params[param])
|
||||
)
|
||||
_.each(
|
||||
["previous", "first", "last", "next"],
|
||||
(param) ->
|
||||
expected = params[param]
|
||||
expect(paramsQuery.find("." + param).data()).toEqual(
|
||||
if expected then pageInfo(expected) else null
|
||||
)
|
||||
)
|
||||
_.each(
|
||||
["lowPages", "highPages"]
|
||||
(param) ->
|
||||
expect(paramsQuery.find("." + param).map(-> $(this).data()).get()).toEqual(
|
||||
_.map(params[param], pageInfo)
|
||||
)
|
||||
)
|
||||
|
||||
it "for one page", ->
|
||||
checkRender(
|
||||
page: 1
|
||||
numPages: 1
|
||||
previous: null
|
||||
first: null
|
||||
leftdots: false
|
||||
lowPages: []
|
||||
highPages: []
|
||||
rightdots: false
|
||||
last: null
|
||||
next: null
|
||||
)
|
||||
|
||||
it "for first page of three (max with no last)", ->
|
||||
checkRender(
|
||||
page: 1
|
||||
numPages: 3
|
||||
previous: null
|
||||
first: null
|
||||
leftdots: false
|
||||
lowPages: []
|
||||
highPages: [2, 3]
|
||||
rightdots: false
|
||||
last: null
|
||||
next: 2
|
||||
)
|
||||
|
||||
it "for first page of four (has last but no dots)", ->
|
||||
checkRender(
|
||||
page: 1
|
||||
numPages: 4
|
||||
previous: null
|
||||
first: null
|
||||
leftdots: false
|
||||
lowPages: []
|
||||
highPages: [2, 3]
|
||||
rightdots: false
|
||||
last: 4
|
||||
next: 2
|
||||
)
|
||||
|
||||
it "for first page of five (has dots)", ->
|
||||
checkRender(
|
||||
page: 1
|
||||
numPages: 5
|
||||
previous: null
|
||||
first: null
|
||||
leftdots: false
|
||||
lowPages: []
|
||||
highPages: [2, 3]
|
||||
rightdots: true
|
||||
last: 5
|
||||
next: 2
|
||||
)
|
||||
|
||||
it "for last page of three (max with no first)", ->
|
||||
checkRender(
|
||||
page: 3
|
||||
numPages: 3
|
||||
previous: 2
|
||||
first: null
|
||||
leftdots: false
|
||||
lowPages: [1, 2]
|
||||
highPages: []
|
||||
rightdots: false
|
||||
last: null
|
||||
next: null
|
||||
)
|
||||
|
||||
it "for last page of four (has first but no dots)", ->
|
||||
checkRender(
|
||||
page: 4
|
||||
numPages: 4
|
||||
previous: 3
|
||||
first: 1
|
||||
leftdots: false
|
||||
lowPages: [2, 3]
|
||||
highPages: []
|
||||
rightdots: false
|
||||
last: null
|
||||
next: null
|
||||
)
|
||||
|
||||
it "for last page of five (has dots)", ->
|
||||
checkRender(
|
||||
page: 5
|
||||
numPages: 5
|
||||
previous: 4
|
||||
first: 1
|
||||
leftdots: true
|
||||
lowPages: [3, 4]
|
||||
highPages: []
|
||||
rightdots: false
|
||||
last: null
|
||||
next: null
|
||||
)
|
||||
|
||||
it "for middle page of five (max with no first/last)", ->
|
||||
checkRender(
|
||||
page: 3
|
||||
numPages: 5
|
||||
previous: 2
|
||||
first: null
|
||||
leftdots: false
|
||||
lowPages: [1, 2]
|
||||
highPages: [4, 5]
|
||||
rightdots: false
|
||||
last: null
|
||||
next: 4
|
||||
)
|
||||
|
||||
it "for middle page of seven (has first/last but no dots)", ->
|
||||
checkRender(
|
||||
page: 4
|
||||
numPages: 7
|
||||
previous: 3
|
||||
first: 1
|
||||
leftdots: false
|
||||
lowPages: [2, 3]
|
||||
highPages: [5, 6]
|
||||
rightdots: false
|
||||
last: 7
|
||||
next: 5
|
||||
)
|
||||
|
||||
it "for middle page of nine (has dots)", ->
|
||||
checkRender(
|
||||
page: 5
|
||||
numPages: 9
|
||||
previous: 4
|
||||
first: 1
|
||||
leftdots: true
|
||||
lowPages: [3, 4]
|
||||
highPages: [6, 7]
|
||||
rightdots: true
|
||||
last: 9
|
||||
next: 6
|
||||
)
|
||||
|
||||
describe "pagination interaction", ->
|
||||
beforeEach ->
|
||||
@view = makeView([], 1, 1)
|
||||
spyOn($, "ajax")
|
||||
|
||||
it "causes updated rendering", ->
|
||||
$.ajax.andCallFake(
|
||||
(params) =>
|
||||
params.success(
|
||||
discussion_data: [{id: "on_page_42", body: "dummy body"}]
|
||||
page: 42
|
||||
num_pages: 99
|
||||
)
|
||||
{always: ->}
|
||||
)
|
||||
@view.$(".pagination a").first().click()
|
||||
expect(@view.$("#thread_on_page_42").length).toEqual(1)
|
||||
expect(@view.$(".pagination-params").data("page")).toEqual(42)
|
||||
expect(@view.$(".pagination-params .last").data("number")).toEqual(99)
|
||||
|
||||
it "handles AJAX errors", ->
|
||||
spyOn(DiscussionUtil, "discussionAlert")
|
||||
$.ajax.andCallFake(
|
||||
(params) =>
|
||||
params.error()
|
||||
{always: ->}
|
||||
)
|
||||
@view.$(".pagination a").first().click()
|
||||
expect(DiscussionUtil.discussionAlert).toHaveBeenCalled()
|
||||
@@ -116,7 +116,7 @@ if Backbone?
|
||||
@discussion.on "add", @addThread
|
||||
@retrieved = true
|
||||
@showed = true
|
||||
@renderPagination(2, response.num_pages)
|
||||
@renderPagination(response.num_pages)
|
||||
if @isWaitingOnNewPost
|
||||
@newPostForm.show()
|
||||
|
||||
@@ -128,21 +128,10 @@ if Backbone?
|
||||
threadView.render()
|
||||
@threadviews.unshift threadView
|
||||
|
||||
renderPagination: (delta, numPages) =>
|
||||
minPage = Math.max(@page - delta, 1)
|
||||
maxPage = Math.min(@page + delta, numPages)
|
||||
renderPagination: (numPages) =>
|
||||
pageUrl = (number) ->
|
||||
"?discussion_page=#{number}"
|
||||
params =
|
||||
page: @page
|
||||
lowPages: _.range(minPage, @page).map (n) -> {number: n, url: pageUrl(n)}
|
||||
highPages: _.range(@page+1, maxPage+1).map (n) -> {number: n, url: pageUrl(n)}
|
||||
previous: if @page-1 >= 1 then {url: pageUrl(@page-1), number: @page-1} else false
|
||||
next: if @page+1 <= numPages then {url: pageUrl(@page+1), number: @page+1} else false
|
||||
leftdots: minPage > 2
|
||||
rightdots: maxPage < numPages-1
|
||||
first: if minPage > 1 then {url: pageUrl(1)} else false
|
||||
last: if maxPage < numPages then {number: numPages, url: pageUrl(numPages)} else false
|
||||
params = DiscussionUtil.getPaginationParams(@page, numPages, pageUrl)
|
||||
thing = Mustache.render @paginationTemplate(), params
|
||||
@$('section.pagination').html(thing)
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@ if Backbone?
|
||||
threads = element.data("threads")
|
||||
user_info = element.data("user-info")
|
||||
window.user = new DiscussionUser(user_info)
|
||||
new DiscussionUserProfileView(el: element, collection: threads)
|
||||
page = element.data("page")
|
||||
numPages = element.data("num-pages")
|
||||
new DiscussionUserProfileView(el: element, collection: threads, page: page, numPages: numPages)
|
||||
$ ->
|
||||
$("section.discussion").each (index, elem) ->
|
||||
DiscussionApp.start(elem)
|
||||
|
||||
@@ -299,3 +299,18 @@ class @DiscussionUtil
|
||||
minLength++
|
||||
return text.substr(0, minLength) + gettext('…')
|
||||
|
||||
@getPaginationParams: (curPage, numPages, pageUrlFunc) =>
|
||||
delta = 2
|
||||
minPage = Math.max(curPage - delta, 1)
|
||||
maxPage = Math.min(curPage + delta, numPages)
|
||||
pageInfo = (pageNum) -> {number: pageNum, url: pageUrlFunc(pageNum)}
|
||||
params =
|
||||
page: curPage
|
||||
lowPages: _.range(minPage, curPage).map(pageInfo)
|
||||
highPages: _.range(curPage+1, maxPage+1).map(pageInfo)
|
||||
previous: if curPage > 1 then pageInfo(curPage - 1) else null
|
||||
next: if curPage < numPages then pageInfo(curPage + 1) else null
|
||||
leftdots: minPage > 2
|
||||
rightdots: maxPage < numPages-1
|
||||
first: if minPage > 1 then pageInfo(1) else null
|
||||
last: if maxPage < numPages then pageInfo(numPages) else null
|
||||
|
||||
@@ -1,23 +1,44 @@
|
||||
if Backbone?
|
||||
class @DiscussionUserProfileView extends Backbone.View
|
||||
# events:
|
||||
# "":""
|
||||
initialize: (options) ->
|
||||
@renderThreads @$el, @collection
|
||||
renderThreads: ($elem, threads) =>
|
||||
#Content.loadContentInfos(response.annotated_content_info)
|
||||
@discussion = new Discussion()
|
||||
@discussion.reset(threads, {silent: false})
|
||||
$discussion = $(Mustache.render $("script#_user_profile").html(), {'threads':threads})
|
||||
$elem.append($discussion)
|
||||
@threadviews = @discussion.map (thread) ->
|
||||
new DiscussionThreadProfileView el: @$("article#thread_#{thread.id}"), model: thread
|
||||
_.each @threadviews, (dtv) -> dtv.render()
|
||||
events:
|
||||
"click .discussion-paginator a": "changePage"
|
||||
|
||||
addThread: (thread, collection, options) =>
|
||||
# TODO: When doing pagination, this will need to repaginate. Perhaps just reload page 1?
|
||||
article = $("<article class='discussion-thread' id='thread_#{thread.id}'></article>")
|
||||
@$('section.discussion > .threads').prepend(article)
|
||||
threadView = new DiscussionThreadInlineView el: article, model: thread
|
||||
threadView.render()
|
||||
@threadviews.unshift threadView
|
||||
initialize: (options) ->
|
||||
super()
|
||||
@page = options.page
|
||||
@numPages = options.numPages
|
||||
@discussion = new Discussion()
|
||||
@discussion.on("reset", @render)
|
||||
@discussion.reset(@collection, {silent: false})
|
||||
|
||||
render: () =>
|
||||
profileTemplate = $("script#_user_profile").html()
|
||||
@$el.html(Mustache.render(profileTemplate, {threads: @discussion.models}))
|
||||
@discussion.map (thread) ->
|
||||
new DiscussionThreadProfileView(el: @$("article#thread_#{thread.id}"), model: thread).render()
|
||||
baseUri = URI(window.location).removeSearch("page")
|
||||
pageUrlFunc = (page) -> baseUri.clone().addSearch("page", page)
|
||||
paginationParams = DiscussionUtil.getPaginationParams(@page, @numPages, pageUrlFunc)
|
||||
paginationTemplate = $("script#_pagination").html()
|
||||
@$el.find(".pagination").html(Mustache.render(paginationTemplate, paginationParams))
|
||||
|
||||
changePage: (event) ->
|
||||
event.preventDefault()
|
||||
url = $(event.target).attr("href")
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$el
|
||||
$loading: $(event.target)
|
||||
takeFocus: true
|
||||
url: url
|
||||
type: "GET"
|
||||
dataType: "json"
|
||||
success: (response, textStatus, xhr) =>
|
||||
@page = response.page
|
||||
@numPages = response.num_pages
|
||||
@discussion.reset(response.discussion_data, {silent: false})
|
||||
history.pushState({}, "", url)
|
||||
error: =>
|
||||
DiscussionUtil.discussionAlert(
|
||||
gettext("Sorry"),
|
||||
gettext("We had some trouble loading the page you requested. Please try again.")
|
||||
)
|
||||
|
||||
@@ -299,8 +299,8 @@ class UserProfileTestCase(ModuleStoreTestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||
html = response.content
|
||||
self.assertRegexpMatches(html, r'var \$\$course_id = \"{}\"'.format(self.course.id))
|
||||
self.assertRegexpMatches(html, r'var \$\$profiled_user_id = \"{}\"'.format(self.profiled_user.id))
|
||||
self.assertRegexpMatches(html, r'data-page="1"')
|
||||
self.assertRegexpMatches(html, r'data-num-pages="1"')
|
||||
self.assertRegexpMatches(html, r'<span>1</span> discussion started')
|
||||
self.assertRegexpMatches(html, r'<span>2</span> comments')
|
||||
self.assertRegexpMatches(html, r'"id": "{}"'.format(self.TEST_THREAD_ID))
|
||||
|
||||
@@ -353,6 +353,8 @@ def user_profile(request, course_id, user_id):
|
||||
'threads': saxutils.escape(json.dumps(threads), escapedict),
|
||||
'user_info': saxutils.escape(json.dumps(user_info), escapedict),
|
||||
'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict),
|
||||
'page': query_params['page'],
|
||||
'num_pages': query_params['num_pages'],
|
||||
# 'content': content,
|
||||
}
|
||||
|
||||
|
||||
@@ -2226,18 +2226,18 @@ body.discussion {
|
||||
a {
|
||||
@include white-button;
|
||||
}
|
||||
}
|
||||
|
||||
li.current-page{
|
||||
height: 35px;
|
||||
padding: 0 15px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 32px;
|
||||
color: #333;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||
&.current-page span {
|
||||
display: inline-block;
|
||||
height: 35px;
|
||||
padding: 0 15px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 32px;
|
||||
color: #333;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
{{#lowPages}}
|
||||
<li><a class="discussion-pagination" href="{{url}}" data-page-number="{{number}}">{{number}}</a></li>
|
||||
{{/lowPages}}
|
||||
<li class="current-page">{{page}}</li>
|
||||
<li class="current-page"><span>{{page}}</span></li>
|
||||
{{#highPages}}
|
||||
<li><a class="discussion-pagination" href="{{url}}" data-page-number="{{number}}">{{number}}</a></li>
|
||||
{{/highPages}}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<section class="discussion-user-threads" >
|
||||
<section class="discussion">
|
||||
{{#threads}}
|
||||
<article class="discussion-thread" id="thread_{{id}}">
|
||||
</article>
|
||||
{{/threads}}
|
||||
</div>
|
||||
<section class="pagination">
|
||||
</section>
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<h2>${_("Active Threads")}</h2>
|
||||
<section class="discussion">
|
||||
{{#threads}}
|
||||
<article class="discussion-thread" id="thread_{{id}}"/>
|
||||
{{/threads}}
|
||||
</section>
|
||||
<section class="pagination"/>
|
||||
|
||||
@@ -32,14 +32,8 @@
|
||||
</nav>
|
||||
</section>
|
||||
|
||||
<section class="course-content container discussion-user-threads" data-user-id="${django_user.id | h}" data-course-id="${course.id | h}" data-threads="${threads}" data-user-info="${user_info}">
|
||||
<h2>${_("Active Threads")}</h2>
|
||||
</section>
|
||||
<section class="course-content container discussion-user-threads" data-course-id="${course.id | h}" data-threads="${threads}" data-user-info="${user_info}" data-page="${page}" data-num-pages="${num_pages}"/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script type="text/javascript">
|
||||
var $$profiled_user_id = "${django_user.id | escapejs}";
|
||||
var $$course_id = "${course.id | escapejs}";
|
||||
</script>
|
||||
<%include file="_underscore_templates.html" />
|
||||
|
||||
Reference in New Issue
Block a user