Commit 6a09ca32 by jsa

add support for pagination of responses/comments within threads.

JIRA: FOR-374
parent 37d3d437
...@@ -18,8 +18,22 @@ get "#{APIPREFIX}/threads/:thread_id" do |thread_id| ...@@ -18,8 +18,22 @@ get "#{APIPREFIX}/threads/:thread_id" do |thread_id|
user.mark_as_read(thread) if user user.mark_as_read(thread) if user
end end
presenter = ThreadPresenter.new([thread], user || nil, thread.course_id) presenter = ThreadPresenter.factory(thread, user || nil)
presenter.to_hash_array(true).first.to_json 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 end
put "#{APIPREFIX}/threads/:thread_id" do |thread_id| put "#{APIPREFIX}/threads/:thread_id" do |thread_id|
...@@ -29,8 +43,8 @@ put "#{APIPREFIX}/threads/:thread_id" do |thread_id| ...@@ -29,8 +43,8 @@ put "#{APIPREFIX}/threads/:thread_id" do |thread_id|
if thread.errors.any? if thread.errors.any?
error 400, thread.errors.full_messages.to_json error 400, thread.errors.full_messages.to_json
else else
presenter = ThreadPresenter.new([thread], nil, thread.course_id) presenter = ThreadPresenter.factory(thread, nil)
presenter.to_hash_array.first.to_json presenter.to_hash.to_json
end end
end end
......
...@@ -42,12 +42,12 @@ get "#{APIPREFIX}/search/threads" do ...@@ -42,12 +42,12 @@ get "#{APIPREFIX}/search/threads" do
if results.length == 0 if results.length == 0
collection = [] collection = []
else else
pres_threads = ThreadSearchResultPresenter.new( pres_threads = ThreadSearchResultsPresenter.new(
results, results,
params[:user_id] ? user : nil, params[:user_id] ? user : nil,
params[:course_id] || results.first.course_id params[:course_id] || results.first.course_id
) )
collection = pres_threads.to_hash_array(bool_recursive) collection = pres_threads.to_hash
end end
num_pages = results.total_pages num_pages = results.total_pages
......
...@@ -41,8 +41,8 @@ get "#{APIPREFIX}/users/:user_id/active_threads" do |user_id| ...@@ -41,8 +41,8 @@ get "#{APIPREFIX}/users/:user_id/active_threads" do |user_id|
paged_thread_ids.index(t.id) paged_thread_ids.index(t.id)
end end
presenter = ThreadPresenter.new(paged_active_threads.to_a, user, params[:course_id]) presenter = ThreadListPresenter.new(paged_active_threads.to_a, user, params[:course_id])
collection = presenter.to_hash_array() collection = presenter.to_hash
json_output = nil json_output = nil
self.class.trace_execution_scoped(['Custom/get_user_active_threads/json_serialize']) do self.class.trace_execution_scoped(['Custom/get_user_active_threads/json_serialize']) do
......
...@@ -178,12 +178,12 @@ helpers do ...@@ -178,12 +178,12 @@ helpers do
if threads.length == 0 if threads.length == 0
collection = [] collection = []
else else
pres_threads = ThreadPresenter.new( pres_threads = ThreadListPresenter.new(
threads, threads,
params[:user_id] ? user : nil, params[:user_id] ? user : nil,
params[:course_id] || threads.first.course_id params[:course_id] || threads.first.course_id
) )
collection = pres_threads.to_hash_array(bool_recursive) collection = pres_threads.to_hash
end end
json_output = nil json_output = nil
......
...@@ -5,4 +5,6 @@ en-US: ...@@ -5,4 +5,6 @@ en-US:
value_is_required: "Value is required" value_is_required: "Value is required"
value_is_invalid: "Value is invalid" value_is_invalid: "Value is invalid"
anonymous: "anonymous" 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' require 'new_relic/agent/method_tracer'
class ThreadPresenter class ThreadPresenter
def initialize(comment_threads, user, course_id) def self.factory(thread, user)
@threads = comment_threads # use when working with one thread at a time. fetches extended /
@user = user # derived attributes from the db and explicitly initializes an instance.
@course_id = course_id course_id = thread.course_id
@read_dates = nil # Hash, sparse, thread_key (str) => date thread_key = thread._id.to_s
@unread_counts = nil # Hash, sparse, thread_key (str) => int is_read, unread_count = ThreadUtils.get_read_states([thread], user, course_id).fetch(thread_key, [false, thread.comment_count])
@endorsed_threads = nil # Hash, sparse, thread_key (str) => bool is_endorsed = ThreadUtils.get_endorsed([thread]).fetch(thread_key, false)
load_aggregates self.new thread, user, is_read, unread_count, is_endorsed
end end
def load_aggregates def initialize(thread, user, is_read, unread_count, is_endorsed)
@read_dates = {} # generally not intended for direct use. instantiated by self.factory or
if @user # by thread list presenters.
read_state = @user.read_states.where(:course_id => @course_id).first @thread = thread
if read_state @user = user
@read_dates = read_state["last_read_times"].to_hash @is_read = is_read
end @unread_count = unread_count
end @is_endorsed = is_endorsed
@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
end end
def to_hash thread, with_comments=false def to_hash with_responses=false, resp_skip=0, resp_limit=nil
thread_key = thread._id.to_s raise ArgumentError unless resp_skip >= 0
h = thread.to_hash raise ArgumentError unless resp_limit.nil? or resp_limit >= 1
if @user h = @thread.to_hash
cnt_unread = @unread_counts.fetch(thread_key, thread.comment_count) h["read"] = @is_read
h["unread_comments_count"] = cnt_unread h["unread_comments_count"] = @unread_count
h["read"] = @read_dates.has_key?(thread_key) && @read_dates[thread_key] >= thread.updated_at h["endorsed"] = @is_endorsed || false
else if with_responses
h["unread_comments_count"] = thread.comment_count unless resp_skip == 0 && resp_limit.nil?
h["read"] = false # 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 end
h["endorsed"] = @endorsed_threads.fetch(thread_key, false)
h = merge_comments_recursive(h) if with_comments
h h
end end
def to_hash_array with_comments=false def merge_comments_recursive thread_hash, comments
@threads.map {|t| to_hash(t, with_comments)}
end
def merge_comments_recursive thread_hash
thread_id = thread_hash["id"] thread_id = thread_hash["id"]
root = thread_hash = thread_hash.merge("children" => []) 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] ancestry = [thread_hash]
# weave the fetched comments into a single hierarchical doc # weave the fetched comments into a single hierarchical doc
rs.each do | comment | comments.each do | comment |
thread_hash = comment.to_hash.merge("children" => []) thread_hash = comment.to_hash.merge("children" => [])
parent_id = comment.parent_id || thread_id parent_id = comment.parent_id || thread_id
found_parent = false found_parent = false
...@@ -94,13 +82,11 @@ class ThreadPresenter ...@@ -94,13 +82,11 @@ class ThreadPresenter
ancestry = [root] ancestry = [root]
end end
end end
ancestry.first ancestry.first
end end
include ::NewRelic::Agent::MethodTracer include ::NewRelic::Agent::MethodTracer
add_method_tracer :load_aggregates
add_method_tracer :to_hash add_method_tracer :to_hash
add_method_tracer :to_hash_array
add_method_tracer :merge_comments_recursive add_method_tracer :merge_comments_recursive
end 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 alias :super_to_hash :to_hash
...@@ -12,12 +12,13 @@ class ThreadSearchResultPresenter < ThreadPresenter ...@@ -12,12 +12,13 @@ class ThreadSearchResultPresenter < ThreadPresenter
super(threads, user, course_id) super(threads, user, course_id)
end end
def to_hash(thread, with_comments=false) def to_hash
thread_hash = super_to_hash(thread, with_comments) super_to_hash.each do |thread_hash|
highlight = @search_result_map[thread.id.to_s].highlight || {} thread_key = thread_hash['id'].to_s
thread_hash["highlighted_body"] = (highlight[:body] || []).first || thread_hash["body"] highlight = @search_result_map[thread_key].highlight || {}
thread_hash["highlighted_title"] = (highlight[:title] || []).first || thread_hash["title"] thread_hash["highlighted_body"] = (highlight[:body] || []).first || thread_hash["body"]
thread_hash thread_hash["highlighted_title"] = (highlight[:title] || []).first || thread_hash["title"]
end
end 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' ...@@ -4,6 +4,7 @@ require 'unicode_shared_examples'
describe "app" do describe "app" do
describe "comment threads" do describe "comment threads" do
describe "GET /api/v1/threads" do describe "GET /api/v1/threads" do
before(:each) { setup_10_threads } before(:each) { setup_10_threads }
...@@ -23,7 +24,7 @@ describe "app" do ...@@ -23,7 +24,7 @@ describe "app" do
rs = thread_result course_id: "abc", sort_order: "asc" rs = thread_result course_id: "abc", sort_order: "asc"
rs.length.should == 2 rs.length.should == 2
rs.each_with_index { |res, i| 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" res["course_id"].should == "abc"
} }
end end
...@@ -42,8 +43,8 @@ describe "app" do ...@@ -42,8 +43,8 @@ describe "app" do
@threads["t4"].save! @threads["t4"].save!
rs = thread_result course_id: "course1", commentable_ids: "commentable1,commentable3" rs = thread_result course_id: "course1", commentable_ids: "commentable1,commentable3"
rs.length.should == 2 rs.length.should == 2
check_thread_result(nil, @threads["t3"], rs[0]) check_thread_result_json(nil, @threads["t3"], rs[0])
check_thread_result(nil, @threads["t1"], rs[1]) check_thread_result_json(nil, @threads["t1"], rs[1])
end end
it "returns only threads where course id and group id match" do it "returns only threads where course id and group id match" do
@threads["t1"].course_id = "omg" @threads["t1"].course_id = "omg"
...@@ -54,7 +55,7 @@ describe "app" do ...@@ -54,7 +55,7 @@ describe "app" do
@threads["t2"].save! @threads["t2"].save!
rs = thread_result course_id: "omg", group_id: 100 rs = thread_result course_id: "omg", group_id: 100
rs.length.should == 1 rs.length.should == 1
check_thread_result(nil, @threads["t1"], rs.first) check_thread_result_json(nil, @threads["t1"], rs.first)
end end
it "returns only threads where course id and group id match or group id is nil" do it "returns only threads where course id and group id match or group id is nil" do
@threads["t1"].course_id = "omg" @threads["t1"].course_id = "omg"
...@@ -67,7 +68,7 @@ describe "app" do ...@@ -67,7 +68,7 @@ describe "app" do
rs = thread_result course_id: "omg", group_id: 100, sort_order: "asc" rs = thread_result course_id: "omg", group_id: 100, sort_order: "asc"
rs.length.should == 2 rs.length.should == 2
rs.each_with_index { |res, i| 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" res["course_id"].should == "omg"
} }
end end
...@@ -87,14 +88,14 @@ describe "app" do ...@@ -87,14 +88,14 @@ describe "app" do
@threads["t1"].save! @threads["t1"].save!
rs = thread_result course_id: DFLT_COURSE_ID, flagged: true rs = thread_result course_id: DFLT_COURSE_ID, flagged: true
rs.length.should == 1 rs.length.should == 1
check_thread_result(nil, @threads["t1"], rs.first) check_thread_result_json(nil, @threads["t1"], rs.first)
end end
it "returns threads that have flagged comments" do it "returns threads that have flagged comments" do
@comments["t2 c3"].abuse_flaggers = [1] @comments["t2 c3"].abuse_flaggers = [1]
@comments["t2 c3"].save! @comments["t2 c3"].save!
rs = thread_result course_id: DFLT_COURSE_ID, flagged: true rs = thread_result course_id: DFLT_COURSE_ID, flagged: true
rs.length.should == 1 rs.length.should == 1
check_thread_result(nil, @threads["t2"], rs.first) check_thread_result_json(nil, @threads["t2"], rs.first)
end end
it "returns an empty result when no posts were flagged" do it "returns an empty result when no posts were flagged" do
rs = thread_result course_id: DFLT_COURSE_ID, flagged: true rs = thread_result course_id: DFLT_COURSE_ID, flagged: true
...@@ -110,7 +111,7 @@ describe "app" do ...@@ -110,7 +111,7 @@ describe "app" do
rs = thread_result course_id: "abc", user_id: "123", sort_order: "asc" rs = thread_result course_id: "abc", user_id: "123", sort_order: "asc"
rs.length.should == 2 rs.length.should == 2
rs.each_with_index { |result, i| 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["course_id"].should == "abc"
result["unread_comments_count"].should == 5 result["unread_comments_count"].should == 5
result["read"].should == false result["read"].should == false
...@@ -120,7 +121,7 @@ describe "app" do ...@@ -120,7 +121,7 @@ describe "app" do
rs = thread_result course_id: "abc", user_id: "123", sort_order: "asc" rs = thread_result course_id: "abc", user_id: "123", sort_order: "asc"
rs.length.should == 2 rs.length.should == 2
rs.each_with_index { |result, i| 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]["read"].should == true
rs[0]["unread_comments_count"].should == 0 rs[0]["unread_comments_count"].should == 0
...@@ -132,7 +133,7 @@ describe "app" do ...@@ -132,7 +133,7 @@ describe "app" do
rs = thread_result course_id: "abc", user_id: "123", sort_order: "asc" rs = thread_result course_id: "abc", user_id: "123", sort_order: "asc"
rs.length.should == 2 rs.length.should == 2
rs.each_with_index { |result, i| 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]["read"].should == false # no unread comments, but the thread itself was updated
rs[0]["unread_comments_count"].should == 0 rs[0]["unread_comments_count"].should == 0
...@@ -307,8 +308,8 @@ describe "app" do ...@@ -307,8 +308,8 @@ describe "app" do
course_id = "unicode_course" course_id = "unicode_course"
thread = make_thread(User.first, text, course_id, "unicode_commentable") thread = make_thread(User.first, text, course_id, "unicode_commentable")
make_comment(User.first, thread, text) make_comment(User.first, thread, text)
result = thread_result(course_id: course_id, recursive: true).first result = thread_result(course_id: course_id).first
check_thread_result(nil, thread, result, true) check_thread_result_json(nil, thread, result)
end end
include_examples "unicode data" include_examples "unicode data"
...@@ -330,7 +331,7 @@ describe "app" do ...@@ -330,7 +331,7 @@ describe "app" do
get "/api/v1/threads/#{thread.id}" get "/api/v1/threads/#{thread.id}"
last_response.should be_ok last_response.should be_ok
response_thread = parse last_response.body response_thread = parse last_response.body
check_thread_result(nil, thread, response_thread) check_thread_result_json(nil, thread, response_thread)
end end
it "computes endorsed? correctly" do it "computes endorsed? correctly" do
...@@ -342,7 +343,7 @@ describe "app" do ...@@ -342,7 +343,7 @@ describe "app" do
last_response.should be_ok last_response.should be_ok
response_thread = parse last_response.body response_thread = parse last_response.body
response_thread["endorsed"].should == true response_thread["endorsed"].should == true
check_thread_result(nil, thread, response_thread) check_thread_result_json(nil, thread, response_thread)
end end
# This is a test to ensure that the username is included even if the # This is a test to ensure that the username is included even if the
...@@ -370,7 +371,8 @@ describe "app" do ...@@ -370,7 +371,8 @@ describe "app" do
thread = CommentThread.first thread = CommentThread.first
get "/api/v1/threads/#{thread.id}", recursive: true get "/api/v1/threads/#{thread.id}", recursive: true
last_response.should be_ok 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 end
it "returns 400 when the thread does not exist" do it "returns 400 when the thread does not exist" do
...@@ -388,11 +390,74 @@ describe "app" do ...@@ -388,11 +390,74 @@ describe "app" do
get "/api/v1/threads/#{thread.id}", recursive: true get "/api/v1/threads/#{thread.id}", recursive: true
last_response.should be_ok last_response.should be_ok
result = parse last_response.body 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 end
include_examples "unicode data" 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 end
describe "PUT /api/v1/threads/:thread_id" do describe "PUT /api/v1/threads/:thread_id" do
before(:each) { init_without_subscriptions } before(:each) { init_without_subscriptions }
...@@ -405,7 +470,7 @@ describe "app" do ...@@ -405,7 +470,7 @@ describe "app" do
changed_thread.body.should == "new body" changed_thread.body.should == "new body"
changed_thread.title.should == "new title" changed_thread.title.should == "new title"
changed_thread.commentable_id.should == "new_commentable_id" 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 end
it "returns 400 when the thread does not exist" do it "returns 400 when the thread does not exist" do
put "/api/v1/threads/does_not_exist", body: "new body", title: "new title" put "/api/v1/threads/does_not_exist", body: "new body", title: "new title"
......
...@@ -25,26 +25,6 @@ describe "app" do ...@@ -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"] == "can anyone help me?"}.should_not be_nil
threads.index{|c| c["body"] == "it is unsolvable"}.should_not be_nil threads.index{|c| c["body"] == "it is unsolvable"}.should_not be_nil
end 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 it "returns an empty array when the commentable object does not exist (no threads)" do
get "/api/v1/does_not_exist/threads" get "/api/v1/does_not_exist/threads"
last_response.should be_ok last_response.should be_ok
...@@ -57,11 +37,11 @@ describe "app" do ...@@ -57,11 +37,11 @@ describe "app" do
commentable_id = "unicode_commentable" commentable_id = "unicode_commentable"
thread = make_thread(User.first, text, "unicode_course", commentable_id) thread = make_thread(User.first, text, "unicode_course", commentable_id)
make_comment(User.first, thread, text) 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 last_response.should be_ok
result = parse(last_response.body)["collection"] result = parse(last_response.body)["collection"]
result.should_not be_empty result.should_not be_empty
check_thread_result(nil, thread, result.first, true) check_thread_result_json(nil, thread, result.first)
end end
include_examples "unicode data" include_examples "unicode data"
......
...@@ -18,7 +18,7 @@ describe "app" do ...@@ -18,7 +18,7 @@ describe "app" do
get "/api/v1/search/threads", text: random_string get "/api/v1/search/threads", text: random_string
last_response.should be_ok last_response.should be_ok
threads = parse(last_response.body)['collection'] 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 end
...@@ -47,7 +47,7 @@ describe "app" do ...@@ -47,7 +47,7 @@ describe "app" do
get "/api/v1/search/threads", text: random_string get "/api/v1/search/threads", text: random_string
last_response.should be_ok last_response.should be_ok
threads = parse(last_response.body)['collection'] 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 end
end end
......
...@@ -16,11 +16,11 @@ describe "app" do ...@@ -16,11 +16,11 @@ describe "app" do
# Elasticsearch does not necessarily make newly indexed content # Elasticsearch does not necessarily make newly indexed content
# available immediately, so we must explicitly refresh the index # available immediately, so we must explicitly refresh the index
CommentThread.tire.index.refresh 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 last_response.should be_ok
result = parse(last_response.body)["collection"] result = parse(last_response.body)["collection"]
result.length.should == 1 result.length.should == 1
check_thread_result(nil, thread, result.first, true, true) check_thread_result_json(nil, thread, result.first, true)
end end
include_examples "unicode data" include_examples "unicode data"
......
...@@ -69,8 +69,8 @@ describe "app" do ...@@ -69,8 +69,8 @@ describe "app" do
@comments["t3 c4"].save! @comments["t3 c4"].save!
rs = thread_result 100, course_id: "xyz" rs = thread_result 100, course_id: "xyz"
rs.length.should == 2 rs.length.should == 2
check_thread_result(@users["u100"], @threads["t3"], rs[0], false) check_thread_result_json(@users["u100"], @threads["t3"], rs[0])
check_thread_result(@users["u100"], @threads["t0"], rs[1], false) check_thread_result_json(@users["u100"], @threads["t0"], rs[1])
end end
it "does not return threads in which the user has only participated anonymously" do it "does not return threads in which the user has only participated anonymously" do
...@@ -79,7 +79,7 @@ describe "app" do ...@@ -79,7 +79,7 @@ describe "app" do
@comments["t3 c4"].save! @comments["t3 c4"].save!
rs = thread_result 100, course_id: "xyz" rs = thread_result 100, course_id: "xyz"
rs.length.should == 1 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 end
it "only returns threads from the specified course" do it "only returns threads from the specified course" do
...@@ -164,7 +164,7 @@ describe "app" do ...@@ -164,7 +164,7 @@ describe "app" do
make_comment(user, thread, text) make_comment(user, thread, text)
result = thread_result(user.id, course_id: course_id) result = thread_result(user.id, course_id: course_id)
result.length.should == 1 result.length.should == 1
check_thread_result(nil, thread, result.first) check_thread_result_json(nil, thread, result.first)
end end
include_examples "unicode data" 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' require 'spec_helper'
describe ThreadSearchResultPresenter do describe ThreadSearchResultsPresenter do
context "#to_hash" do context "#to_hash" do
before(:each) { setup_10_threads } before(:each) { setup_10_threads }
...@@ -11,7 +11,7 @@ describe ThreadSearchResultPresenter do ...@@ -11,7 +11,7 @@ describe ThreadSearchResultPresenter do
hash["highlighted_title"].should == ((search_result.highlight[:title] || []).first || hash["title"]) hash["highlighted_title"].should == ((search_result.highlight[:title] || []).first || hash["title"])
end 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} expected_order = search_results.map {|t| t.id}
actual_order = hashes.map {|h| h["id"].to_s} actual_order = hashes.map {|h| h["id"].to_s}
actual_order.should == expected_order actual_order.should == expected_order
...@@ -23,8 +23,8 @@ describe ThreadSearchResultPresenter do ...@@ -23,8 +23,8 @@ describe ThreadSearchResultPresenter do
mock_results = threads_random_order.map do |t| mock_results = threads_random_order.map do |t|
double(Tire::Results::Item, :id => t._id.to_s, :highlight => {:body => ["foo"], :title => ["bar"]}) double(Tire::Results::Item, :id => t._id.to_s, :highlight => {:body => ["foo"], :title => ["bar"]})
end end
pres = ThreadSearchResultPresenter.new(mock_results, nil, DFLT_COURSE_ID) pres = ThreadSearchResultsPresenter.new(mock_results, nil, DFLT_COURSE_ID)
check_search_results_hash_array(mock_results, pres.to_hash_array) check_search_results_hash(mock_results, pres.to_hash)
end end
it "presents search results with correct default highlights" do it "presents search results with correct default highlights" do
...@@ -32,8 +32,8 @@ describe ThreadSearchResultPresenter do ...@@ -32,8 +32,8 @@ describe ThreadSearchResultPresenter do
mock_results = threads_random_order.map do |t| mock_results = threads_random_order.map do |t|
double(Tire::Results::Item, :id => t._id.to_s, :highlight => {}) double(Tire::Results::Item, :id => t._id.to_s, :highlight => {})
end end
pres = ThreadSearchResultPresenter.new(mock_results, nil, DFLT_COURSE_ID) pres = ThreadSearchResultsPresenter.new(mock_results, nil, DFLT_COURSE_ID)
check_search_results_hash_array(mock_results, pres.to_hash_array) check_search_results_hash(mock_results, pres.to_hash)
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe ThreadPresenter do 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 } before(:each) { @cid_seq = 10 }
def stub_each_from_array(obj, ary) def stub_each_from_array(obj, ary)
...@@ -12,13 +121,20 @@ describe ThreadPresenter do ...@@ -12,13 +121,20 @@ describe ThreadPresenter do
end end
def set_comment_results(thread, ary) 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 # avoids an unrelated expecation error
thread.stub(:endorsed?).and_return(true) thread.stub(:endorsed?).and_return(true)
rs = stub_each_from_array(double("rs"), ary) rs = stub_each_from_array(double("rs"), ary)
criteria = double("criteria") criteria = double("criteria")
criteria.stub(:order_by).and_return(rs) criteria.stub(:order_by).and_return(rs)
# stub Content, not Comment, because that's the model we will be querying against # 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 end
def make_comment(parent=nil) def make_comment(parent=nil)
...@@ -30,17 +146,13 @@ describe ThreadPresenter do ...@@ -30,17 +146,13 @@ describe ThreadPresenter do
end end
it "nests comments in the correct order" do it "nests comments in the correct order" do
thread = CommentThread.new
thread.id = 1
thread.author = User.new
c0 = make_comment() c0 = make_comment()
c00 = make_comment(c0) c00 = make_comment(c0)
c01 = make_comment(c0) c01 = make_comment(c0)
c010 = make_comment(c01) 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"].size.should == 1 # c0
h["children"][0]["id"].should == c0.id h["children"][0]["id"].should == c0.id
h["children"][0]["children"].size.should == 2 # c00, c01 h["children"][0]["children"].size.should == 2 # c00, c01
...@@ -51,10 +163,6 @@ describe ThreadPresenter do ...@@ -51,10 +163,6 @@ describe ThreadPresenter do
end end
it "handles orphaned child comments gracefully" do it "handles orphaned child comments gracefully" do
thread = CommentThread.new
thread.id = 33
thread.author = User.new
c0 = make_comment() c0 = make_comment()
c00 = make_comment(c0) c00 = make_comment(c0)
c000 = make_comment(c00) c000 = make_comment(c00)
...@@ -64,9 +172,9 @@ describe ThreadPresenter do ...@@ -64,9 +172,9 @@ describe ThreadPresenter do
c111 = make_comment(c11) c111 = make_comment(c11)
# lose c0 and c11 from result set. their descendants should # lose c0 and c11 from result set. their descendants should
# be silently skipped over. # 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"].size.should == 1 # c1
h["children"][0]["id"].should == c1.id h["children"][0]["id"].should == c1.id
h["children"][0]["children"].size.should == 1 # c10 h["children"][0]["children"].size.should == 1 # c10
......
...@@ -177,7 +177,7 @@ end ...@@ -177,7 +177,7 @@ end
# this method is used to test results produced using the helper function handle_threads_query # this method is used to test results produced using the helper function handle_threads_query
# which is used in multiple areas of the API # 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(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(anonymous anonymous_to_peers at_position_list closed user_id)
expected_keys += %w(username votes abuse_flaggers tags type group_id pinned) 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 ...@@ -185,53 +185,44 @@ def check_thread_result(user, thread, json_response, check_comments=false, is_se
if is_search if is_search
expected_keys += %w(highlighted_body highlighted_title) expected_keys += %w(highlighted_body highlighted_title)
end end
# the "children" key is not always present - depends on the invocation + test use case. # these keys are checked separately, when desired, using check_thread_response_paging.
# exclude it from this check - if check_comments is set, we'll assert against it later actual_keys = hash.keys - ["children", "resp_skip", "resp_limit", "resp_total"]
actual_keys = json_response.keys - ["children"]
actual_keys.sort.should == expected_keys.sort actual_keys.sort.should == expected_keys.sort
json_response["title"].should == thread.title hash["title"].should == thread.title
json_response["body"].should == thread.body hash["body"].should == thread.body
json_response["course_id"].should == thread.course_id hash["course_id"].should == thread.course_id
json_response["anonymous"].should == thread.anonymous hash["anonymous"].should == thread.anonymous
json_response["anonymous_to_peers"].should == thread.anonymous_to_peers hash["anonymous_to_peers"].should == thread.anonymous_to_peers
json_response["commentable_id"].should == thread.commentable_id hash["commentable_id"].should == thread.commentable_id
json_response["created_at"].should == thread.created_at.utc.strftime("%Y-%m-%dT%H:%M:%SZ") hash["at_position_list"].should == thread.at_position_list
json_response["updated_at"].should == thread.updated_at.utc.strftime("%Y-%m-%dT%H:%M:%SZ") hash["closed"].should == thread.closed
json_response["at_position_list"].should == thread.at_position_list hash["user_id"].should == thread.author.id
json_response["closed"].should == thread.closed hash["username"].should == thread.author.username
json_response["id"].should == thread._id.to_s hash["votes"]["point"].should == thread.votes["point"]
json_response["user_id"].should == thread.author.id hash["votes"]["count"].should == thread.votes["count"]
json_response["username"].should == thread.author.username hash["votes"]["up_count"].should == thread.votes["up_count"]
json_response["votes"]["point"].should == thread.votes["point"] hash["votes"]["down_count"].should == thread.votes["down_count"]
json_response["votes"]["count"].should == thread.votes["count"] hash["abuse_flaggers"].should == thread.abuse_flaggers
json_response["votes"]["up_count"].should == thread.votes["up_count"] hash["tags"].should == []
json_response["votes"]["down_count"].should == thread.votes["down_count"] hash["type"].should == "thread"
json_response["abuse_flaggers"].should == thread.abuse_flaggers hash["group_id"].should == thread.group_id
json_response["tags"].should == [] hash["pinned"].should == thread.pinned?
json_response["type"].should == "thread" hash["endorsed"].should == thread.endorsed?
json_response["group_id"].should == thread.group_id hash["comments_count"].should == thread.comments.length
json_response["pinned"].should == thread.pinned?
json_response["endorsed"].should == thread.endorsed? if is_json
if check_comments hash["id"].should == thread._id.to_s
# warning - this only checks top-level comments and may not handle all possible sorting scenarios hash["created_at"].should == thread.created_at.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
# proper composition / ordering of the children is currently covered in models/comment_thread_spec. hash["updated_at"].should == thread.updated_at.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
# it also does not check for author-only results (e.g. user active threads view) else
# author-only is covered by a test in api/user_spec. hash["created_at"].should == thread.created_at
root_comments = thread.root_comments.sort(_id:1).to_a hash["updated_at"].should == thread.updated_at
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
}
end end
json_response["comments_count"].should == thread.comments.length
if user.nil? if user.nil?
json_response["unread_comments_count"].should == thread.comments.length hash["unread_comments_count"].should == thread.comments.length
json_response["read"].should == false hash["read"].should == false
else else
expected_unread_cnt = thread.comments.length # initially assume nothing has been read 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 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 ...@@ -243,15 +234,56 @@ def check_thread_result(user, thread, json_response, check_comments=false, is_se
expected_unread_cnt -= 1 expected_unread_cnt -= 1
end end
end end
json_response["read"].should == (read_date >= thread.updated_at) hash["read"].should == (read_date >= thread.updated_at)
else else
json_response["read"].should == false hash["read"].should == false
end end
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
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 # general purpose factory helpers
def make_thread(author, text, course_id, commentable_id) 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