diff --git a/db/migrate/20190529133507_update_replying_comments_to_version_8.rb b/db/migrate/20190529133507_update_replying_comments_to_version_8.rb new file mode 100644 index 00000000..c4d910ab --- /dev/null +++ b/db/migrate/20190529133507_update_replying_comments_to_version_8.rb @@ -0,0 +1,5 @@ +class UpdateReplyingCommentsToVersion8 < ActiveRecord::Migration[5.2] + def change + update_view :replying_comments, version: 8, revert_to_version: 7 + end +end diff --git a/db/schema.rb b/db/schema.rb index 95928b69..fdebf603 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2018_12_02_194809) do +ActiveRecord::Schema.define(version: 2019_05_29_133507) do create_table "comments", id: :bigint, unsigned: true, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t| t.datetime "created_at", null: false @@ -332,7 +332,7 @@ ActiveRecord::Schema.define(version: 2018_12_02_194809) do add_foreign_key "votes", "users", name: "votes_user_id_fk" create_view "replying_comments", sql_definition: <<-SQL - select `read_ribbons`.`user_id` AS `user_id`,`comments`.`id` AS `comment_id`,`read_ribbons`.`story_id` AS `story_id`,`comments`.`parent_comment_id` AS `parent_comment_id`,`comments`.`created_at` AS `comment_created_at`,`parent_comments`.`user_id` AS `parent_comment_author_id`,`comments`.`user_id` AS `comment_author_id`,`stories`.`user_id` AS `story_author_id`,`read_ribbons`.`updated_at` < `comments`.`created_at` AS `is_unread`,(select `votes`.`vote` from `votes` where `votes`.`user_id` = `read_ribbons`.`user_id` and `votes`.`comment_id` = `comments`.`id`) AS `current_vote_vote`,(select `votes`.`reason` from `votes` where `votes`.`user_id` = `read_ribbons`.`user_id` and `votes`.`comment_id` = `comments`.`id`) AS `current_vote_reason` from (((`read_ribbons` join `comments` on(`comments`.`story_id` = `read_ribbons`.`story_id`)) join `stories` on(`stories`.`id` = `comments`.`story_id`)) left join `comments` `parent_comments` on(`parent_comments`.`id` = `comments`.`parent_comment_id`)) where `read_ribbons`.`is_following` = 1 and `comments`.`user_id` <> `read_ribbons`.`user_id` and `comments`.`is_deleted` = 0 and `comments`.`is_moderated` = 0 and (`parent_comments`.`user_id` = `read_ribbons`.`user_id` or `parent_comments`.`user_id` is null and `stories`.`user_id` = `read_ribbons`.`user_id`) and `comments`.`upvotes` - `comments`.`downvotes` >= 0 and (`parent_comments`.`id` is null or `parent_comments`.`upvotes` - `parent_comments`.`downvotes` >= 0) and cast(`stories`.`upvotes` as signed) - cast(`stories`.`downvotes` as signed) >= 0 + select `read_ribbons`.`user_id` AS `user_id`,`comments`.`id` AS `comment_id`,`read_ribbons`.`story_id` AS `story_id`,`comments`.`parent_comment_id` AS `parent_comment_id`,`comments`.`created_at` AS `comment_created_at`,`parent_comments`.`user_id` AS `parent_comment_author_id`,`comments`.`user_id` AS `comment_author_id`,`stories`.`user_id` AS `story_author_id`,`read_ribbons`.`updated_at` < `comments`.`created_at` AS `is_unread`,(select `votes`.`vote` from `votes` where `votes`.`user_id` = `read_ribbons`.`user_id` and `votes`.`comment_id` = `comments`.`id`) AS `current_vote_vote`,(select `votes`.`reason` from `votes` where `votes`.`user_id` = `read_ribbons`.`user_id` and `votes`.`comment_id` = `comments`.`id`) AS `current_vote_reason` from (((`read_ribbons` join `comments` on(`comments`.`story_id` = `read_ribbons`.`story_id`)) join `stories` on(`stories`.`id` = `comments`.`story_id`)) left join `comments` `parent_comments` on(`parent_comments`.`id` = `comments`.`parent_comment_id`)) where `read_ribbons`.`is_following` = 1 and `comments`.`user_id` <> `read_ribbons`.`user_id` and `comments`.`is_deleted` = 0 and `comments`.`is_moderated` = 0 and (`parent_comments`.`user_id` = `read_ribbons`.`user_id` or `parent_comments`.`user_id` is null and `stories`.`user_id` = `read_ribbons`.`user_id`) and `comments`.`upvotes` - `comments`.`downvotes` >= 0 and (`parent_comments`.`id` is null or `parent_comments`.`upvotes` - `parent_comments`.`downvotes` >= 0 and `parent_comments`.`is_moderated` = 0 and `parent_comments`.`is_deleted` = 0) and !exists(select 1 from (`votes` `f` join `comments` `c` on(`f`.`comment_id` = `c`.`id`)) where `f`.`vote` < 0 and `f`.`user_id` = `parent_comments`.`user_id` and `c`.`user_id` = `comments`.`user_id` and `f`.`story_id` = `comments`.`story_id`) and cast(`stories`.`upvotes` as signed) - cast(`stories`.`downvotes` as signed) >= 0 SQL end diff --git a/db/views/replying_comments_v08.sql b/db/views/replying_comments_v08.sql new file mode 100644 index 00000000..c55c2130 --- /dev/null +++ b/db/views/replying_comments_v08.sql @@ -0,0 +1,52 @@ +SELECT + read_ribbons.user_id, + comments.id as comment_id, + read_ribbons.story_id as story_id, + comments.parent_comment_id, + comments.created_at as comment_created_at, + parent_comments.user_id as parent_comment_author_id, + comments.user_id as comment_author_id, + stories.user_id as story_author_id, + (read_ribbons.updated_at < comments.created_at) as is_unread, + (select votes.vote from votes where votes.user_id = read_ribbons.user_id and votes.comment_id = comments.id) as current_vote_vote, + (select votes.reason from votes where votes.user_id = read_ribbons.user_id and votes.comment_id = comments.id) as current_vote_reason +FROM + read_ribbons +JOIN + comments ON comments.story_id = read_ribbons.story_id +JOIN + stories ON stories.id = comments.story_id +LEFT JOIN + comments parent_comments ON parent_comments.id = comments.parent_comment_id +WHERE + read_ribbons.is_following = 1 + AND comments.user_id != read_ribbons.user_id + AND comments.is_deleted = FALSE + AND comments.is_moderated = FALSE + AND ( + parent_comments.user_id = read_ribbons.user_id + OR (parent_comments.user_id IS NULL + AND stories.user_id = read_ribbons.user_id) + ) + AND (comments.upvotes - comments.downvotes) >= 0 -- comment doesn't have negative score + AND ( + parent_comments.id IS NULL + OR ( + -- parent doesn't have negative score, isn't deleted, isn't moderated + (parent_comments.upvotes - parent_comments.downvotes) >= 0 + AND parent_comments.is_moderated = FALSE + AND parent_comments.is_deleted = FALSE + ) + ) + AND ( -- parent user has not flagged replier in the thread + NOT EXISTS ( + SELECT 1 FROM votes f JOIN comments c ON f.comment_id = c.id + WHERE + f.vote < 0 + AND f.user_id = parent_comments.user_id + AND c.user_id = comments.user_id + AND f.story_id = comments.story_id + ) + ) + AND (CAST(stories.upvotes as signed) - CAST(stories.downvotes as signed)) >= 0 +; diff --git a/spec/factories/vote.rb b/spec/factories/vote.rb index aff934e6..830fefcd 100644 --- a/spec/factories/vote.rb +++ b/spec/factories/vote.rb @@ -1,5 +1,6 @@ FactoryBot.define do factory :vote do + association(:user) vote { 1 } end end diff --git a/spec/models/replying_comment.rb b/spec/models/replying_comment.rb new file mode 100644 index 00000000..c28dfd05 --- /dev/null +++ b/spec/models/replying_comment.rb @@ -0,0 +1,111 @@ +require "rails_helper" + +RSpec::Matchers.define :have_reply do |expected| + match do |actual| + ReplyingComment.for_user(actual.user).map(&:comment).include? expected + end + + failure_message do |actual| + "expected that comment #{actual.id} would be in " \ + "#{ReplyingComment.for_user(expected.user).map(&:comment_id)}" + end +end + +describe ReplyingComment do + def followed_parent + p = create(:comment) + ReadRibbon.create(user_id: p.user_id, story_id: p.story_id, updated_at: p.created_at - 1.second) + p + end + + def flag_comment(comment, by = create(:user)) + Vote.vote_thusly_on_story_or_comment_for_user_because( + -1, comment.story_id, comment.id, by.id, 'T' + ) + end + + describe "is listed when" do + it "it's a direct reply" do + p = followed_parent + c = create(:comment, story_id: p.story_id, parent_comment: p) + + expect(p).to have_reply(c) + end + end + + describe "is not listed when" do + it "parent has a negative score" do + p = followed_parent + flag_comment(p) + flag_comment(p) + c = create(:comment, story_id: p.story_id, parent_comment: p) + + expect(p).to_not have_reply(c) + end + + it "it has a negative score" do + p = followed_parent + c = create(:comment, story_id: p.story_id, parent_comment: p) + flag_comment(c) + flag_comment(c) + + expect(p).to_not have_reply(c) + end + + it "parent is deleted" do + p = followed_parent + c = create(:comment, story_id: p.story_id, parent_comment: p) + p.delete_for_user(p.user) + + expect(p).to_not have_reply(c) + end + + it "it is deleted" do + p = followed_parent + c = create(:comment, story_id: p.story_id, parent_comment: p) + c.delete_for_user(c.user) + + expect(p).to_not have_reply(c) + end + + it "parent is moderated" do + p = followed_parent + c = create(:comment, story_id: p.story_id, parent_comment: p) + p.delete_for_user(create(:user, :admin), "obvs because I disagree with your politics") + + expect(p).to_not have_reply(c) + end + + it "it is moderated" do + p = followed_parent + c = create(:comment, story_id: p.story_id, parent_comment: p) + c.delete_for_user(create(:user, :admin), "obvs because I disagree with your politics") + + expect(p).to_not have_reply(c) + end + + it "it is on a story with a negative score" do + p = followed_parent + c = create(:comment, story_id: p.story_id, parent_comment: p) + Vote.vote_thusly_on_story_or_comment_for_user_because( + -1, p.story_id, nil, create(:user).id, 'O' + ) + Vote.vote_thusly_on_story_or_comment_for_user_because( + -1, p.story_id, nil, create(:user).id, 'O' + ) + + expect(p.story.reload.score).to be < 0 + expect(p).to_not have_reply(c) + end + + it "commenter has not flagged child commenter in the story" do + p = followed_parent + c = create(:comment, story_id: p.story_id, parent_comment: p) + + expect(p).to have_reply(c) + + flag_comment(c, p.user) + expect(p).to_not have_reply(c) + end + end +end