diff --git a/app/components/shared/in_favor_against_component.html.erb b/app/components/shared/in_favor_against_component.html.erb index c4c403649..954c10ddb 100644 --- a/app/components/shared/in_favor_against_component.html.erb +++ b/app/components/shared/in_favor_against_component.html.erb @@ -4,7 +4,7 @@ title: t("votes.agree"), "aria-label": agree_aria_label, "aria-pressed": pressed?("yes"), - method: "post", + method: user_already_voted_with("yes") ? "delete" : "post", remote: true do %> <%= t("votes.agree") %> <% end %> @@ -16,7 +16,7 @@ title: t("votes.disagree"), "aria-label": disagree_aria_label, "aria-pressed": pressed?("no"), - method: "post", + method: user_already_voted_with("no") ? "delete" : "post", remote: true do %> <%= t("votes.disagree") %> <% end %> diff --git a/app/components/shared/in_favor_against_component.rb b/app/components/shared/in_favor_against_component.rb index 0c0736407..68e96e99b 100644 --- a/app/components/shared/in_favor_against_component.rb +++ b/app/components/shared/in_favor_against_component.rb @@ -28,10 +28,31 @@ class Shared::InFavorAgainstComponent < ApplicationComponent end def vote_in_favor_against_path(value) - if votable.class.name == "Debate" - debate_votes_path(votable, value: value) + if user_already_voted_with(value) + remove_vote_path(value) else - legislation_process_proposal_votes_path(votable.process, votable, value: value) + if votable.class.name == "Debate" + debate_votes_path(votable, value: value) + else + legislation_process_proposal_votes_path(votable.process, votable, value: value) + end end end + + def user_already_voted_with(value) + current_user&.voted_as_when_voted_for(votable) == parse_vote(value) + end + + def remove_vote_path(value) + vote = votable.votes_for.find_by!(voter: current_user) + if votable.class.name == "Debate" + debate_vote_path(votable, vote, value: value) + else + legislation_process_proposal_vote_path(votable.process, votable, vote, value: value) + end + end + + def parse_vote(value) + value == "yes" ? true : false + end end diff --git a/app/controllers/debates/votes_controller.rb b/app/controllers/debates/votes_controller.rb index e0b92c472..ae0cf33d5 100644 --- a/app/controllers/debates/votes_controller.rb +++ b/app/controllers/debates/votes_controller.rb @@ -2,6 +2,7 @@ module Debates class VotesController < ApplicationController before_action :authenticate_user! load_and_authorize_resource :debate + load_and_authorize_resource through: :debate, through_association: :votes_for, only: :destroy def create authorize! :create, Vote.new(voter: current_user, votable: @debate) @@ -11,5 +12,13 @@ module Debates format.js { render :show } end end + + def destroy + @debate.unvote_by(current_user) + + respond_to do |format| + format.js { render :show } + end + end end end diff --git a/app/controllers/legislation/proposals/votes_controller.rb b/app/controllers/legislation/proposals/votes_controller.rb index 03886c41c..e51d97db5 100644 --- a/app/controllers/legislation/proposals/votes_controller.rb +++ b/app/controllers/legislation/proposals/votes_controller.rb @@ -4,6 +4,7 @@ module Legislation before_action :authenticate_user! load_and_authorize_resource :process, class: "Legislation::Process" load_and_authorize_resource :proposal, class: "Legislation::Proposal", through: :process + load_and_authorize_resource through: :proposal, through_association: :votes_for, only: :destroy def create authorize! :create, Vote.new(voter: current_user, votable: @proposal) @@ -13,6 +14,14 @@ module Legislation format.js { render :show } end end + + def destroy + @proposal.unvote_by(current_user) + + respond_to do |format| + format.js { render :show } + end + end end end end diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb index 53378b4be..f4493840b 100644 --- a/app/models/abilities/common.rb +++ b/app/models/abilities/common.rb @@ -81,14 +81,14 @@ module Abilities can [:create, :destroy], DirectUpload unless user.organization? - can :create, ActsAsVotable::Vote, voter_id: user.id, votable_type: "Debate" + can [:create, :destroy], ActsAsVotable::Vote, voter_id: user.id, votable_type: "Debate" can :vote, Comment end if user.level_two_or_three_verified? can :vote, Proposal, &:published? - can :create, ActsAsVotable::Vote, voter_id: user.id, votable_type: "Legislation::Proposal" + can [:create, :destroy], ActsAsVotable::Vote, voter_id: user.id, votable_type: "Legislation::Proposal" can :create, Legislation::Answer diff --git a/config/routes/debate.rb b/config/routes/debate.rb index 45d6f06f6..03735cc15 100644 --- a/config/routes/debate.rb +++ b/config/routes/debate.rb @@ -11,5 +11,5 @@ resources :debates do put "recommendations/disable", only: :index, controller: "debates", action: :disable_recommendations end - resources :votes, controller: "debates/votes", only: :create + resources :votes, controller: "debates/votes", only: [:create, :destroy] end diff --git a/config/routes/legislation.rb b/config/routes/legislation.rb index 7e3890c42..9b3f1a0f9 100644 --- a/config/routes/legislation.rb +++ b/config/routes/legislation.rb @@ -23,7 +23,7 @@ namespace :legislation do get :suggest end - resources :votes, controller: "proposals/votes", only: :create + resources :votes, controller: "proposals/votes", only: [:create, :destroy] end resources :draft_versions, only: [:show] do diff --git a/spec/components/shared/in_favor_against_component_spec.rb b/spec/components/shared/in_favor_against_component_spec.rb index ece805749..f3ade0ec9 100644 --- a/spec/components/shared/in_favor_against_component_spec.rb +++ b/spec/components/shared/in_favor_against_component_spec.rb @@ -22,6 +22,24 @@ describe Shared::InFavorAgainstComponent do end end + it "can undo vote when the user has already voted" do + create(:vote, votable: debate, voter: user) + + sign_in user + + render_inline component + + page.find(".in-favor") do |in_favor_block| + expect(in_favor_block).to have_css "form[action*='votes'][method='post']" + expect(in_favor_block).to have_css "input[name='_method'][value='delete']", visible: :hidden + end + + page.find(".against") do |against_block| + expect(against_block).to have_css "form[action*='votes'][method='post']" + expect(against_block).not_to have_css "input[name='_method']", visible: :all + end + end + it "does not include result percentages" do create(:vote, votable: debate) sign_in(user) diff --git a/spec/controllers/debates/votes_controller_spec.rb b/spec/controllers/debates/votes_controller_spec.rb index 9e6090ee6..74bd009d2 100644 --- a/spec/controllers/debates/votes_controller_spec.rb +++ b/spec/controllers/debates/votes_controller_spec.rb @@ -40,4 +40,17 @@ describe Debates::VotesController do end end end + + describe "DELETE destroy" do + let(:user) { create(:user) } + let(:debate) { create(:debate) } + let!(:vote) { create(:vote, votable: debate, voter: user) } + before { sign_in user } + + it "allows undoing a vote if user is allowed" do + expect do + delete :destroy, xhr: true, params: { debate_id: debate.id, id: vote } + end.to change { debate.reload.votes_for.size }.by(-1) + end + end end diff --git a/spec/controllers/legislation/proposals/votes_controller_spec.rb b/spec/controllers/legislation/proposals/votes_controller_spec.rb index fd8c780a6..f618277b1 100644 --- a/spec/controllers/legislation/proposals/votes_controller_spec.rb +++ b/spec/controllers/legislation/proposals/votes_controller_spec.rb @@ -37,4 +37,20 @@ describe Legislation::Proposals::VotesController do end.not_to change { proposal.reload.votes_for.size } end end + + describe "DELETE destroy" do + let(:user) { create(:user, :level_two) } + let!(:vote) { create(:vote, votable: proposal, voter: user) } + let(:vote_params) do + { process_id: legislation_process.id, legislation_proposal_id: proposal.id, id: vote } + end + + it "allows undoing a vote" do + sign_in user + + expect do + delete :destroy, xhr: true, params: vote_params + end.to change { proposal.reload.votes_for.size }.by(-1) + end + end end diff --git a/spec/models/abilities/common_spec.rb b/spec/models/abilities/common_spec.rb index 9ba947776..5d13ab111 100644 --- a/spec/models/abilities/common_spec.rb +++ b/spec/models/abilities/common_spec.rb @@ -77,6 +77,8 @@ describe Abilities::Common do it { should be_able_to(:show, debate) } it { should be_able_to(:create, user.votes.build(votable: debate)) } it { should_not be_able_to(:create, another_user.votes.build(votable: debate)) } + it { should be_able_to(:destroy, user.votes.build(votable: debate)) } + it { should_not be_able_to(:destroy, another_user.votes.build(votable: debate)) } it { should be_able_to(:show, user) } it { should be_able_to(:edit, user) } @@ -184,12 +186,15 @@ describe Abilities::Common do describe "vote legislation proposal" do context "when user is not level_two_or_three_verified" do it { should_not be_able_to(:create, user.votes.build(votable: legislation_proposal)) } + it { should_not be_able_to(:destroy, user.votes.build(votable: legislation_proposal)) } end context "when user is level_two_or_three_verified" do before { user.update(level_two_verified_at: Date.current) } it { should be_able_to(:create, user.votes.build(votable: legislation_proposal)) } it { should_not be_able_to(:create, another_user.votes.build(votable: legislation_proposal)) } + it { should be_able_to(:destroy, user.votes.build(votable: legislation_proposal)) } + it { should_not be_able_to(:destroy, another_user.votes.build(votable: legislation_proposal)) } end end diff --git a/spec/models/abilities/organization_spec.rb b/spec/models/abilities/organization_spec.rb index 9bfd9b69f..50d52dda1 100644 --- a/spec/models/abilities/organization_spec.rb +++ b/spec/models/abilities/organization_spec.rb @@ -15,6 +15,7 @@ describe "Abilities::Organization" do it { should be_able_to(:index, Debate) } it { should be_able_to(:show, debate) } it { should_not be_able_to(:create, user.votes.build(votable: debate)) } + it { should_not be_able_to(:destroy, user.votes.build(votable: debate)) } it { should be_able_to(:index, Proposal) } it { should be_able_to(:show, proposal) } diff --git a/spec/system/votes_spec.rb b/spec/system/votes_spec.rb index abed25032..96eaf11e5 100644 --- a/spec/system/votes_spec.rb +++ b/spec/system/votes_spec.rb @@ -100,20 +100,27 @@ describe "Votes" do expect(page).to have_content "1 vote" end - scenario "Trying to vote multiple times" do + scenario "Allow undoing votes" do visit debate_path(create(:debate)) click_button "I agree" + expect(page).to have_content "1 vote" + expect(page).not_to have_content "No votes" + click_button "I agree" + + expect(page).to have_content "No votes" expect(page).not_to have_content "2 votes" within(".in-favor") do - expect(page).to have_content "100%" + expect(page).to have_content "0%" + expect(page).to have_css "button[aria-pressed='false']" end within(".against") do expect(page).to have_content "0%" + expect(page).to have_css "button[aria-pressed='false']" end end @@ -337,4 +344,31 @@ describe "Votes" do expect(page).not_to have_button "Support", disabled: :all end end + + describe "Legislation Proposals" do + let(:proposal) { create(:legislation_proposal) } + + scenario "Allow undoing votes" do + login_as verified + visit legislation_process_proposal_path(proposal.process, proposal) + + click_button "I agree" + + expect(page).to have_content "1 vote" + expect(page).not_to have_content "No votes" + + click_button "I agree" + + expect(page).to have_content "No votes" + expect(page).not_to have_content "2 votes" + + within(".in-favor") do + expect(page).to have_content "0%" + end + + within(".against") do + expect(page).to have_content "0%" + end + end + end end