Commit 02feaf16 by Jim Abramson

Merge pull request #82 from edx/jsa/paginate-responses

allow pagination of responses within thread views
parents 37d3d437 6a09ca32
......@@ -18,8 +18,22 @@ get "#{APIPREFIX}/threads/:thread_id" do |thread_id|
user.mark_as_read(thread) if user
end
presenter = ThreadPresenter.new([thread], user || nil, thread.course_id)
presenter.to_hash_array(true).first.to_json
presenter = ThreadPresenter.factory(thread, user || nil)
if params.has_key?("resp_skip")
unless (resp_skip = Integer(params["resp_skip"]) rescue nil) && resp_skip >= 0
error 400, [t(:param_must_be_a_non_negative_number, :param => 'resp_skip')].to_json
end
else
resp_skip = 0
end
if params["resp_limit"]
unless (resp_limit = Integer(params["resp_limit"]) rescue nil) && resp_limit >= 0
error 400, [t(:param_must_be_a_number_greater_than_zero, :param => 'resp_limit')].to_json
end
else
resp_limit = nil
end
presenter.to_hash(true, resp_skip, resp_limit).to_json
end
put "#{APIPREFIX}/threads/:thread_id" do |thread_id|
......@@ -29,8 +43,8 @@ put "#{APIPREFIX}/threads/:thread_id" do |thread_id|
if thread.errors.any?
error 400, thread.errors.full_messages.to_json
else
presenter = ThreadPresenter.new([thread], nil, thread.course_id)
presenter.to_hash_array.first.to_json
presenter = ThreadPresenter.factory(thread, nil)
presenter.to_hash.to_json
end
end
......
......@@ -42,12 +42,12 @@ get "#{APIPREFIX}/search/threads" do
if results.length == 0
collection = []
else
pres_threads = ThreadSearchResultPresenter.new(
pres_threads = ThreadSearchResultsPresenter.new(
results,
params[:user_id] ? user : nil,
params[:course_id] || results.first.course_id
)
collection = pres_threads.to_hash_array(bool_recursive)
collection = pres_threads.to_hash
end
num_pages = results.total_pages
......
......@@ -41,8 +41,8 @@ get "#{APIPREFIX}/users/:user_id/active_threads" do |user_id|
paged_thread_ids.index(t.id)
end
presenter = ThreadPresenter.new(paged_active_threads.to_a, user, params[:course_id])
collection = presenter.to_hash_array()
presenter = ThreadListPresenter.new(paged_active_threads.to_a, user, params[:course_id])
collection = presenter.to_hash
json_output = nil
self.class.trace_execution_scoped(['Custom/get_user_active_threads/json_serialize']) do
......
......@@ -178,12 +178,12 @@ helpers do
if threads.length == 0
collection = []
else
pres_threads = ThreadPresenter.new(
pres_threads = ThreadListPresenter.new(
threads,
params[:user_id] ? user : nil,
params[:course_id] || threads.first.course_id
)
collection = pres_threads.to_hash_array(bool_recursive)
collection = pres_threads.to_hash
end
json_output = nil
......
......@@ -5,4 +5,6 @@ en-US:
value_is_required: "Value is required"
value_is_invalid: "Value is invalid"
anonymous: "anonymous"
blocked_content_with_body_hash: blocked content with body hash %{hash}
blocked_content_with_body_hash: "blocked content with body hash %{hash}"
param_must_be_a_non_negative_number: "%{param} must be a non-negative number"
param_must_be_a_number_greater_than_zero: "%{param} must be a number greater than zero"
require_relative 'thread_utils'
require 'new_relic/agent/method_tracer'
class ThreadPresenter
def initialize(comment_threads, user, course_id)
@threads = comment_threads
@user = user
@course_id = course_id
@read_dates = nil # Hash, sparse, thread_key (str) => date
@unread_counts = nil # Hash, sparse, thread_key (str) => int
@endorsed_threads = nil # Hash, sparse, thread_key (str) => bool
load_aggregates
def self.factory(thread, user)
# use when working with one thread at a time. fetches extended /
# derived attributes from the db and explicitly initializes an instance.
course_id = thread.course_id
thread_key = thread._id.to_s
is_read, unread_count = ThreadUtils.get_read_states([thread], user, course_id).fetch(thread_key, [false, thread.comment_count])
is_endorsed = ThreadUtils.get_endorsed([thread]).fetch(thread_key, false)
self.new thread, user, is_read, unread_count, is_endorsed
end
def load_aggregates
@read_dates = {}
if @user
read_state = @user.read_states.where(:course_id => @course_id).first
if read_state
@read_dates = read_state["last_read_times"].to_hash
end
end
@unread_counts = {}
@endorsed_threads = {}
thread_ids = @threads.collect {|t| t._id}
Comment.collection.aggregate(
{"$match" => {"comment_thread_id" => {"$in" => thread_ids}, "endorsed" => true}},
{"$group" => {"_id" => "$comment_thread_id"}}
).each do |res|
@endorsed_threads[res["_id"].to_s] = true
end
@threads.each do |t|
thread_key = t._id.to_s
if @read_dates.has_key? thread_key
@unread_counts[thread_key] = Comment.collection.where(
:comment_thread_id => t._id,
:author_id => {"$ne" => @user.id},
:updated_at => {"$gte" => @read_dates[thread_key]}
).count
end
end
def initialize(thread, user, is_read, unread_count, is_endorsed)
# generally not intended for direct use. instantiated by self.factory or
# by thread list presenters.
@thread = thread
@user = user
@is_read = is_read
@unread_count = unread_count
@is_endorsed = is_endorsed
end
def to_hash thread, with_comments=false
thread_key = thread._id.to_s
h = thread.to_hash
if @user
cnt_unread = @unread_counts.fetch(thread_key, thread.comment_count)
h["unread_comments_count"] = cnt_unread
h["read"] = @read_dates.has_key?(thread_key) && @read_dates[thread_key] >= thread.updated_at
else
h["unread_comments_count"] = thread.comment_count
h["read"] = false
def to_hash with_responses=false, resp_skip=0, resp_limit=nil
raise ArgumentError unless resp_skip >= 0
raise ArgumentError unless resp_limit.nil? or resp_limit >= 1
h = @thread.to_hash
h["read"] = @is_read
h["unread_comments_count"] = @unread_count
h["endorsed"] = @is_endorsed || false
if with_responses
unless resp_skip == 0 && resp_limit.nil?
# need to find responses first, set the window accordingly, then fetch the comments
# bypass mongoid/odm, to get just the response ids we need as directly as possible
responses = Content.collection.find({"comment_thread_id" => @thread._id, "parent_id" => {"$exists" => false}})
responses = responses.sort({"sk" => 1})
all_response_ids = responses.select({"_id" => 1}).to_a.map{|doc| doc["_id"] }
response_ids = (resp_limit.nil? ? all_response_ids[resp_skip..-1] : (all_response_ids[resp_skip,resp_limit])) || []
# now use the odm to fetch the desired responses and their comments
content = Content.where({"parent_id" => {"$in" => response_ids}}).to_a + Content.where({"_id" => {"$in" => response_ids}}).to_a
content.sort!{|a,b| a.sk <=> b.sk }
response_total = all_response_ids.length
else
content = Content.where({"comment_thread_id" => @thread._id}).order_by({"sk"=> 1})
response_total = content.to_a.select{|d| d.depth == 0 }.length
end
h = merge_comments_recursive(h, content)
h["resp_skip"] = resp_skip
h["resp_limit"] = resp_limit
h["resp_total"] = response_total
end
h["endorsed"] = @endorsed_threads.fetch(thread_key, false)
h = merge_comments_recursive(h) if with_comments
h
end
def to_hash_array with_comments=false
@threads.map {|t| to_hash(t, with_comments)}
end
def merge_comments_recursive thread_hash
def merge_comments_recursive thread_hash, comments
thread_id = thread_hash["id"]
root = thread_hash = thread_hash.merge("children" => [])
# Content model is used deliberately here (instead of Comment), to work with sparse index
rs = Content.where(comment_thread_id: thread_id).order_by({"sk"=> 1})
ancestry = [thread_hash]
# weave the fetched comments into a single hierarchical doc
rs.each do | comment |
comments.each do | comment |
thread_hash = comment.to_hash.merge("children" => [])
parent_id = comment.parent_id || thread_id
found_parent = false
......@@ -94,13 +82,11 @@ class ThreadPresenter
ancestry = [root]
end
end
ancestry.first
ancestry.first
end
include ::NewRelic::Agent::MethodTracer
add_method_tracer :load_aggregates
add_method_tracer :to_hash
add_method_tracer :to_hash_array
add_method_tracer :merge_comments_recursive
end
require_relative 'thread'
require_relative 'thread_utils'
class ThreadListPresenter
def initialize(threads, user, course_id)
read_states = ThreadUtils.get_read_states(threads, user, course_id)
threads_endorsed = ThreadUtils.get_endorsed(threads)
@presenters = threads.map do |thread|
thread_key = thread._id.to_s
is_read, unread_count = read_states.fetch(thread_key, [false, thread.comment_count])
is_endorsed = threads_endorsed.fetch(thread_key, false)
ThreadPresenter.new(thread, user, is_read, unread_count, is_endorsed)
end
end
def to_hash
@presenters.map { |p| p.to_hash }
end
end
require_relative 'thread'
require_relative 'thread_list'
class ThreadSearchResultPresenter < ThreadPresenter
class ThreadSearchResultsPresenter < ThreadListPresenter
alias :super_to_hash :to_hash
......@@ -12,12 +12,13 @@ class ThreadSearchResultPresenter < ThreadPresenter
super(threads, user, course_id)
end
def to_hash(thread, with_comments=false)
thread_hash = super_to_hash(thread, with_comments)
highlight = @search_result_map[thread.id.to_s].highlight || {}
thread_hash["highlighted_body"] = (highlight[:body] || []).first || thread_hash["body"]
thread_hash["highlighted_title"] = (highlight[:title] || []).first || thread_hash["title"]
thread_hash
def to_hash
super_to_hash.each do |thread_hash|
thread_key = thread_hash['id'].to_s
highlight = @search_result_map[thread_key].highlight || {}
thread_hash["highlighted_body"] = (highlight[:body] || []).first || thread_hash["body"]
thread_hash["highlighted_title"] = (highlight[:title] || []).first || thread_hash["title"]
end
end
end
module ThreadUtils
def self.get_endorsed(threads)
# returns sparse hash {thread_key => true, ...}
# only threads which are endorsed will have entries, value will always be true.
endorsed_threads = {}
thread_ids = threads.collect {|t| t._id}
Comment.collection.aggregate(
{"$match" => {"comment_thread_id" => {"$in" => thread_ids}, "endorsed" => true}},
{"$group" => {"_id" => "$comment_thread_id"}}
).each do |res|
endorsed_threads[res["_id"].to_s] = true
end
endorsed_threads
end
def self.get_read_states(threads, user, course_id)
# returns sparse hash {thread_key => [is_read, unread_comment_count], ...}
read_states = {}
if user
read_dates = {}
read_state = user.read_states.where(:course_id => course_id).first
if read_state
read_dates = read_state["last_read_times"].to_hash
threads.each do |t|
thread_key = t._id.to_s
if read_dates.has_key? thread_key
is_read = read_dates[thread_key] >= t.updated_at
unread_comment_count = Comment.collection.where(
:comment_thread_id => t._id,
:author_id => {"$ne" => user.id},
:updated_at => {"$gte" => read_dates[thread_key]}
).count
read_states[thread_key] = [is_read, unread_comment_count]
end
end
end
end
read_states
end
extend self
include ::NewRelic::Agent::MethodTracer
add_method_tracer :get_read_states
add_method_tracer :get_endorsed
end
......@@ -4,6 +4,7 @@ require 'unicode_shared_examples'
describe "app" do
describe "comment threads" do
describe "GET /api/v1/threads" do
before(:each) { setup_10_threads }
......@@ -23,7 +24,7 @@ describe "app" do
rs = thread_result course_id: "abc", sort_order: "asc"
rs.length.should == 2
rs.each_with_index { |res, i|
check_thread_result(nil, @threads["t#{i+1}"], res)
check_thread_result_json(nil, @threads["t#{i+1}"], res)
res["course_id"].should == "abc"
}
end
......@@ -42,8 +43,8 @@ describe "app" do
@threads["t4"].save!
rs = thread_result course_id: "course1", commentable_ids: "commentable1,commentable3"
rs.length.should == 2
check_thread_result(nil, @threads["t3"], rs[0])
check_thread_result(nil, @threads["t1"], rs[1])
check_thread_result_json(nil, @threads["t3"], rs[0])
check_thread_result_json(nil, @threads["t1"], rs[1])
end
it "returns only threads where course id and group id match" do
@threads["t1"].course_id = "omg"
......@@ -54,7 +55,7 @@ describe "app" do
@threads["t2"].save!
rs = thread_result course_id: "omg", group_id: 100
rs.length.should == 1
check_thread_result(nil, @threads["t1"], rs.first)
check_thread_result_json(nil, @threads["t1"], rs.first)
end
it "returns only threads where course id and group id match or group id is nil" do
@threads["t1"].course_id = "omg"
......@@ -67,7 +68,7 @@ describe "app" do
rs = thread_result course_id: "omg", group_id: 100, sort_order: "asc"
rs.length.should == 2
rs.each_with_index { |res, i|
check_thread_result(nil, @threads["t#{i+1}"], res)
check_thread_result_json(nil, @threads["t#{i+1}"], res)
res["course_id"].should == "omg"
}
end
......@@ -87,14 +88,14 @@ describe "app" do
@threads["t1"].save!
rs = thread_result course_id: DFLT_COURSE_ID, flagged: true
rs.length.should == 1
check_thread_result(nil, @threads["t1"], rs.first)
check_thread_result_json(nil, @threads["t1"], rs.first)
end
it "returns threads that have flagged comments" do
@comments["t2 c3"].abuse_flaggers = [1]
@comments["t2 c3"].save!
rs = thread_result course_id: DFLT_COURSE_ID, flagged: true
rs.length.should == 1
check_thread_result(nil, @threads["t2"], rs.first)
check_thread_result_json(nil, @threads["t2"], rs.first)
end
it "returns an empty result when no posts were flagged" do
rs = thread_result course_id: DFLT_COURSE_ID, flagged: true
......@@ -110,7 +111,7 @@ describe "app" do
rs = thread_result course_id: "abc", user_id: "123", sort_order: "asc"
rs.length.should == 2
rs.each_with_index { |result, i|
check_thread_result(user, @threads["t#{i+1}"], result)
check_thread_result_json(user, @threads["t#{i+1}"], result)
result["course_id"].should == "abc"
result["unread_comments_count"].should == 5
result["read"].should == false
......@@ -120,7 +121,7 @@ describe "app" do
rs = thread_result course_id: "abc", user_id: "123", sort_order: "asc"
rs.length.should == 2
rs.each_with_index { |result, i|
check_thread_result(user, @threads["t#{i+1}"], result)
check_thread_result_json(user, @threads["t#{i+1}"], result)
}
rs[0]["read"].should == true
rs[0]["unread_comments_count"].should == 0
......@@ -132,7 +133,7 @@ describe "app" do
rs = thread_result course_id: "abc", user_id: "123", sort_order: "asc"
rs.length.should == 2
rs.each_with_index { |result, i|
check_thread_result(user, @threads["t#{i+1}"], result)
check_thread_result_json(user, @threads["t#{i+1}"], result)
}
rs[0]["read"].should == false # no unread comments, but the thread itself was updated
rs[0]["unread_comments_count"].should == 0
......@@ -307,8 +308,8 @@ describe "app" do
course_id = "unicode_course"
thread = make_thread(User.first, text, course_id, "unicode_commentable")
make_comment(User.first, thread, text)
result = thread_result(course_id: course_id, recursive: true).first
check_thread_result(nil, thread, result, true)
result = thread_result(course_id: course_id).first
check_thread_result_json(nil, thread, result)
end
include_examples "unicode data"
......@@ -330,7 +331,7 @@ describe "app" do
get "/api/v1/threads/#{thread.id}"
last_response.should be_ok
response_thread = parse last_response.body
check_thread_result(nil, thread, response_thread)
check_thread_result_json(nil, thread, response_thread)
end
it "computes endorsed? correctly" do
......@@ -342,7 +343,7 @@ describe "app" do
last_response.should be_ok
response_thread = parse last_response.body
response_thread["endorsed"].should == true
check_thread_result(nil, thread, response_thread)
check_thread_result_json(nil, thread, response_thread)
end
# This is a test to ensure that the username is included even if the
......@@ -370,7 +371,8 @@ describe "app" do
thread = CommentThread.first
get "/api/v1/threads/#{thread.id}", recursive: true
last_response.should be_ok
check_thread_result(nil, thread, parse(last_response.body), true)
check_thread_result_json(nil, thread, parse(last_response.body))
check_thread_response_paging_json(thread, parse(last_response.body))
end
it "returns 400 when the thread does not exist" do
......@@ -388,11 +390,74 @@ describe "app" do
get "/api/v1/threads/#{thread.id}", recursive: true
last_response.should be_ok
result = parse last_response.body
check_thread_result(nil, thread, result, true)
check_thread_result_json(nil, thread, result)
check_thread_response_paging_json(thread, result)
end
include_examples "unicode data"
context "response pagination" do
before(:each) do
User.all.delete
Content.all.delete
@user = create_test_user(999)
@threads = {}
@comments = {}
[20,10,3,2,1,0].each do |n|
thread_key = "t#{n}"
thread = make_thread(@user, thread_key, DFLT_COURSE_ID, "pdq")
@threads[n] = thread
n.times do |i|
# generate n responses in this thread
comment_key = "#{thread_key} r#{i}"
comment = make_comment(@user, thread, comment_key)
i.times do |j|
subcomment_key = "#{comment_key} c#{j}"
subcomment = make_comment(@user, comment, subcomment_key)
end
@comments[comment_key] = comment
end
end
end
def thread_result(id, params)
get "/api/v1/threads/#{id}", params
last_response.should be_ok
parse(last_response.body)
end
it "returns all responses when no skip/limit params given" do
@threads.each do |n, thread|
res = thread_result thread.id, {}
check_thread_response_paging_json thread, res
end
end
it "skips the specified number of responses" do
@threads.each do |n, thread|
res = thread_result thread.id, {:resp_skip => 1}
check_thread_response_paging_json thread, res, 1, nil
end
end
it "limits the specified number of responses" do
@threads.each do |n, thread|
res = thread_result thread.id, {:resp_limit => 2}
check_thread_response_paging_json thread, res, 0, 2
end
end
it "skips and limits responses" do
@threads.each do |n, thread|
res = thread_result thread.id, {:resp_skip => 3, :resp_limit => 5}
check_thread_response_paging_json thread, res, 3, 5
end
end
end
end
describe "PUT /api/v1/threads/:thread_id" do
before(:each) { init_without_subscriptions }
......@@ -405,7 +470,7 @@ describe "app" do
changed_thread.body.should == "new body"
changed_thread.title.should == "new title"
changed_thread.commentable_id.should == "new_commentable_id"
check_thread_result(nil, changed_thread, parse(last_response.body))
check_thread_result_json(nil, changed_thread, parse(last_response.body))
end
it "returns 400 when the thread does not exist" do
put "/api/v1/threads/does_not_exist", body: "new body", title: "new title"
......
......@@ -25,26 +25,6 @@ describe "app" do
threads.index{|c| c["body"] == "can anyone help me?"}.should_not be_nil
threads.index{|c| c["body"] == "it is unsolvable"}.should_not be_nil
end
it "get all comment threads and comments associated with a commentable object" do
get "/api/v1/question_1/threads", recursive: true
last_response.should be_ok
response = parse last_response.body
threads = response['collection']
threads.length.should == 2
threads.index{|c| c["body"] == "can anyone help me?"}.should_not be_nil
threads.index{|c| c["body"] == "it is unsolvable"}.should_not be_nil
thread = threads.select{|c| c["body"] == "can anyone help me?"}.first
children = thread["children"]
children.length.should == 2
children.index{|c| c["body"] == "this problem is so easy"}.should_not be_nil
children.index{|c| c["body"] =~ /^see the textbook/}.should_not be_nil
so_easy = children.select{|c| c["body"] == "this problem is so easy"}.first
so_easy["children"].length.should == 1
not_for_me = so_easy["children"].first
not_for_me["body"].should == "not for me!"
not_for_me["children"].length.should == 1
not_for_me["children"].first["body"].should == "not for me neither!"
end
it "returns an empty array when the commentable object does not exist (no threads)" do
get "/api/v1/does_not_exist/threads"
last_response.should be_ok
......@@ -57,11 +37,11 @@ describe "app" do
commentable_id = "unicode_commentable"
thread = make_thread(User.first, text, "unicode_course", commentable_id)
make_comment(User.first, thread, text)
get "/api/v1/#{commentable_id}/threads", recursive: true
get "/api/v1/#{commentable_id}/threads"
last_response.should be_ok
result = parse(last_response.body)["collection"]
result.should_not be_empty
check_thread_result(nil, thread, result.first, true)
check_thread_result_json(nil, thread, result.first)
end
include_examples "unicode data"
......
......@@ -18,7 +18,7 @@ describe "app" do
get "/api/v1/search/threads", text: random_string
last_response.should be_ok
threads = parse(last_response.body)['collection']
check_thread_result(nil, thread, threads.select{|t| t["id"] == thread.id.to_s}.first, false, true)
check_thread_result_json(nil, thread, threads.select{|t| t["id"] == thread.id.to_s}.first, true)
end
end
......@@ -47,7 +47,7 @@ describe "app" do
get "/api/v1/search/threads", text: random_string
last_response.should be_ok
threads = parse(last_response.body)['collection']
check_thread_result(nil, thread, threads.select{|t| t["id"] == thread.id.to_s}.first, false, true)
check_thread_result_json(nil, thread, threads.select{|t| t["id"] == thread.id.to_s}.first, true)
end
end
end
......
......@@ -16,11 +16,11 @@ describe "app" do
# Elasticsearch does not necessarily make newly indexed content
# available immediately, so we must explicitly refresh the index
CommentThread.tire.index.refresh
get "/api/v1/search/threads", course_id: course_id, text: search_term, recursive: true
get "/api/v1/search/threads", course_id: course_id, text: search_term
last_response.should be_ok
result = parse(last_response.body)["collection"]
result.length.should == 1
check_thread_result(nil, thread, result.first, true, true)
check_thread_result_json(nil, thread, result.first, true)
end
include_examples "unicode data"
......
......@@ -69,8 +69,8 @@ describe "app" do
@comments["t3 c4"].save!
rs = thread_result 100, course_id: "xyz"
rs.length.should == 2
check_thread_result(@users["u100"], @threads["t3"], rs[0], false)
check_thread_result(@users["u100"], @threads["t0"], rs[1], false)
check_thread_result_json(@users["u100"], @threads["t3"], rs[0])
check_thread_result_json(@users["u100"], @threads["t0"], rs[1])
end
it "does not return threads in which the user has only participated anonymously" do
......@@ -79,7 +79,7 @@ describe "app" do
@comments["t3 c4"].save!
rs = thread_result 100, course_id: "xyz"
rs.length.should == 1
check_thread_result(@users["u100"], @threads["t0"], rs.first, false)
check_thread_result_json(@users["u100"], @threads["t0"], rs.first)
end
it "only returns threads from the specified course" do
......@@ -164,7 +164,7 @@ describe "app" do
make_comment(user, thread, text)
result = thread_result(user.id, course_id: course_id)
result.length.should == 1
check_thread_result(nil, thread, result.first)
check_thread_result_json(nil, thread, result.first)
end
include_examples "unicode data"
......
require 'spec_helper'
describe ThreadListPresenter do
context "#initialize" do
before(:each) do
User.all.delete
Content.all.delete
@threads = (1..3).map do |n|
t = make_thread(
create_test_user("author#{n}"),
"thread #{n}",
'foo', 'bar'
)
end
@reader = create_test_user('reader')
end
it "handles unread threads" do
pres = ThreadListPresenter.new(@threads, @reader, 'foo')
pres.to_hash.each_with_index do |h, i|
h.should == ThreadPresenter.factory(@threads[i], @reader).to_hash
end
end
it "handles read threads" do
@reader.mark_as_read(@threads[0])
@reader.save!
pres = ThreadListPresenter.new(@threads, @reader, 'foo')
pres.to_hash.each_with_index do |h, i|
h.should == ThreadPresenter.factory(@threads[i], @reader).to_hash
end
end
it "handles empty list of threads" do
pres = ThreadListPresenter.new([], @reader, 'foo')
pres.to_hash.should == []
end
end
end
require 'spec_helper'
describe ThreadSearchResultPresenter do
describe ThreadSearchResultsPresenter do
context "#to_hash" do
before(:each) { setup_10_threads }
......@@ -11,7 +11,7 @@ describe ThreadSearchResultPresenter do
hash["highlighted_title"].should == ((search_result.highlight[:title] || []).first || hash["title"])
end
def check_search_results_hash_array(search_results, hashes)
def check_search_results_hash(search_results, hashes)
expected_order = search_results.map {|t| t.id}
actual_order = hashes.map {|h| h["id"].to_s}
actual_order.should == expected_order
......@@ -23,8 +23,8 @@ describe ThreadSearchResultPresenter do
mock_results = threads_random_order.map do |t|
double(Tire::Results::Item, :id => t._id.to_s, :highlight => {:body => ["foo"], :title => ["bar"]})
end
pres = ThreadSearchResultPresenter.new(mock_results, nil, DFLT_COURSE_ID)
check_search_results_hash_array(mock_results, pres.to_hash_array)
pres = ThreadSearchResultsPresenter.new(mock_results, nil, DFLT_COURSE_ID)
check_search_results_hash(mock_results, pres.to_hash)
end
it "presents search results with correct default highlights" do
......@@ -32,8 +32,8 @@ describe ThreadSearchResultPresenter do
mock_results = threads_random_order.map do |t|
double(Tire::Results::Item, :id => t._id.to_s, :highlight => {})
end
pres = ThreadSearchResultPresenter.new(mock_results, nil, DFLT_COURSE_ID)
check_search_results_hash_array(mock_results, pres.to_hash_array)
pres = ThreadSearchResultsPresenter.new(mock_results, nil, DFLT_COURSE_ID)
check_search_results_hash(mock_results, pres.to_hash)
end
end
......
require 'spec_helper'
describe ThreadPresenter do
context "#to_hash_array" do
context "#to_hash" do
before(:each) do
User.all.delete
Content.all.delete
course_id, commentable_id = ['foo', 'bar']
@thread_no_responses = make_thread(
create_test_user('author1'),
'thread with no responses',
course_id, commentable_id
)
@thread_one_empty_response = make_thread(
create_test_user('author2'),
'thread with one response',
course_id, commentable_id
)
make_comment(create_test_user('author3'), @thread_one_empty_response, 'empty response')
@thread_one_response = make_thread(
create_test_user('author4'),
'thread with one response and some comments',
course_id, commentable_id
)
resp = make_comment(
create_test_user('author5'),
@thread_one_response,
'a response'
)
make_comment(create_test_user('author6'), resp, 'first comment')
make_comment(create_test_user('author7'), resp, 'second comment')
make_comment(create_test_user('author8'), resp, 'third comment')
@thread_ten_responses = make_thread(
create_test_user('author9'),
'thread with ten responses',
course_id, commentable_id
)
(1..10).each do |n|
resp = make_comment(create_test_user("author#{n+10}"), @thread_ten_responses, "response #{n}")
(1..3).each do |n2|
make_comment(create_test_user("author#{n+10}+#{n2}"), resp, "comment #{n+10}+#{n}")
end
end
@threads_with_num_comments = [
[@thread_no_responses, 0],
[@thread_one_empty_response, 1],
[@thread_one_response, 4],
[@thread_ten_responses, 40]
]
@reader = create_test_user('thread reader')
end
it "handles with_responses=false" do
@threads_with_num_comments.each do |thread, num_comments|
hash = ThreadPresenter.new(thread, @reader, false, num_comments, false).to_hash
check_thread_result(@reader, thread, hash)
['children', 'resp_skip', 'resp_limit', 'resp_total'].each {|k| (hash.has_key? k).should be_false }
end
end
it "handles with_responses=true" do
@threads_with_num_comments.each do |thread, num_comments|
hash = ThreadPresenter.new(thread, @reader, false, num_comments, false).to_hash true
check_thread_result(@reader, thread, hash)
check_thread_response_paging(thread, hash)
end
end
it "handles skip with no limit" do
@threads_with_num_comments.each do |thread, num_comments|
[0, 1, 2, 9, 10, 11, 1000].each do |skip|
hash = ThreadPresenter.new(thread, @reader, false, num_comments, false).to_hash true, skip
check_thread_result(@reader, thread, hash)
check_thread_response_paging(thread, hash, skip)
end
end
end
it "handles skip and limit" do
@threads_with_num_comments.each do |thread, num_comments|
[1, 2, 3, 9, 10, 11, 1000].each do |limit|
[0, 1, 2, 9, 10, 11, 1000].each do |skip|
hash = ThreadPresenter.new(thread, @reader, false, num_comments, false).to_hash true, skip, limit
check_thread_result(@reader, thread, hash)
check_thread_response_paging(thread, hash, skip, limit)
end
end
end
end
it "fails with invalid arguments" do
@threads_with_num_comments.each do |thread, num_comments|
[-1, true, "", nil].each do |skip|
expect{ThreadPresenter.new(thread, @reader, false, num_comments, false).to_hash true, 0, limit}.to raise_error
end
[-1, 0, true, "hello"].each do |limit|
expect{ThreadPresenter.new(thread, @reader, false, num_comments, false).to_hash true, 0, limit}.to raise_error
end
end
end
end
context "#merge_comments_recursive" do
before(:each) { @cid_seq = 10 }
def stub_each_from_array(obj, ary)
......@@ -12,13 +121,20 @@ describe ThreadPresenter do
end
def set_comment_results(thread, ary)
# example usage:
# c0 = make_comment()
# c00 = make_comment(c0)
# c01 = make_comment(c0)
# c010 = make_comment(c01)
# set_comment_results(thread, [c0, c00, c01, c010])
# avoids an unrelated expecation error
thread.stub(:endorsed?).and_return(true)
rs = stub_each_from_array(double("rs"), ary)
criteria = double("criteria")
criteria.stub(:order_by).and_return(rs)
# stub Content, not Comment, because that's the model we will be querying against
Content.stub(:where).with(comment_thread_id: thread.id).and_return(criteria)
Content.stub(:where).with({"comment_thread_id" => thread.id}).and_return(criteria)
end
def make_comment(parent=nil)
......@@ -30,17 +146,13 @@ describe ThreadPresenter do
end
it "nests comments in the correct order" do
thread = CommentThread.new
thread.id = 1
thread.author = User.new
c0 = make_comment()
c00 = make_comment(c0)
c01 = make_comment(c0)
c010 = make_comment(c01)
set_comment_results(thread, [c0, c00, c01, c010])
h = ThreadPresenter.new([thread], nil, thread.course_id).to_hash_array(true).first
pres = ThreadPresenter.new(nil, nil, nil, nil, nil)
h = pres.merge_comments_recursive({}, [c0, c00, c01, c010])
h["children"].size.should == 1 # c0
h["children"][0]["id"].should == c0.id
h["children"][0]["children"].size.should == 2 # c00, c01
......@@ -51,10 +163,6 @@ describe ThreadPresenter do
end
it "handles orphaned child comments gracefully" do
thread = CommentThread.new
thread.id = 33
thread.author = User.new
c0 = make_comment()
c00 = make_comment(c0)
c000 = make_comment(c00)
......@@ -64,9 +172,9 @@ describe ThreadPresenter do
c111 = make_comment(c11)
# lose c0 and c11 from result set. their descendants should
# be silently skipped over.
set_comment_results(thread, [c00, c000, c1, c10, c111])
h = ThreadPresenter.new([thread], nil, thread.course_id).to_hash_array(true).first
pres = ThreadPresenter.new(nil, nil, nil, nil, nil)
h = pres.merge_comments_recursive({}, [c00, c000, c1, c10, c111])
h["children"].size.should == 1 # c1
h["children"][0]["id"].should == c1.id
h["children"][0]["children"].size.should == 1 # c10
......
......@@ -177,7 +177,7 @@ end
# this method is used to test results produced using the helper function handle_threads_query
# which is used in multiple areas of the API
def check_thread_result(user, thread, json_response, check_comments=false, is_search=false)
def check_thread_result(user, thread, hash, is_search=false, is_json=false)
expected_keys = %w(id title body course_id commentable_id created_at updated_at)
expected_keys += %w(anonymous anonymous_to_peers at_position_list closed user_id)
expected_keys += %w(username votes abuse_flaggers tags type group_id pinned)
......@@ -185,53 +185,44 @@ def check_thread_result(user, thread, json_response, check_comments=false, is_se
if is_search
expected_keys += %w(highlighted_body highlighted_title)
end
# the "children" key is not always present - depends on the invocation + test use case.
# exclude it from this check - if check_comments is set, we'll assert against it later
actual_keys = json_response.keys - ["children"]
# these keys are checked separately, when desired, using check_thread_response_paging.
actual_keys = hash.keys - ["children", "resp_skip", "resp_limit", "resp_total"]
actual_keys.sort.should == expected_keys.sort
json_response["title"].should == thread.title
json_response["body"].should == thread.body
json_response["course_id"].should == thread.course_id
json_response["anonymous"].should == thread.anonymous
json_response["anonymous_to_peers"].should == thread.anonymous_to_peers
json_response["commentable_id"].should == thread.commentable_id
json_response["created_at"].should == thread.created_at.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
json_response["updated_at"].should == thread.updated_at.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
json_response["at_position_list"].should == thread.at_position_list
json_response["closed"].should == thread.closed
json_response["id"].should == thread._id.to_s
json_response["user_id"].should == thread.author.id
json_response["username"].should == thread.author.username
json_response["votes"]["point"].should == thread.votes["point"]
json_response["votes"]["count"].should == thread.votes["count"]
json_response["votes"]["up_count"].should == thread.votes["up_count"]
json_response["votes"]["down_count"].should == thread.votes["down_count"]
json_response["abuse_flaggers"].should == thread.abuse_flaggers
json_response["tags"].should == []
json_response["type"].should == "thread"
json_response["group_id"].should == thread.group_id
json_response["pinned"].should == thread.pinned?
json_response["endorsed"].should == thread.endorsed?
if check_comments
# warning - this only checks top-level comments and may not handle all possible sorting scenarios
# proper composition / ordering of the children is currently covered in models/comment_thread_spec.
# it also does not check for author-only results (e.g. user active threads view)
# author-only is covered by a test in api/user_spec.
root_comments = thread.root_comments.sort(_id:1).to_a
json_response["children"].should_not be_nil
json_response["children"].length.should == root_comments.length
json_response["children"].each_with_index { |v, i|
v["body"].should == root_comments[i].body
v["user_id"].should == root_comments[i].author_id
v["username"].should == root_comments[i].author_username
}
hash["title"].should == thread.title
hash["body"].should == thread.body
hash["course_id"].should == thread.course_id
hash["anonymous"].should == thread.anonymous
hash["anonymous_to_peers"].should == thread.anonymous_to_peers
hash["commentable_id"].should == thread.commentable_id
hash["at_position_list"].should == thread.at_position_list
hash["closed"].should == thread.closed
hash["user_id"].should == thread.author.id
hash["username"].should == thread.author.username
hash["votes"]["point"].should == thread.votes["point"]
hash["votes"]["count"].should == thread.votes["count"]
hash["votes"]["up_count"].should == thread.votes["up_count"]
hash["votes"]["down_count"].should == thread.votes["down_count"]
hash["abuse_flaggers"].should == thread.abuse_flaggers
hash["tags"].should == []
hash["type"].should == "thread"
hash["group_id"].should == thread.group_id
hash["pinned"].should == thread.pinned?
hash["endorsed"].should == thread.endorsed?
hash["comments_count"].should == thread.comments.length
if is_json
hash["id"].should == thread._id.to_s
hash["created_at"].should == thread.created_at.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
hash["updated_at"].should == thread.updated_at.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
else
hash["created_at"].should == thread.created_at
hash["updated_at"].should == thread.updated_at
end
json_response["comments_count"].should == thread.comments.length
if user.nil?
json_response["unread_comments_count"].should == thread.comments.length
json_response["read"].should == false
hash["unread_comments_count"].should == thread.comments.length
hash["read"].should == false
else
expected_unread_cnt = thread.comments.length # initially assume nothing has been read
read_states = user.read_states.where(course_id: thread.course_id).to_a
......@@ -243,15 +234,56 @@ def check_thread_result(user, thread, json_response, check_comments=false, is_se
expected_unread_cnt -= 1
end
end
json_response["read"].should == (read_date >= thread.updated_at)
hash["read"].should == (read_date >= thread.updated_at)
else
json_response["read"].should == false
hash["read"].should == false
end
end
json_response["unread_comments_count"].should == expected_unread_cnt
hash["unread_comments_count"].should == expected_unread_cnt
end
end
def check_thread_result_json(user, thread, json_response, is_search=false)
check_thread_result(user, thread, json_response, is_search, true)
end
def check_thread_response_paging(thread, hash, resp_skip=0, resp_limit=nil, is_json=false)
all_responses = thread.root_comments.sort({"sk" => 1}).to_a
total_responses = all_responses.length
hash["resp_total"].should == total_responses
expected_response_slice = (resp_skip..(resp_limit.nil? ? total_responses : [total_responses, resp_skip + resp_limit].min)-1).to_a
expected_response_ids = expected_response_slice.map{|i| all_responses[i]["_id"] }
expected_response_ids.map!{|id| id.to_s } if is_json # otherwise they are BSON ObjectIds
actual_response_ids = []
hash["children"].each_with_index do |response, i|
actual_response_ids << response["id"]
response["body"].should == all_responses[expected_response_slice[i]].body
response["user_id"].should == all_responses[expected_response_slice[i]].author_id
response["username"].should == all_responses[expected_response_slice[i]].author_username
comments = Comment.where({"parent_id" => response["id"]}).sort({"sk" => 1}).to_a
expected_comment_ids = comments.map{|doc| doc["_id"] }
expected_comment_ids.map!{|id| id.to_s } if is_json # otherwise they are BSON ObjectIds
actual_comment_ids = []
response["children"].each_with_index do |comment, j|
actual_comment_ids << comment["id"]
comment["body"].should == comments[j].body
comment["user_id"].should == comments[j].author_id
comment["username"].should == comments[j].author_username
end
actual_comment_ids.should == expected_comment_ids
end
actual_response_ids.should == expected_response_ids
hash["resp_skip"].to_i.should == resp_skip
if resp_limit.nil?
hash["resp_limit"].should be_nil
else
hash["resp_limit"].to_i.should == resp_limit
end
end
def check_thread_response_paging_json(thread, hash, resp_skip=0, resp_limit=nil)
check_thread_response_paging(thread, hash, resp_skip, resp_limit, true)
end
# general purpose factory helpers
def make_thread(author, text, course_id, commentable_id)
......
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