diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 51ac3dea5..a7d7d0fbb 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -174,7 +174,8 @@ class PostsController < ApplicationController ] permitted_params += %i[is_rating_locked] if CurrentUser.is_privileged? permitted_params += %i[is_note_locked bg_color] if CurrentUser.is_janitor? - permitted_params += %i[is_status_locked is_comment_locked locked_tags hide_from_anonymous hide_from_search_engines] if CurrentUser.is_admin? + permitted_params += %i[is_comment_locked] if CurrentUser.is_moderator? + permitted_params += %i[is_status_locked is_comment_disabled locked_tags hide_from_anonymous hide_from_search_engines] if CurrentUser.is_admin? params.require(:post).permit(permitted_params) end diff --git a/app/logical/vote_manager.rb b/app/logical/vote_manager.rb index 6e29cdb93..331f32009 100644 --- a/app/logical/vote_manager.rb +++ b/app/logical/vote_manager.rb @@ -93,6 +93,7 @@ class VoteManager raise UserVote::Error, "Invalid vote" unless [1, -1].include?(score) raise UserVote::Error, "You do not have permission to vote" unless user.is_member? raise UserVote::Error, "Comment section is locked" if comment.post.is_comment_locked? + raise UserVote::Error, "Comment section is disabled" if comment.post.is_comment_disabled? CommentVote.transaction(**ISOLATION) do CommentVote.uncached do score_modifier = score diff --git a/app/models/comment.rb b/app/models/comment.rb index 111bcd5e4..859aa119b 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -31,6 +31,10 @@ class Comment < ApplicationRecord belongs_to :warning_user, class_name: "User", optional: true has_many :votes, :class_name => "CommentVote", :dependent => :destroy + scope :deleted, -> { where(is_hidden: true) } + scope :undeleted, -> { where(is_hidden: false) } + scope :stickied, -> { where(is_sticky: true) } + module SearchMethods def recent reorder("comments.id desc").limit(RECENT_COUNT) @@ -47,21 +51,16 @@ class Comment < ApplicationRecord end def visible(user) - if user.is_moderator? - where("comments.score >= ? or comments.is_sticky = true", user.comment_threshold) - elsif user.is_janitor? - where("(comments.score >= ? or comments.is_sticky = true) and (comments.is_sticky = true or comments.is_hidden = false or comments.creator_id = ?)", user.comment_threshold, user.id) - else - where("(comments.score >= ? or comments.is_sticky = true) and (comments.is_hidden = false or comments.creator_id = ?)", user.comment_threshold, user.id) + q = where("comments.score >= ? or comments.is_sticky = true", user.comment_threshold) + unless user.is_moderator? + q = q.joins(:post).where("comments.is_sticky = true or posts.is_comment_disabled = false or comments.creator_id = ?", user.id) + if user.is_janitor? + q = q.where("comments.is_sticky = true or comments.is_hidden = false or comments.creator_id = ?", user.id) + else + q = q.where("comments.is_hidden = false or comments.creator_id = ?", user.id) + end end - end - - def deleted - where("comments.is_hidden = true") - end - - def undeleted - where("comments.is_hidden = false") + q end def post_tags_match(query) @@ -139,7 +138,11 @@ class Comment < ApplicationRecord end def post_not_comment_locked - errors.add(:base, "Post has comments locked") if !CurrentUser.is_moderator? && Post.find_by(id: post_id)&.is_comment_locked? + return if CurrentUser.is_moderator? + post = Post.find_by(id: post_id) + return if post.blank? + errors.add(:base, "Post has comments locked") if post.is_comment_locked? + errors.add(:base, "Post has comments disabled") if post.is_comment_disabled? end def update_last_commented_at_on_create @@ -179,25 +182,26 @@ class Comment < ApplicationRecord def can_reply?(user) return false if is_sticky? - return false if post.is_comment_locked? && !user.is_moderator? + return false if (post.is_comment_locked? || post.is_comment_disabled?) && !user.is_moderator? true end def editable_by?(user) return true if user.is_admin? - return false if post.is_comment_locked? && !user.is_moderator? + return false if (post.is_comment_locked? || post.is_comment_disabled?) && !user.is_moderator? return false if was_warned? creator_id == user.id end def can_hide?(user) return true if user.is_moderator? - return false if was_warned? + return false if !visible_to?(user) || was_warned? || post&.is_comment_disabled? user.id == creator_id end def visible_to?(user) return true if user.is_moderator? + return false if !is_sticky? && (post&.is_comment_disabled? && creator_id != user.id) return true if user.is_janitor? && is_sticky? return true if is_hidden? == false creator_id == user.id # Can always see your own comments, even if hidden. diff --git a/app/models/post.rb b/app/models/post.rb index aa2685c01..680a11af5 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -58,9 +58,6 @@ class Post < ApplicationRecord attr_accessor :old_tag_string, :old_parent_id, :old_source, :old_rating, :do_not_version_changes, :tag_string_diff, :source_diff, :edit_reason - # FIXME: Remove this - alias_attribute :is_comment_locked, :is_comment_disabled - has_many :versions, -> {order("post_versions.id ASC")}, :class_name => "PostVersion", :dependent => :destroy IMAGE_TYPES = %i[original large preview crop] @@ -1573,6 +1570,10 @@ class Post < ApplicationRecord action = is_comment_locked? ? :comment_locked : :comment_unlocked PostEvent.add(id, CurrentUser.user, action) end + if saved_change_to_is_comment_disabled? + action = is_comment_disabled? ? :comment_disabled : :comment_enabled + PostEvent.add(id, CurrentUser.user, action) + end if saved_change_to_bg_color? PostEvent.add(id, CurrentUser.user, :changed_bg_color, { bg_color: bg_color }) end @@ -1770,8 +1771,12 @@ class Post < ApplicationRecord !has_tag?("grandfathered_content") && created_at.after?("2015-01-01") end - def visible_comment_count(_user) - comment_count + def visible_comment_count(user) + if user.is_moderator? || !is_comment_disabled? + comment_count + else + comments.visible(user).count + end end def avoid_posting_artists diff --git a/app/models/post_event.rb b/app/models/post_event.rb index e27ab8706..9e5f3adde 100644 --- a/app/models/post_event.rb +++ b/app/models/post_event.rb @@ -19,6 +19,8 @@ class PostEvent < ApplicationRecord note_unlocked: 13, comment_locked: 18, comment_unlocked: 19, + comment_disabled: 22, + comment_enabled: 23, replacement_accepted: 14, replacement_rejected: 15, replacement_promoted: 20, @@ -26,9 +28,11 @@ class PostEvent < ApplicationRecord expunged: 17, changed_bg_color: 21, } - MOD_ONLY_ACTIONS = [ + MOD_ONLY_SEARCH_ACTIONS = [ actions[:comment_locked], actions[:comment_unlocked], + actions[:comment_disabled], + actions[:comment_enabled], ].freeze def self.add(post_id, creator, action, data = {}) @@ -47,11 +51,8 @@ class PostEvent < ApplicationRecord def self.search(params) q = super - unless CurrentUser.is_moderator? - q = q.where.not(action: MOD_ONLY_ACTIONS) - end if params[:post_id].present? - q = q.where("post_id = ?", params[:post_id].to_i) + q = q.where(post_id: params[:post_id]) end q = q.where_user(:creator_id, :creator, params) do |condition, user_ids| @@ -62,9 +63,18 @@ class PostEvent < ApplicationRecord end if params[:action].present? - q = q.where('action = ?', actions[params[:action]]) + if !CurrentUser.user.is_moderator? && MOD_ONLY_SEARCH_ACTIONS.include?(actions[params[:action]]) + raise(User::PrivilegeError) + end + q = q.where(action: actions[params[:action]]) end q.apply_basic_order(params) end + + def self.search_options_for(user) + options = actions.keys + return options if user.is_moderator? + options.reject { |action| MOD_ONLY_SEARCH_ACTIONS.any?(actions[action]) } + end end diff --git a/app/views/comments/_index_by_post.html.erb b/app/views/comments/_index_by_post.html.erb index a4d1669d1..0e3fc14b2 100644 --- a/app/views/comments/_index_by_post.html.erb +++ b/app/views/comments/_index_by_post.html.erb @@ -19,7 +19,7 @@
- <%= render "comments/partials/index/list", :post => post, :comments => post.comments.visible(CurrentUser.user).recent.reverse %> + <%= render "comments/partials/index/list", post: post, comments: post.comments.visible(CurrentUser.user).recent.reverse %>
<% end %> diff --git a/app/views/comments/partials/index/_list.html.erb b/app/views/comments/partials/index/_list.html.erb index f732257ad..794e8305b 100644 --- a/app/views/comments/partials/index/_list.html.erb +++ b/app/views/comments/partials/index/_list.html.erb @@ -1,28 +1,40 @@
+ <% if post.is_comment_disabled? %> + Comment section has been disabled. + <% end %> <% if post.is_comment_locked? %> Comment section has been locked. <% end %> -
- <% if post.comments.hidden(CurrentUser.user).count > 0 || (params[:controller] == "comments" && post.comments.count > Comment::RECENT_COUNT) %> + <% if !CurrentUser.user.is_moderator? && post.is_comment_disabled? %> + <% comments = comments.stickied %> +
+ <% if comments.any? %> + <%= render partial: "comments/partials/show/comment", collection: comments, locals: { post: post } %> + <% end %> +
+ <% else %> +
+ <% if post.comments.hidden(CurrentUser.user).count > 0 || (params[:controller] == "comments" && post.comments.count > Comment::RECENT_COUNT) %> <%= link_to "Show all comments", comments_path(:post_id => post.id), 'data-pid': post.id, class: 'show-all-comments-for-post-link' %> - <% end %> -
- -
- <% if comments.empty? %> - <% if post.last_commented_at.present? %> -

There are no visible comments.

- <% else %> -

There are no comments.

<% end %> - <% else %> - <%= render partial: "comments/partials/show/comment", collection: comments, locals: { post: post } %> - <% end %> -
+
- <% if post.is_comment_locked? && !CurrentUser.is_moderator? %> +
+ <% if comments.empty? %> + <% if post.last_commented_at.present? %> +

There are no visible comments.

+ <% else %> +

There are no comments.

+ <% end %> + <% else %> + <%= render partial: "comments/partials/show/comment", collection: comments, locals: { post: post } %> + <% end %> +
+ <% end %> + + <% if (post.is_comment_locked? || post.is_comment_disabled?) && !CurrentUser.is_moderator? %> <% elsif CurrentUser.is_member? %>
<% if !CurrentUser.is_anonymous? && !CurrentUser.user.is_janitor? %> diff --git a/app/views/post_events/_search.html.erb b/app/views/post_events/_search.html.erb index 3fc53ab07..c61b23220 100644 --- a/app/views/post_events/_search.html.erb +++ b/app/views/post_events/_search.html.erb @@ -1,5 +1,5 @@ <%= form_search path: post_events_path do |f| %> <%= f.input :post_id, label: "Post #" %> <%= f.user :creator %> - <%= f.input :action, label: "Action", collection: PostEvent.actions.keys.map {|k| [k.to_s.capitalize.tr("_"," "), k.to_s]}, include_blank: true %> + <%= f.input :action, label: "Action", collection: PostEvent.search_options_for(CurrentUser.user).map { |k| [k.to_s.capitalize.tr("_"," "), k.to_s] }, include_blank: true %> <% end %> diff --git a/app/views/posts/partials/show/_edit.html.erb b/app/views/posts/partials/show/_edit.html.erb index 3b56ff31c..45013710f 100644 --- a/app/views/posts/partials/show/_edit.html.erb +++ b/app/views/posts/partials/show/_edit.html.erb @@ -55,7 +55,9 @@ <% end %> <%= f.input :is_rating_locked, label: "Rating" %> <% if CurrentUser.is_admin? %> - <%= f.input :is_status_locked, label: "Status" %> + <%= f.input :is_status_locked, label: "Status" %> + <% end %> + <% if CurrentUser.is_moderator? %> <%= f.input :is_comment_locked, label: "Comments" %> <% end %> @@ -72,6 +74,7 @@
<%= f.input :hide_from_anonymous, as: :boolean, label: "Hide from Anon" %> <%= f.input :hide_from_search_engines, as: :boolean, label: "Hide from search engines" %> + <%= f.input :is_comment_disabled, label: "Hide comments" %>
<% end %> diff --git a/db/fixes/119_convert_comment_disabled_to_locked.rb b/db/fixes/119_convert_comment_disabled_to_locked.rb new file mode 100755 index 000000000..ead3e900e --- /dev/null +++ b/db/fixes/119_convert_comment_disabled_to_locked.rb @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "config", "environment")) + +Post.without_timeout do + Post.where(is_comment_disabled: true).update_all("is_comment_locked = is_comment_disabled, is_comment_disabled = false") +end diff --git a/db/migrate/20240905160626_add_is_comment_locked_to_posts.rb b/db/migrate/20240905160626_add_is_comment_locked_to_posts.rb new file mode 100644 index 000000000..b9523a6d0 --- /dev/null +++ b/db/migrate/20240905160626_add_is_comment_locked_to_posts.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddIsCommentLockedToPosts < ActiveRecord::Migration[7.1] + def change + add_column(:posts, :is_comment_locked, :boolean, null: false, default: false) + end +end diff --git a/db/structure.sql b/db/structure.sql index dae4db36a..2d53a1db1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1685,7 +1685,8 @@ CREATE TABLE public.posts ( bg_color character varying, generated_samples character varying[], duration numeric, - is_comment_disabled boolean DEFAULT false NOT NULL + is_comment_disabled boolean DEFAULT false NOT NULL, + is_comment_locked boolean DEFAULT false NOT NULL ); @@ -4676,6 +4677,7 @@ ALTER TABLE ONLY public.avoid_postings SET search_path TO "$user", public; INSERT INTO "schema_migrations" (version) VALUES +('20240905160626'), ('20240726170041'), ('20240709134926'), ('20240706061122'), diff --git a/test/functional/comment_votes_controller_test.rb b/test/functional/comment_votes_controller_test.rb index e799d07b1..cfbf46c55 100644 --- a/test/functional/comment_votes_controller_test.rb +++ b/test/functional/comment_votes_controller_test.rb @@ -14,10 +14,10 @@ class CommentVotesControllerTest < ActionDispatch::IntegrationTest CurrentUser.user = @user end - context "#create.json" do + context "create action" do should "create a vote" do - assert_difference(-> { CommentVote.count }, 1) do - post_auth comment_votes_path(@comment), @user, params: { score: -1, format: :json} + assert_difference("CommentVote.count", 1) do + post_auth comment_votes_path(@comment), @user, params: { score: -1, format: :json } assert_response :success end end @@ -29,6 +29,40 @@ class CommentVotesControllerTest < ActionDispatch::IntegrationTest assert_response :success end end + + should "prevent voting on comment locked posts" do + @post.update(is_comment_locked: true) + assert_no_difference("CommentVote.count") do + post_auth comment_votes_path(@comment), @user, params: { score: -1, format: :json } + assert_response 422 + end + end + + should "prevent unvoting on comment locked posts" do + @post.update(is_comment_locked: true) + create(:comment_vote, comment: @comment, user: @user, score: -1) + assert_no_difference("CommentVote.count") do + post_auth comment_votes_path(@comment), @user, params: { score: -1, format: :json } + assert_response 422 + end + end + + should "prevent voting on comment disabled posts" do + @post.update(is_comment_disabled: true) + assert_no_difference("CommentVote.count") do + post_auth comment_votes_path(@comment), @user, params: { score: -1, format: :json } + assert_response 422 + end + end + + should "prevent unvoting on comment disabled posts" do + @post.update(is_comment_disabled: true) + create(:comment_vote, comment: @comment, user: @user, score: -1) + assert_no_difference("CommentVote.count") do + post_auth comment_votes_path(@comment), @user, params: { score: -1, format: :json } + assert_response 422 + end + end end end end diff --git a/test/functional/comments_controller_test.rb b/test/functional/comments_controller_test.rb index 96df1b6c3..98e8fc801 100644 --- a/test/functional/comments_controller_test.rb +++ b/test/functional/comments_controller_test.rb @@ -108,6 +108,22 @@ class CommentsControllerTest < ActionDispatch::IntegrationTest assert_equal(false, @comment.reload.do_not_bump_post) assert_equal(@post.id, @comment.post_id) end + + should "not allow changing comments on comment locked posts" do + @post.update(is_comment_locked: true) + body = @comment.body + put_auth comment_path(@comment.id), @user, params: { comment: { body: "abc" } } + assert_response(:forbidden) + assert_equal(body, @comment.reload.body) + end + + should "not allow changing comments on comment disabled posts" do + @post.update(is_comment_disabled: true) + body = @comment.body + put_auth comment_path(@comment.id), @user, params: { comment: { body: "abc" } } + assert_response(:forbidden) + assert_equal(body, @comment.reload.body) + end end context "new action" do @@ -132,14 +148,39 @@ class CommentsControllerTest < ActionDispatch::IntegrationTest end assert_redirected_to comments_path end + + should "not allow commenting on comment locked posts" do + @post.update(is_comment_locked: true) + assert_difference("Comment.count", 0) do + post_auth comments_path, @user, params: { comment: { body: "abc", post_id: @post.id } } + assert_redirected_to(post_path(@post)) + assert_equal("Post has comments locked", flash[:notice]) + end + end + + should "not allow commenting on comment disabled posts" do + @post.update(is_comment_disabled: true) + assert_difference("Comment.count", 0) do + post_auth comments_path, @user, params: { comment: { body: "abc", post_id: @post.id } } + assert_redirected_to(post_path(@post)) + assert_equal("Post has comments disabled", flash[:notice]) + end + end end context "hide action" do should "mark comment as hidden" do - post_auth hide_comment_path(@comment.id), @user + post_auth hide_comment_path(@comment), @user assert_equal(true, @comment.reload.is_hidden) assert_redirected_to @comment end + + should "not allow hiding comments on comment disabled posts" do + @post.update(is_comment_disabled: true) + post_auth hide_comment_path(@comment), @user + assert_equal(false, @comment.reload.is_hidden) + assert_response(403) + end end context "unhide action" do @@ -148,13 +189,13 @@ class CommentsControllerTest < ActionDispatch::IntegrationTest end should "mark comment as unhidden if mod" do - post_auth unhide_comment_path(@comment.id), @mod + post_auth unhide_comment_path(@comment), @mod assert_equal(false, @comment.reload.is_hidden) assert_redirected_to(@comment) end should "not mark comment as unhidden if not mod" do - post_auth unhide_comment_path(@comment.id), @user + post_auth unhide_comment_path(@comment), @user assert_equal(true, @comment.reload.is_hidden) assert_response :forbidden end @@ -162,7 +203,7 @@ class CommentsControllerTest < ActionDispatch::IntegrationTest context "destroy action" do should "destroy the comment" do - delete_auth comment_path(@comment.id), create(:admin_user) + delete_auth comment_path(@comment), create(:admin_user) assert_equal(0, Comment.where(id: @comment.id).count) end end diff --git a/test/functional/posts_controller_test.rb b/test/functional/posts_controller_test.rb index ee5f8d580..20f73c69f 100644 --- a/test/functional/posts_controller_test.rb +++ b/test/functional/posts_controller_test.rb @@ -79,6 +79,38 @@ class PostsControllerTest < ActionDispatch::IntegrationTest put_auth post_path(@post), @user, params: {:post => {:last_noted_at => 1.minute.ago}} assert_nil(@post.reload.last_noted_at) end + + should "allow moderators to lock comments" do + assert_difference("PostEvent.count", 1) do + put_auth post_path(@post), create(:moderator_user), params: { post: { is_comment_locked: true } } + end + assert_equal(true, @post.reload.is_comment_locked?) + assert_equal("comment_locked", PostEvent.last.action) + end + + should "allow moderators to unlock comments" do + @post.update_columns(is_comment_locked: true) + assert_difference("PostEvent.count", 1) do + put_auth post_path(@post), create(:moderator_user), params: { post: { is_comment_locked: false } } + end + assert_equal(false, @post.reload.is_comment_locked?) + assert_equal("comment_unlocked", PostEvent.last.action) + end + + should "not allow moderators to disable comments" do + assert_no_difference("PostEvent.count") do + put_auth post_path(@post), create(:moderator_user), params: { post: { is_comment_disabled: true } } + end + assert_equal(false, @post.reload.is_comment_disabled?) + end + + should "not allow moderators to enable comments" do + @post.update_columns(is_comment_disabled: true) + assert_no_difference("PostEvent.count") do + put_auth post_path(@post), create(:moderator_user), params: { post: { is_comment_disabled: false } } + end + assert_equal(true, @post.reload.is_comment_disabled?) + end end context "revert action" do diff --git a/test/unit/comment_test.rb b/test/unit/comment_test.rb index ca50f6ca7..4bf0fe1c6 100644 --- a/test/unit/comment_test.rb +++ b/test/unit/comment_test.rb @@ -281,6 +281,18 @@ class CommentTest < ActiveSupport::TestCase assert_equal(["Post has comments locked"], comment.errors.full_messages) end end + + context "on a comment disabled post" do + setup do + @post = create(:post, is_comment_disabled: true) + end + + should "prevent new comments" do + comment = build(:comment, post: @post) + comment.save + assert_equal(["Post has comments disabled"], comment.errors.full_messages) + end + end end context "during validation" do