Add/update thread filters (flagged, unread, unanswered).

This commit is contained in:
jsa 2014-07-30 01:09:16 -04:00
Родитель 3021d7163b
Коммит 3f21215e2f
11 изменённых файлов: 409 добавлений и 220 удалений

Просмотреть файл

@ -1,13 +1,29 @@
get "#{APIPREFIX}/threads" do # retrieve threads by course
threads = Content.where({"_type" => "CommentThread", "course_id" => params["course_id"]})
if params[:commentable_ids]
threads = threads.in({"commentable_id" => params[:commentable_ids].split(",")})
end
#if a group id is sent, then process the set of threads with that group id or with no group id
threads = Content.where(_type:"CommentThread", course_id: params["course_id"])
if params["group_id"]
threads = threads.any_of(
{:group_id => params[:group_id].to_i},
{:group_id.exists => false},
{"group_id" => params[:group_id].to_i},
{"group_id" => {"$exists" => false}},
)
end
handle_threads_query(threads)
handle_threads_query(
threads,
params["user_id"],
params["course_id"],
value_to_boolean(params["flagged"]),
value_to_boolean(params["unread"]),
value_to_boolean(params["unanswered"]),
params["sort_key"],
params["sort_order"],
params["page"],
params["per_page"]
).to_json
end
get "#{APIPREFIX}/threads/:thread_id" do |thread_id|

Просмотреть файл

@ -4,14 +4,26 @@ delete "#{APIPREFIX}/:commentable_id/threads" do |commentable_id|
end
get "#{APIPREFIX}/:commentable_id/threads" do |commentable_id|
threads = Content.where(_type:"CommentThread", commentable_id: commentable_id)
threads = Content.where({"_type" => "CommentThread", "commentable_id" => commentable_id})
if params["group_id"]
threads = threads.any_of(
{:group_id => params[:group_id].to_i},
{:group_id.exists => false},
{"group_id" => params[:group_id].to_i},
{"group_id" => {"$exists" => false}},
)
end
handle_threads_query(threads)
handle_threads_query(
threads,
params["user_id"],
params["course_id"],
value_to_boolean(params["flagged"]),
value_to_boolean(params["unread"]),
value_to_boolean(params["unanswered"]),
params["sort_key"],
params["sort_order"],
params["page"],
params["per_page"]
).to_json
end
post "#{APIPREFIX}/:commentable_id/threads" do |commentable_id|

Просмотреть файл

@ -3,7 +3,18 @@ get "#{APIPREFIX}/users/:user_id/notifications" do |user_id|
end
get "#{APIPREFIX}/users/:user_id/subscribed_threads" do |user_id|
handle_threads_query(user.subscribed_threads)
handle_threads_query(
user.subscribed_threads.where({"course_id" => params[:course_id]}),
params["user_id"],
params["course_id"],
value_to_boolean(params["flagged"]),
value_to_boolean(params["unread"]),
value_to_boolean(params["unanswered"]),
params["sort_key"],
params["sort_order"],
params["page"],
params["per_page"]
).to_json
end
post "#{APIPREFIX}/users/:user_id/subscriptions" do |user_id|

Просмотреть файл

