Commit 0e59038c by Greg Price

Add pagination to forum user profile page

parent 643069c6
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
if Backbone?
class @DiscussionUserProfileView extends Backbone.View
# events:
# "":""
events:
"click .discussion-paginator a": "changePage"
initialize: (options) ->
@renderThreads @$el, @collection
renderThreads: ($elem, threads) =>
#Content.loadContentInfos(response.annotated_content_info)
super()
@page = options.page
@numPages = options.numPages
@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()
@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))
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
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'&quot;id&quot;: &quot;{}&quot;'.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}}
......
<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" />
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment