diff --git a/app/controllers/reactions_controller.rb b/app/controllers/reactions_controller.rb index eaf53fe63..4ac6ce829 100644 --- a/app/controllers/reactions_controller.rb +++ b/app/controllers/reactions_controller.rb @@ -55,55 +55,13 @@ class ReactionsController < ApplicationController def create remove_count_cache_key - if params[:reactable_type] == "Article" && params[:category].in?(Reaction::PRIVILEGED_CATEGORIES) - clear_moderator_reactions( - params[:reactable_id], - params[:reactable_type], - current_user, - params[:category], - ) - end + result = ReactionToggle.toggle(params, current_user: current_user) - category = params[:category] || "like" - - reaction = Reaction.where( - user_id: current_user.id, - reactable_id: params[:reactable_id], - reactable_type: params[:reactable_type], - category: category, - ).first - - # if the reaction already exists, destroy it - if reaction - result = handle_existing_reaction(reaction) + if result.success? + render json: { result: result.action, category: result.category } else - reaction = build_reaction(category) - - if reaction.save - rate_limiter.track_limit_by_action(:reaction_creation) - Moderator::SinkArticles.call(reaction.reactable_id) if reaction.vomit_on_user? - - Notification.send_reaction_notification(reaction, reaction.target_user) - if reaction.reaction_on_organization_article? - Notification.send_reaction_notification(reaction, - reaction.reactable.organization) - end - - result = "create" - - if category == "readinglist" && current_user.setting.experience_level - rate_article(reaction) - end - - if current_user.auditable? - Audit::Logger.log(:moderator, current_user, params.dup) - end - else - render json: { error: reaction.errors_as_sentence, status: 422 }, status: :unprocessable_entity - return - end + render json: { error: result.errors_as_sentence, status: 422 }, status: :unprocessable_entity end - render json: { result: result, category: category } end def cached_user_public_comment_reactions(user, comment_ids) @@ -118,67 +76,6 @@ class ReactionsController < ApplicationController private - def build_reaction(category) - create_params = { - user_id: current_user.id, - reactable_id: params[:reactable_id], - reactable_type: params[:reactable_type], - category: category - } - if (current_user&.any_admin? || current_user&.super_moderator?) && - Reaction::NEGATIVE_PRIVILEGED_CATEGORIES.include?(category) - create_params[:status] = "confirmed" - end - Reaction.new(create_params) - end - - def destroy_reaction(reaction) - reaction.destroy - Moderator::SinkArticles.call(reaction.reactable_id) if reaction.vomit_on_user? - Notification.send_reaction_notification_without_delay(reaction, reaction.target_user) - if reaction.reaction_on_organization_article? - Notification.send_reaction_notification_without_delay(reaction, - reaction.reactable.organization) - end - "destroy" - end - - def rate_article(reaction) - user_experience_level = current_user.setting.experience_level - return unless user_experience_level - - RatingVote.create(article_id: reaction.reactable_id, - group: "experience_level", - user_id: current_user.id, - context: "readinglist_reaction", - rating: user_experience_level) - end - - def clear_moderator_reactions(id, type, mod, category) - reactions = if category == "thumbsup" - Reaction.where(reactable_id: id, reactable_type: type, user: mod, - category: Reaction::NEGATIVE_PRIVILEGED_CATEGORIES) - elsif category.in?(Reaction::NEGATIVE_PRIVILEGED_CATEGORIES) - Reaction.where(reactable_id: id, reactable_type: type, user: mod, category: "thumbsup") - end - - return if reactions.blank? - - reactions.find_each { |reaction| destroy_reaction(reaction) } - end - - def handle_existing_reaction(reaction) - result = destroy_reaction(reaction) - - if reaction.negative? && current_user.auditable? - updated_params = params.dup - updated_params[:action] = "destroy" - Audit::Logger.log(:moderator, current_user, updated_params) - end - - result - end - def check_limit rate_limit!(:reaction_creation) end @@ -190,6 +87,7 @@ class ReactionsController < ApplicationController authorize(Reaction, policy_query) end + # TODO: should this move to toggle service? refactor? def remove_count_cache_key return unless params[:reactable_type] == "Article" diff --git a/app/services/reaction_toggle.rb b/app/services/reaction_toggle.rb new file mode 100644 index 000000000..d2afc1604 --- /dev/null +++ b/app/services/reaction_toggle.rb @@ -0,0 +1,141 @@ +class ReactionToggle + class Result + attr_accessor :action, :category, :reaction + + delegate :errors_as_sentence, to: :reaction + + def initialize(reaction: nil, action: nil, category: nil) + @reaction = reaction + @category = category + @action = action + end + + def success? + reaction.errors.none? + end + end + + def self.toggle(params, current_user:) + new(params, current_user: current_user).toggle + end + + def initialize(params, current_user:) + @params = params + @current_user = current_user + @category = params[:category] || "like" + end + + attr_reader :category, :params, :current_user + + delegate :rate_limiter, to: :current_user + + def toggle + if params[:reactable_type] == "Article" && params[:category].in?(Reaction::PRIVILEGED_CATEGORIES) + clear_moderator_reactions( + params[:reactable_id], + params[:reactable_type], + current_user, + params[:category], + ) + end + + reaction = Reaction.where( + user_id: current_user.id, + reactable_id: params[:reactable_id], + reactable_type: params[:reactable_type], + category: category, + ).first + + # if the reaction already exists, destroy it + return handle_existing_reaction(reaction) if reaction + + reaction = build_reaction(category) + result = Result.new reaction: reaction, category: category + + if reaction.save + rate_limiter.track_limit_by_action(:reaction_creation) + Moderator::SinkArticles.call(reaction.reactable_id) if reaction.vomit_on_user? + + Notification.send_reaction_notification(reaction, reaction.target_user) + if reaction.reaction_on_organization_article? + Notification.send_reaction_notification(reaction, + reaction.reactable.organization) + end + + result.action = "create" + + if category == "readinglist" && current_user.setting.experience_level + rate_article(reaction) + end + + if current_user.auditable? + Audit::Logger.log(:moderator, current_user, params.dup) + end + end + + result + end + + private + + def clear_moderator_reactions(id, type, mod, category) + reactions = if category == "thumbsup" + Reaction.where(reactable_id: id, reactable_type: type, user: mod, + category: Reaction::NEGATIVE_PRIVILEGED_CATEGORIES) + elsif category.in?(Reaction::NEGATIVE_PRIVILEGED_CATEGORIES) + Reaction.where(reactable_id: id, reactable_type: type, user: mod, category: "thumbsup") + end + + return if reactions.blank? + + reactions.find_each { |reaction| destroy_reaction(reaction) } + end + + def handle_existing_reaction(reaction) + destroy_reaction(reaction) + + if reaction.negative? && current_user.auditable? + updated_params = params.dup + updated_params[:action] = "destroy" + Audit::Logger.log(:moderator, current_user, updated_params) + end + + Result.new category: category, reaction: reaction, action: "destroy" + end + + def build_reaction(category) + create_params = { + user_id: current_user.id, + reactable_id: params[:reactable_id], + reactable_type: params[:reactable_type], + category: category + } + if (current_user&.any_admin? || current_user&.super_moderator?) && + Reaction::NEGATIVE_PRIVILEGED_CATEGORIES.include?(category) + create_params[:status] = "confirmed" + end + Reaction.new(create_params) + end + + def destroy_reaction(reaction) + reaction.destroy + Moderator::SinkArticles.call(reaction.reactable_id) if reaction.vomit_on_user? + Notification.send_reaction_notification_without_delay(reaction, reaction.target_user) + if reaction.reaction_on_organization_article? + Notification.send_reaction_notification_without_delay(reaction, + reaction.reactable.organization) + end + "destroy" + end + + def rate_article(reaction) + user_experience_level = current_user.setting.experience_level + return unless user_experience_level + + RatingVote.create(article_id: reaction.reactable_id, + group: "experience_level", + user_id: current_user.id, + context: "readinglist_reaction", + rating: user_experience_level) + end +end diff --git a/spec/requests/reactions_spec.rb b/spec/requests/reactions_spec.rb index cbc2aaf88..702af8813 100644 --- a/spec/requests/reactions_spec.rb +++ b/spec/requests/reactions_spec.rb @@ -183,7 +183,7 @@ RSpec.describe "Reactions", type: :request do allow(rate_limiter).to receive(:limit_by_action).and_return(true) post "/reactions", params: article_params - expect(response.status).to eq(429) + expect(response).to have_http_status(:too_many_requests) end end @@ -205,6 +205,11 @@ RSpec.describe "Reactions", type: :request do post "/reactions", params: article_params end.to change(Reaction, :count).by(-1) end + + it "has success http status" do + post "/reactions", params: article_params + expect(response).to be_successful + end end context "when creating readinglist" do @@ -247,6 +252,15 @@ RSpec.describe "Reactions", type: :request do expect(RatingVote).not_to have_received(:create) end + + it "has success http status" do + post "/reactions", params: { + reactable_id: article.id, + reactable_type: "Article", + category: "readinglist" + } + expect(response).to be_successful + end end context "when attempting to create thumbsup as regular user" do @@ -284,6 +298,15 @@ RSpec.describe "Reactions", type: :request do expect(Reaction.where(category: "thumbsdown").count).to eq(0) expect(Reaction.where(category: "like").count).to eq(1) end + + it "has success http status" do + post "/reactions", params: { + reactable_id: article.id, + reactable_type: "Article", + category: "thumbsup" + } + expect(response).to be_successful + end end context "when creating thumbsdown" do @@ -306,6 +329,15 @@ RSpec.describe "Reactions", type: :request do expect(Reaction.where(category: "like").size).to be 1 expect(Reaction.where(category: "vomit").size).to be 1 end + + it "has success http status" do + post "/reactions", params: { + reactable_id: article.id, + reactable_type: "Article", + category: "thumbsdown" + } + expect(response).to be_successful + end end context "when vomiting on a user" do @@ -325,6 +357,11 @@ RSpec.describe "Reactions", type: :request do post "/reactions", params: user_params end.to change(Reaction, :count).by(-1) end + + it "has success http status" do + post "/reactions", params: user_params + expect(response).to be_successful + end end context "when signed in as admin" do @@ -357,6 +394,11 @@ RSpec.describe "Reactions", type: :request do expect(reaction.category).to eq("like") expect(reaction.status).to eq("valid") end + + it "has success http status" do + post "/reactions", params: article_params + expect(response).to be_successful + end end context "when signed out" do