@ -2,15 +2,10 @@ require 'new_relic/agent/method_tracer'
get "#{APIPREFIX}/search/threads" do
local_params = params # Necessary for params to be available inside blocks
sort_criteria = get_sort_criteria(local_params)
search_text = local_params["text"]
if !search_text || !sort_criteria
if !search_text
{}.to_json
else
page = (local_params["page"] || DEFAULT_PAGE).to_i
per_page = (local_params["per_page"] || DEFAULT_PER_PAGE).to_i
# Because threads and comments are currently separate unrelated documents in
# Elasticsearch, we must first query for all matching documents, then
# extract the set of thread ids, and then sort the threads by the specified
@ -72,40 +67,26 @@ get "#{APIPREFIX}/search/threads" do
corrected_text = nil if thread_ids.empty?
end
results = nil
self.class.trace_execution_scoped(["Custom/get_search_threads/mongo_sort_page"]) do
results = CommentThread.
where(:id.in => thread_ids.to_a).
order_by(sort_criteria).
page(page).
per(per_page).
to_a
result_obj = handle_threads_query(
CommentThread.in({"_id" => thread_ids.to_a}),
local_params["user_id"],
local_params["course_id"],
value_to_boolean(local_params["flagged"]),
value_to_boolean(local_params["unread"]),
value_to_boolean(local_params["unanswered"]),
local_params["sort_key"],
local_params["sort_order"],
local_params["page"],
local_params["per_page"]
)
if !result_obj.empty?
result_obj[:corrected_text] = corrected_text
# NOTE this reflects the total results from ES, but does not consider
# any post-filtering that might happen (e.g. unread, flagged...) before
# results are shown to the user.
result_obj[:total_results] = thread_ids.size
end
total_results = thread_ids.size
num_pages = (total_results + per_page - 1) / per_page
if results.length == 0
collection = []
else
pres_threads = ThreadListPresenter.new(
results,
local_params[:user_id] ? user : nil,
local_params[:course_id] || results.first.course_id
)
collection = pres_threads.to_hash
end
json_output = nil
self.class.trace_execution_scoped(['Custom/get_search_threads/json_serialize']) do
json_output = {
collection: collection,
corrected_text: corrected_text,
total_results: total_results,
num_pages: num_pages,
page: page,
}.to_json
end
json_output
result_obj.to_json
end
end

Просмотреть файл

@ -3,3 +3,4 @@ api_key: <%= ENV['API_KEY'] || 'PUT_YOUR_API_KEY_HERE' %>
elasticsearch_server: <%= ENV['SEARCH_SERVER'] || 'http://localhost:9200' %>
max_deep_search_comment_count: 5000
default_locale: <%= ENV['SERVICE_LANGUAGE'] || 'en-US' %>
manual_pagination_batch_size: <%= ENV['MANUAL_PAGINATION_BATCH_SIZE'] || 500 %>

Просмотреть файл

@ -116,72 +116,101 @@ helpers do
end
def handle_threads_query(comment_threads)
def handle_threads_query(comment_threads, user_id, course_id, filter_flagged, filter_unread, filter_unanswered, sort_key, sort_order, page, per_page)
if params[:course_id]
comment_threads = comment_threads.where(:course_id=>params[:course_id])
if params[:flagged]
self.class.trace_execution_scoped(['Custom/handle_threads_query/find_flagged']) do
#get flagged threads and threads containing flagged responses
comment_ids = Comment.where(:course_id=>params[:course_id]).
where(:abuse_flaggers.ne => [],:abuse_flaggers.exists => true).
collect{|c| c.comment_thread_id}.uniq
thread_ids = comment_threads.where(:abuse_flaggers.ne => [],:abuse_flaggers.exists => true).
collect{|c| c.id}
comment_ids += thread_ids
if filter_flagged
self.class.trace_execution_scoped(['Custom/handle_threads_query/find_flagged']) do
# TODO replace with aggregate query?
comment_ids = Comment.where(:course_id => course_id).
where(:abuse_flaggers.ne => [], :abuse_flaggers.exists => true).
collect{|c| c.comment_thread_id}.uniq
comment_threads = comment_threads.where(:id.in => comment_ids)
end
thread_ids = comment_threads.where(:abuse_flaggers.ne => [], :abuse_flaggers.exists => true).
collect{|c| c.id}
comment_threads = comment_threads.in({"_id" => (comment_ids + thread_ids).uniq})
end
end
if params[:commentable_ids]
comment_threads = comment_threads.in(commentable_id: params[:commentable_ids].split(","))
if filter_unanswered
self.class.trace_execution_scoped(['Custom/handle_threads_query/find_unanswered']) do
endorsed_thread_ids = Comment.where(:course_id => course_id).
where(:parent_id.exists => false, :endorsed => true).
collect{|c| c.comment_thread_id}.uniq
comment_threads = comment_threads.where({"thread_type" => :question}).nin({"_id" => endorsed_thread_ids})
end
end
sort_criteria = get_sort_criteria(params)
sort_criteria = get_sort_criteria(sort_key, sort_order)
if not sort_criteria
{}.to_json
{}
else
page = (params["page"] || DEFAULT_PAGE).to_i
per_page = (params["per_page"] || DEFAULT_PER_PAGE).to_i
request_user = user_id ? user : nil
page = (page || DEFAULT_PAGE).to_i
per_page = (per_page || DEFAULT_PER_PAGE).to_i
comment_threads = comment_threads.order_by(sort_criteria)
num_pages = [1, (comment_threads.count / per_page.to_f).ceil].max
page = [num_pages, [1, page].max].min
# actual query happens here (by doing to_a)
threads = comment_threads.page(page).per(per_page).to_a
if request_user and filter_unread
# Filter and paginate based on user read state. Requires joining a subdocument of the
# user object with documents in the contents collection, which has to be done in memory.
read_dates = {}
read_state = request_user.read_states.where(:course_id => course_id).first
if read_state
read_dates = read_state["last_read_times"].to_hash
end
threads = []
skipped = 0
to_skip = (page - 1) * per_page
has_more = false
# batch_size is used to cap the number of documents we might load into memory at any given time
# TODO: starting with Mongoid 3.1, you can just do comment_threads.batch_size(size).each()
comment_threads.query.batch_size(CommentService.config["manual_pagination_batch_size"].to_i)
Mongoid.unit_of_work(disable: :current) do # this is to prevent Mongoid from memoizing every document we look at
comment_threads.each do |thread|
thread_key = thread._id.to_s
if !read_dates.has_key?(thread_key) || read_dates[thread_key] < thread.last_activity_at
if skipped >= to_skip
if threads.length == per_page
has_more = true
break
end
threads << thread
else
skipped += 1
end
end
end
end
# The following trick makes frontend pagers work without recalculating
# the number of all unread threads per user on every request (since the number
# of threads in a course could be tens or hundreds of thousands). It has the
# effect of showing that there's always just one more page of results, until
# there definitely are no more pages. This is really only acceptable for pagers
# that don't actually reveal the total number of pages to the user onscreen.
num_pages = has_more ? page + 1 : page
else
# let the installed paginator library handle pagination
num_pages = [1, (comment_threads.count / per_page.to_f).ceil].max
page = [1, page].max
threads = comment_threads.page(page).per(per_page).to_a
end
if threads.length == 0
collection = []
else
pres_threads = ThreadListPresenter.new(
threads,
params[:user_id] ? user : nil,
params[:course_id] || threads.first.course_id
)
pres_threads = ThreadListPresenter.new(threads, request_user, course_id)
collection = pres_threads.to_hash
end
json_output = nil
self.class.trace_execution_scoped(['Custom/handle_threads_query/json_serialize']) do
json_output = {
collection: collection,
num_pages: num_pages,
page: page,
}.to_json
end
json_output
{collection: collection, num_pages: num_pages, page: page}
end
end
# Given query params, return sort criteria appropriate for passing to the
# order_by function of a Mongoid query. Returns nil if params are not valid.
def get_sort_criteria(params)
def get_sort_criteria(sort_key, sort_order)
sort_key_mapper = {
"date" => :created_at,
"activity" => :last_activity_at,

Просмотреть файл

@ -31,7 +31,7 @@ class User
end
def subscribed_threads
CommentThread.where(:id.in => subscribed_thread_ids)
CommentThread.in({"_id" => subscribed_thread_ids})
end
def to_hash(params={})

Просмотреть файл

@ -103,6 +103,59 @@ describe "app" do
rs.length.should == 0
end
end
it "filters unread posts" do
user = create_test_user(Random.new)
rs = thread_result course_id: DFLT_COURSE_ID, user_id: user.id
rs.length.should == 10
rs2 = thread_result course_id: DFLT_COURSE_ID, user_id: user.id, unread: true
rs2.should == rs
user.mark_as_read(@threads[rs.first["title"]])
rs3 = thread_result course_id: DFLT_COURSE_ID, user_id: user.id, unread: true
rs3.should == rs[1..9]
rs[1..8].each { |r| user.mark_as_read(@threads[r["title"]]) }
rs4 = thread_result course_id: DFLT_COURSE_ID, user_id: user.id, unread: true
rs4.should == rs[9, 1]
user.mark_as_read(@threads[rs.last["title"]])
rs5 = thread_result course_id: DFLT_COURSE_ID, user_id: user.id, unread: true
rs5.should == []
make_comment(create_test_user(Random.new), @threads[rs.first["title"]], "new activity")
rs6 = thread_result course_id: DFLT_COURSE_ID, user_id: user.id, unread: true
rs6.length.should == 1
rs6.first["title"].should == rs.first["title"]
end
it "filters unanswered questions" do
%w[t9 t7 t5 t3 t1].each do |thread_key|
@threads[thread_key].thread_type = :question
@threads[thread_key].save!
end
rs = thread_result course_id: DFLT_COURSE_ID, unanswered: true
rs.length.should == 5
@comments["t1 c0"].endorsed = true
@comments["t1 c0"].save!
rs2 = thread_result course_id: DFLT_COURSE_ID, unanswered: true
rs2.length.should == 4
%w[t9 t7 t5].each do |thread_key|
comment = @threads[thread_key].comments.first
comment.endorsed = true
comment.save!
end
rs3 = thread_result course_id: DFLT_COURSE_ID, unanswered: true
rs3.length.should == 1
@comments["t3 c0"].endorsed = true
@comments["t3 c0"].save!
rs3 = thread_result course_id: DFLT_COURSE_ID, unanswered: true
rs3.length.should == 0
end
it "ignores endorsed comments that are not question responses" do
thread = @threads["t0"]
thread.thread_type = :question
thread.save!
comment = make_comment(create_test_user(Random.new), thread.comments.first, "comment on a response")
comment.endorsed = true
comment.save!
rs = thread_result course_id: DFLT_COURSE_ID, unanswered: true
rs.length.should == 1
end
it "correctly considers read state" do
user = create_test_user(123)
[@threads["t1"], @threads["t2"]].each do |t|
@ -259,8 +312,8 @@ describe "app" do
end
context "pagination" do
def thread_result_page (sort_key, sort_order, page, per_page)
get "/api/v1/threads", course_id: DFLT_COURSE_ID, sort_key: sort_key, sort_order: sort_order, page: page, per_page: per_page
def thread_result_page (sort_key, sort_order, page, per_page, user_id=nil, unread=false)
get "/api/v1/threads", course_id: DFLT_COURSE_ID, sort_key: sort_key, sort_order: sort_order, page: page, per_page: per_page, user_id: user_id, unread: unread
last_response.should be_ok
parse(last_response.body)
end
@ -282,23 +335,59 @@ describe "app" do
result["num_pages"].should == 2
result["page"].should == 2
end
def test_paged_order (sort_spec, expected_order, filter_spec=[], user_id=nil)
# sort spec is a hash with keys: sort_key, sort_dir, per_page
# filter spec is an array of filters to set, e.g. "unread", "flagged"
# expected order is an array of the expected titles of returned threads, in the expected order
actual_order = []
per_page = sort_spec['per_page']
num_pages = (expected_order.length + per_page - 1) / per_page
num_pages.times do |i|
page = i + 1
result = thread_result_page(
sort_spec['sort_key'],
sort_spec['sort_dir'],
page,
per_page,
user_id,
filter_spec.include?("unread")
)
result["collection"].length.should == (page * per_page <= expected_order.length ? per_page : expected_order.length % per_page)
if filter_spec.include?("unread")
# because of the way we handle num_pages for the unread filter, this is a special case.
result["num_pages"].should == (page == num_pages ? page : page + 1)
else
result["num_pages"].should == num_pages
end
result["page"].should == page
actual_order += result["collection"].map {|v| v["title"]}
end
actual_order.should == expected_order
end
it "orders correctly across pages" do
make_comment(@threads["t5"].author, @threads["t5"], "extra comment")
@threads["t7"].pinned = true
@threads["t7"].save!
expected_order = move_to_front(move_to_end(@default_order, "t5"), "t7")
actual_order = []
per_page = 3
num_pages = (@threads.length + per_page - 1) / per_page
num_pages.times do |i|
page = i + 1
result = thread_result_page("comments", "asc", page, per_page)
result["collection"].length.should == (page * per_page <= @threads.length ? per_page : @threads.length % per_page)
result["num_pages"].should == num_pages
result["page"].should == page
actual_order += result["collection"].map {|v| v["title"]}
end
actual_order.should == expected_order
test_paged_order({'sort_key'=>'comments', 'sort_dir'=>'asc', 'per_page'=>3}, expected_order)
end
it "orders correctly acrosss pages with unread filter" do
user = create_test_user(Random.new)
user.mark_as_read(@threads["t0"])
user.mark_as_read(@threads["t9"])
make_comment(@threads["t5"].author, @threads["t5"], "extra comment")
@threads["t7"].pinned = true
@threads["t7"].save!
expected_order = move_to_front(move_to_end(@default_order[1..8], "t5"), "t7")
test_paged_order(
{'sort_key'=>'comments', 'sort_dir'=>'asc', 'per_page'=>3},
expected_order,
["unread"],
user.id
)
end
end
end

Просмотреть файл

@ -0,0 +1,116 @@
require 'spec_helper'
describe "app" do
describe "notifications and subscriptions" do
let(:subscriber) { create_test_user(42) }
before(:each) do
set_api_key_header
setup_10_threads
%w[t9 t7 t5 t3 t1].each { |t| subscriber.subscribe(@threads[t]) }
end
describe "GET /api/v1/users/:user_id/subscribed_threads" do
def thread_result(params)
get "/api/v1/users/#{subscriber.id}/subscribed_threads", params
last_response.should be_ok
parse(last_response.body)["collection"]
end
context "when filtering flagged posts" do
it "returns threads that are flagged" do
@threads["t1"].abuse_flaggers = [1]
@threads["t1"].save!
rs = thread_result course_id: DFLT_COURSE_ID, flagged: true
rs.length.should == 1
check_thread_result_json(nil, @threads["t1"], rs.first)
end
it "returns threads that have flagged comments" do
@comments["t2 c3"].abuse_flaggers = [1] # note: not subscribed
@comments["t2 c3"].save!
@comments["t3 c3"].abuse_flaggers = [1] # subscribed
@comments["t3 c3"].save!
rs = thread_result course_id: DFLT_COURSE_ID, flagged: true
rs.length.should == 1
check_thread_result_json(nil, @threads["t3"], rs.first)
end
it "returns an empty result when no posts were flagged" do
rs = thread_result course_id: DFLT_COURSE_ID, flagged: true
rs.length.should == 0
end
end
it "filters unread posts" do
rs = thread_result course_id: DFLT_COURSE_ID
rs.length.should == 5
rs2 = thread_result course_id: DFLT_COURSE_ID, unread: true
rs2.should == rs
subscriber.mark_as_read(@threads[rs.first["title"]])
rs3 = thread_result course_id: DFLT_COURSE_ID, unread: true
rs3.should == rs[1..4]
rs[1..3].each { |r| subscriber.mark_as_read(@threads[r["title"]]) }
rs4 = thread_result course_id: DFLT_COURSE_ID, unread: true
rs4.should == rs[4, 1]
subscriber.mark_as_read(@threads[rs.last["title"]])
rs5 = thread_result course_id: DFLT_COURSE_ID, unread: true
rs5.should == []
make_comment(create_test_user(Random.new), @threads[rs.first["title"]], "new activity")
rs6 = thread_result course_id: DFLT_COURSE_ID, unread: true
rs6.length.should == 1
rs6.first["title"].should == rs.first["title"]
end
it "filters unanswered questions" do
%w[t9 t7].each do |thread_key|
@threads[thread_key].thread_type = :question
@threads[thread_key].save!
end
rs = thread_result course_id: DFLT_COURSE_ID, unanswered: true
rs.length.should == 2
@comments["t7 c0"].endorsed = true
@comments["t7 c0"].save!
rs2 = thread_result course_id: DFLT_COURSE_ID, unanswered: true
rs2.length.should == 1
@comments["t9 c0"].endorsed = true
@comments["t9 c0"].save!
rs3 = thread_result course_id: DFLT_COURSE_ID, unanswered: true
rs3.length.should == 0
end
it "ignores endorsed comments that are not question responses" do
thread = @threads["t1"]
thread.thread_type = :question
thread.save!
rs = thread_result course_id: DFLT_COURSE_ID, unanswered: true
rs.length.should == 1
comment = make_comment(create_test_user(Random.new), thread.comments.first, "comment on a response")
comment.endorsed = true
comment.save!
rs2 = thread_result course_id: DFLT_COURSE_ID, unanswered: true
rs2.length.should == 1
end
end
describe "POST /api/v1/users/:user_id/subscriptions" do
it "subscribe a comment thread" do
thread = @threads["t0"]
post "/api/v1/users/#{subscriber.external_id}/subscriptions", source_type: "thread", source_id: thread.id
last_response.should be_ok
thread.subscribers.length.should == 1
thread.subscribers[0].should == subscriber
end
end
describe "DELETE /api/v1/users/:user_id/subscriptions" do
it "unsubscribe a comment thread" do
thread = @threads["t2"]
subscriber.subscribe(thread)
thread.subscribers.length.should == 1
thread.subscribers[0].should == subscriber
delete "/api/v1/users/#{subscriber.external_id}/subscriptions", source_type: "thread", source_id: thread.id
last_response.should be_ok
thread.subscribers.length.should == 0
end
end
end
end

Просмотреть файл

@ -40,10 +40,21 @@ describe "app" do
let!(:threads) do
threads = (0..29).map do |i|
thread = make_thread(author, "text", course_id + (i % 2).to_s, "commentable" + (i % 3).to_s)
if i < 2
comment = make_comment(author, thread, "objectionable")
comment.abuse_flaggers = [1]
comment.save!
end
if i % 5 != 0
thread.group_id = i % 5
thread.save!
end
if [0, 2, 4].include? i
thread.thread_type = :question
thread.save!
comment = make_comment(author, thread, "response")
comment.save!
end
thread
end
refresh_es_index
@ -63,6 +74,42 @@ describe "app" do
assert_response_contains((0..29).find_all {|i| i % 2 == 0})
end
it "with unread filter" do
user = create_test_user(Random.new)
user.mark_as_read(threads[0])
get "/api/v1/search/threads", text: "text", course_id: "test/course/id0", user_id: user.id, unread: true
assert_response_contains((1..29).find_all {|i| i % 2 == 0})
end
it "with flagged filter" do
get "/api/v1/search/threads", text: "text", course_id: "test/course/id0", flagged: true
assert_response_contains([0])
end
it "with unanswered filter" do
get "/api/v1/search/threads", text: "text", course_id: "test/course/id0", unanswered: true
assert_response_contains([0, 2, 4])
comment = threads[2].comments.first
comment.endorsed = true
comment.save!
get "/api/v1/search/threads", text: "text", course_id: "test/course/id0", unanswered: true
assert_response_contains([0, 4])
end
it "with unanswered filter and group_id" do
get "/api/v1/search/threads", text: "text", course_id: "test/course/id0", unanswered: true
assert_response_contains([0, 2, 4])
get "/api/v1/search/threads", text: "text", course_id: "test/course/id0", unanswered: true, group_id: 2
assert_response_contains([0, 2])
get "/api/v1/search/threads", text: "text", course_id: "test/course/id0", unanswered: true, group_id: 4
assert_response_contains([0, 4])
comment = threads[2].comments.first
comment.endorsed = true
comment.save!
get "/api/v1/search/threads", text: "text", course_id: "test/course/id0", unanswered: true, group_id: 2
assert_response_contains([0])
end
it "by commentable_id" do
get "/api/v1/search/threads", text: "text", commentable_id: "commentable0"
assert_response_contains((0..29).find_all {|i| i % 3 == 0})

Просмотреть файл

@ -1,113 +0,0 @@
require 'spec_helper'
# Commenting out until notifications are used again.
#
#describe "app" do
# describe "subscriptions and notifications" do
# before(:each) { init_with_subscriptions }
# describe "GET /api/v1/users/:user_id/notifications" do
# it "get all notifications on the subscribed comment threads for the user" do
# user = User.find("1")
# get "/api/v1/users/#{user.external_id}/notifications"
# last_response.should be_ok
# notifications = parse last_response.body
# so_easy = Comment.all.select{|c| c.body == "this problem is so easy"}.first
# not_for_me_neither = Comment.all.select{|c| c.body == "not for me neither!"}.first
# notification_so_easy = notifications.select{|f| f["notification_type"] == "post_reply" and f["info"]["comment_id"] == so_easy.id.to_s}.first
# notification_so_easy.should_not be_nil
# notification_not_for_me_neither = notifications.select{|f| f["notification_type"] == "post_reply" and f["info"]["comment_id"] == not_for_me_neither.id.to_s}.first
# notification_not_for_me_neither.should_not be_nil
# end
# it "returns error if user does not exist" do #TODO may change later if have user service
# get "/api/v1/users/does_not_exist/notifications"
# last_response.status.should == 400
# end
# it "get all notifications on the subscribed commentable for the user" do
# user = User.find("1")
# get "/api/v1/users/#{user.external_id}/notifications"
# last_response.should be_ok
# notifications = parse last_response.body
# notifications.select{|f| f["notification_type"] == "post_topic"}.length.should == 1
# problem_wrong = notifications.select{|f| f["notification_type"] == "post_topic"}.first
# problem_wrong["info"]["thread_title"].should == "This problem is wrong"
# end
# it "get all notifications on the followed user for the user" do
# user = User.find("2")
# get "/api/v1/users/#{user.external_id}/notifications"
# last_response.should be_ok
# notifications = parse last_response.body
# notifications.select{|f| f["info"]["thread_title"] =~ /what to say/}.first.should_not be_nil
# end
# end
# describe "POST /api/v1/users/:user_id/subscriptions" do
# it "follow user" do
# user1 = User.find("1")
# user2 = User.find("2")
# post "/api/v1/users/#{user1.external_id}/subscriptions", source_type: "user", source_id: user2.external_id
# last_response.should be_ok
# User.find("2").followers.length.should == 1
# User.find("2").followers.should include user1
# end
# it "does not follow the same user twice" do
# user1 = User.find("1")
# user2 = User.find("2")
# post "/api/v1/users/#{user1.external_id}/subscriptions", source_type: "user", source_id: user2.external_id
# post "/api/v1/users/#{user1.external_id}/subscriptions", source_type: "user", source_id: user2.external_id
# last_response.should be_ok
# User.find("2").followers.length.should == 1
# end
# it "does not follow oneself" do
# user = create_test_user(3)
# post "/api/v1/users/#{user.external_id}/subscriptions", source_type: "user", source_id: user.external_id
# last_response.status.should == 400
# user.reload.followers.length.should == 0
# end
# it "unfollow user" do
# user1 = User.find("1")
# user2 = User.find("2")
# delete "/api/v1/users/#{user2.external_id}/subscriptions", source_type: "user", source_id: user1.external_id
# last_response.should be_ok
# User.find("1").followers.length.should == 0
# end
# it "respond ok when unfollowing user twice" do
# user1 = User.find("1")
# user2 = User.find("2")
# delete "/api/v1/users/#{user2.external_id}/subscriptions", source_type: "user", source_id: user1.external_id
# delete "/api/v1/users/#{user2.external_id}/subscriptions", source_type: "user", source_id: user1.external_id
# last_response.should be_ok
# User.find("1").followers.length.should == 0
# end
# it "subscribe a commentable" do
# user3 = create_test_user(3)
# post "/api/v1/users/#{user3.external_id}/subscriptions", source_type: "other", source_id: "question_1"
# last_response.should be_ok
# Commentable.find("question_1").subscribers.length.should == 3
# Commentable.find("question_1").subscribers.should include user3
# end
# it "unsubscribe a commentable" do
# user2 = User.find_by(external_id: "2")
# delete "/api/v1/users/#{user2.external_id}/subscriptions", source_type: "other", source_id: "question_1"
# last_response.should be_ok
# Commentable.find("question_1").subscribers.length.should == 1
# Commentable.find("question_1").subscribers.should_not include user2
# end
# it "subscribe a comment thread" do
# user1 = User.find_by(external_id: "1")
# thread = CommentThread.where(body: "it is unsolvable").first
# post "/api/v1/users/#{user1.external_id}/subscriptions", source_type: "thread", source_id: thread.id
# last_response.should be_ok
# thread = CommentThread.where(body: "it is unsolvable").first
# thread.subscribers.length.should == 2
# thread.subscribers.should include user1
# end
# it "unsubscribe a comment thread" do
# user2 = User.find_by(external_id: "2")
# thread = CommentThread.where(body: "it is unsolvable").first
# delete "/api/v1/users/#{user2.external_id}/subscriptions", source_type: "thread", source_id: thread.id
# last_response.should be_ok
# thread = CommentThread.where(body: "it is unsolvable").first
# thread.subscribers.length.should == 0
# end
# end
# end
#end