Merge pull request #4504 from consul/remove-supports

Allow users to remove their supports on budget investments
This commit is contained in:
Javi Martín
2021-06-14 15:50:58 +02:00
committed by GitHub
35 changed files with 297 additions and 120 deletions

View File

@@ -5,19 +5,28 @@
</span>
<div class="in-favor">
<% if user_voted_for? %>
<div class="supported callout success">
<%= t("budgets.investments.investment.already_supported") %>
</div>
<% elsif investment.should_show_votes? %>
<%= button_to t("budgets.investments.investment.give_support"), vote_url,
class: "button button-support small expanded",
title: t("budgets.investments.investment.support_title"),
method: "post",
remote: !display_support_alert?,
data: ({ confirm: confirm_vote_message } if display_support_alert?),
disabled: !current_user,
"aria-label": support_aria_label %>
<% if investment.should_show_votes? %>
<% if user_voted_for? %>
<div class="supported">
<div class="callout success">
<%= t("budgets.investments.votes.already_supported") %>
</div>
<%= button_to t("budgets.investments.votes.remove_support"), remove_support_path,
class: "button button-remove-support expanded",
method: "delete",
remote: true,
"aria-label": remove_support_aria_label %>
</div>
<% else %>
<%= button_to t("budgets.investments.votes.support"), support_path,
class: "button button-support expanded",
title: t("budgets.investments.investment.support_title"),
method: "post",
remote: !display_support_alert?,
data: ({ confirm: confirm_vote_message } if display_support_alert?),
disabled: !current_user,
"aria-label": support_aria_label %>
<% end %>
<% end %>
</div>

View File

@@ -1,12 +1,30 @@
class Budgets::Investments::VotesComponent < ApplicationComponent
attr_reader :investment, :investment_votes, :vote_url
delegate :current_user, :voted_for?, :image_absolute_url,
attr_reader :investment
delegate :namespace, :current_user, :voted_for?, :image_absolute_url,
:link_to_verify_account, :link_to_signin, :link_to_signup, to: :helpers
def initialize(investment, investment_votes:, vote_url:)
def initialize(investment)
@investment = investment
@investment_votes = investment_votes
@vote_url = vote_url
end
def support_path
case namespace
when "management"
management_budget_investment_votes_path(investment.budget, investment)
else
budget_investment_votes_path(investment.budget, investment)
end
end
def remove_support_path
vote = investment.votes_for.find_by!(voter: current_user)
case namespace
when "management"
management_budget_investment_vote_path(investment.budget, investment, vote)
else
budget_investment_vote_path(investment.budget, investment, vote)
end
end
private
@@ -20,7 +38,8 @@ class Budgets::Investments::VotesComponent < ApplicationComponent
end
def user_voted_for?
@user_voted_for ||= voted_for?(investment_votes, investment)
@user_voted_for = current_user&.voted_for?(investment) unless defined?(@user_voted_for)
@user_voted_for
end
def display_support_alert?
@@ -30,10 +49,14 @@ class Budgets::Investments::VotesComponent < ApplicationComponent
end
def confirm_vote_message
t("budgets.investments.investment.confirm_group", count: investment.group.max_votable_headings)
t("budgets.investments.votes.confirm_group", count: investment.group.max_votable_headings)
end
def support_aria_label
t("budgets.investments.investment.support_label", investment: investment.title)
t("budgets.investments.votes.support_label", investment: investment.title)
end
def remove_support_aria_label
t("budgets.investments.votes.remove_support_label", investment: investment.title)
end
end

View File

@@ -5,7 +5,6 @@
</header>
<p><%= t("budgets.supports_info.next") %></p>
<p><%= sanitize(t("budgets.supports_info.remember")) %></p>
<p><%= t("budgets.supports_info.different") %></p>
</section>

View File

@@ -0,0 +1,30 @@
module Budgets
module Investments
class VotesController < ApplicationController
load_and_authorize_resource :budget
load_and_authorize_resource :investment, through: :budget, class: "Budget::Investment"
load_and_authorize_resource through: :investment, through_association: :votes_for, only: :destroy
def create
@investment.register_selection(current_user)
respond_to do |format|
format.html do
redirect_to budget_investments_path(heading_id: @investment.heading.id),
notice: t("flash.actions.create.support")
end
format.js { render :show }
end
end
def destroy
@investment.unliked_by(current_user)
respond_to do |format|
format.js { render :show }
end
end
end
end
end

View File

@@ -47,7 +47,6 @@ module Budgets
@investment_ids = @investments.pluck(:id)
@investments_map_coordinates = MapLocation.where(investment: investments).map(&:json_data)
load_investment_votes(@investments)
@tag_cloud = tag_cloud
@remote_translations = detect_remote_translations(@investments)
end
@@ -60,7 +59,6 @@ module Budgets
@comment_tree = CommentTree.new(@commentable, params[:page], @current_order)
@related_contents = Kaminari.paginate_array(@investment.relationed_contents).page(params[:page]).per(5)
set_comment_flags(@comment_tree.comments)
load_investment_votes(@investment)
@investment_ids = [@investment.id]
@remote_translations = detect_remote_translations([@investment], @comment_tree.comments)
end
@@ -92,15 +90,6 @@ module Budgets
redirect_to user_path(current_user, filter: "budget_investments"), notice: t("flash.actions.destroy.budget_investment")
end
def vote
@investment.register_selection(current_user)
load_investment_votes(@investment)
respond_to do |format|
format.html { redirect_to budget_investments_path(heading_id: @investment.heading.id) }
format.js
end
end
def suggest
@resource_path_method = :namespaced_budget_investment_path
@resource_relation = resource_model.where(budget: @budget).apply_filters_and_search(@budget, params, @current_filter)
@@ -130,10 +119,6 @@ module Budgets
"budget_investment"
end
def load_investment_votes(investments)
@investment_votes = current_user ? current_user.budget_investment_votes(investments) : {}
end
def investment_params
attributes = [:heading_id, :tag_list, :organization_name, :location,
:terms_of_service, :related_sdg_list,

View File

@@ -0,0 +1,26 @@
class Management::Budgets::Investments::VotesController < Management::BaseController
load_resource :budget, find_by: :slug_or_id
load_resource :investment, through: :budget, class: "Budget::Investment"
load_and_authorize_resource through: :investment, through_association: :votes_for, only: :destroy
def create
@investment.register_selection(managed_user)
respond_to do |format|
format.html do
redirect_to management_budget_investments_path(heading_id: @investment.heading.id),
notice: t("flash.actions.create.support")
end
format.js { render :show }
end
end
def destroy
@investment.unliked_by(managed_user)
respond_to do |format|
format.js { render :show }
end
end
end

View File

@@ -15,7 +15,6 @@ class Management::Budgets::InvestmentsController < Management::BaseController
def index
@investments = @investments.apply_filters_and_search(@budget, params).page(params[:page])
load_investment_votes(@investments)
end
def new
@@ -37,29 +36,14 @@ class Management::Budgets::InvestmentsController < Management::BaseController
end
def show
load_investment_votes(@investment)
end
def vote
@investment.register_selection(managed_user)
load_investment_votes(@investment)
respond_to do |format|
format.html { redirect_to management_budget_investments_path(heading_id: @investment.heading.id) }
format.js
end
end
def print
@investments = @investments.apply_filters_and_search(@budget, params).order(cached_votes_up: :desc).for_render.limit(15)
load_investment_votes(@investments)
end
private
def load_investment_votes(investments)
@investment_votes = managed_user ? managed_user.budget_investment_votes(investments) : {}
end
def investment_params
attributes = [:external_url, :heading_id, :tag_list, :organization_name, :location,
image_attributes: image_attributes,

View File

@@ -15,15 +15,6 @@ module BudgetsHelper
end
end
def namespaced_budget_investment_vote_path(investment, options = {})
case namespace
when "management"
vote_management_budget_investment_path(investment.budget, investment, options)
else
vote_budget_investment_path(investment.budget, investment, options)
end
end
def css_for_ballot_heading(heading)
return "" if current_ballot.blank? || @current_filter == "unfeasible"

View File

@@ -99,7 +99,10 @@ module Abilities
can :update, Budget::Investment, budget: { phase: "accepting" }, author_id: user.id
can :suggest, Budget::Investment, budget: { phase: "accepting" }
can :destroy, Budget::Investment, budget: { phase: ["accepting", "reviewing"] }, author_id: user.id
can :vote, Budget::Investment, budget: { phase: "selecting" }
can [:create, :destroy], ActsAsVotable::Vote,
voter_id: user.id,
votable_type: "Budget::Investment",
votable: { budget: { phase: "selecting" }}
can [:show, :create], Budget::Ballot, budget: { phase: "balloting" }
can [:create, :destroy], Budget::Ballot::Line, budget: { phase: "balloting" }

View File

@@ -277,7 +277,7 @@ class Budget
def permission_problem(user)
return :not_logged_in unless user
return :organization if user.organization?
return :not_verified unless user.can?(:vote, Budget::Investment)
return :not_verified unless user.can?(:create, ActsAsVotable::Vote)
nil
end

View File

@@ -161,11 +161,6 @@ class User < ApplicationRecord
voted.each_with_object({}) { |v, h| h[v.votable_id] = v.value }
end
def budget_investment_votes(budget_investments)
voted = votes.for_budget_investments(budget_investments)
voted.each_with_object({}) { |v, h| h[v.votable_id] = v.value }
end
def comment_flags(comments)
comment_flags = flags.for_comments(comments)
comment_flags.each_with_object({}) { |f, h| h[f.flaggable_id] = true }

View File

@@ -38,10 +38,7 @@
<div id="<%= dom_id(investment) %>_votes"
class="small-12 medium-3 column text-center"
<%= "data-equalizer-watch" if feature?(:allow_images) && investment.image.present? %>>
<%= render "/budgets/investments/votes",
investment: investment,
investment_votes: investment_votes,
vote_url: namespaced_budget_investment_vote_path(investment, value: "yes") %>
<%= render Budgets::Investments::VotesComponent.new(investment) %>
</div>
<% elsif investment.should_show_vote_count? %>
<div id="<%= dom_id(investment) %>_votes"

View File

@@ -13,7 +13,7 @@
investment.author,
Flag.flagged?(current_user, investment),
investment.followed_by?(current_user),
@investment_votes] do %>
current_user&.voted_for?(investment)] do %>
<section class="budget-investment-show" id="<%= dom_id(investment) %>">
<div class="row">
@@ -32,10 +32,7 @@
<h2><%= t("budgets.investments.show.supports") %></h2>
<div class="text-center">
<div id="<%= dom_id(investment) %>_votes">
<%= render "/budgets/investments/votes",
investment: investment,
investment_votes: investment_votes,
vote_url: namespaced_budget_investment_vote_path(investment, value: "yes") %>
<%= render Budgets::Investments::VotesComponent.new(investment) %>
</div>
</div>
<% elsif investment.should_show_vote_count? %>

View File

@@ -1,5 +0,0 @@
<%= render Budgets::Investments::VotesComponent.new(
investment,
investment_votes: investment_votes,
vote_url: vote_url
) %>

View File

@@ -78,7 +78,6 @@
<%= render "/budgets/investments/investment",
investment: investment,
investment_ids: @investment_ids,
investment_votes: @investment_votes,
ballot: @ballot %>
<% end %>
<% else %>

View File

@@ -6,7 +6,6 @@
<%= render "/budgets/investments/investment_show",
investment: @investment,
investment_ids: @investment_ids,
investment_votes: @investment_votes,
ballot: @ballot %>
<div class="row">

View File

@@ -1,4 +0,0 @@
$("#<%= dom_id(@investment) %>_votes").html("<%= j render("/budgets/investments/votes",
investment: @investment,
investment_votes: @investment_votes,
vote_url: namespaced_budget_investment_vote_path(@investment, value: "yes")) %>");

View File

@@ -0,0 +1,2 @@
$("#<%= dom_id(@investment) %>_votes")
.html("<%= j render Budgets::Investments::VotesComponent.new(@investment) %>");

View File

@@ -21,7 +21,6 @@
<%= render "/budgets/investments/investment",
investment: investment,
investment_ids: @investment_ids,
investment_votes: @investment_votes,
ballot: @ballot %>
<% end %>

View File

@@ -26,7 +26,6 @@
<%= render "/budgets/investments/investment",
investment: investment,
investment_ids: @investment_ids,
investment_votes: @investment_votes,
ballot: @ballot %>
<% end %>

View File

@@ -2,6 +2,4 @@
<%= render "/shared/print" %>
<%= render "/budgets/investments/investment_show",
investment: @investment,
investment_votes: @investment_votes %>
<%= render "/budgets/investments/investment_show", investment: @investment %>

View File

@@ -1,4 +0,0 @@
$("#<%= dom_id(@investment) %>_votes").html("<%= j render("/budgets/investments/votes",
investment: @investment,
investment_votes: @investment_votes,
vote_url: namespaced_budget_investment_vote_path(@investment, value: "yes")) %>");

View File

@@ -0,0 +1,2 @@
$("#<%= dom_id(@investment) %>_votes")
.html("<%= j render Budgets::Investments::VotesComponent.new(@investment) %>");

View File

@@ -156,23 +156,26 @@ en:
investment:
add: Vote
already_added: You have already added this investment project
already_supported: You have already supported this investment project. Share it!
support_title: Support this project
support_label: "Support %{investment}"
confirm_group:
one: "You can only support investments in %{count} district. If you continue you cannot change the election of your district. Are you sure?"
other: "You can only support investments in %{count} districts. If you continue you cannot change the election of your district. Are you sure?"
supports:
one: 1 support
other: "%{count} supports"
zero: No supports
give_support: Support
header:
check_ballot: "Submit my ballot"
different_heading_assigned: "You have active votes in another heading: %{heading_link}"
change_ballot: "If you change your mind you can remove your votes in %{check_ballot} and start again."
check_ballot_link: "submit my ballot"
price: "Total budget"
votes:
already_supported: "You have already supported this investment project. Share it!"
confirm_group:
one: "You can only support investments in %{count} district. If you continue you cannot change the election of your district. Are you sure?"
other: "You can only support investments in %{count} districts. If you continue you cannot change the election of your district. Are you sure?"
remove_support: "Remove your support"
remove_support_label: "Remove your support to %{investment}"
support: "Support"
support_label: "Support %{investment}"
investments_list:
investment:
price: "Price"
@@ -185,7 +188,6 @@ en:
supports_info:
different: "You may support on as many different projects as you would like."
next: "Support the projects you would like to see move on to the next phase."
remember: "<strong>Remember!</strong> You can only cast your support <strong>once</strong> for each project and each support is <strong>irreversible</strong>."
scrolling: "Keep scrolling to see all ideas"
share: "You can share the projects you have supported on through social media and attract more attention and support to them!"
supported:

View File

@@ -14,6 +14,7 @@ en:
proposal_notification: "Your message has been sent correctly."
budget_investment: "Budget Investment created successfully."
signature_sheet: "Signature sheet created successfully"
support: "Investment supported successfully"
topic: "Topic created successfully."
valuator_group: "Valuator group created successfully"
save_changes:

View File

@@ -156,23 +156,26 @@ es:
investment:
add: Votar
already_added: Ya has añadido este proyecto de gasto
already_supported: Ya has apoyado este proyecto de gasto. ¡Compártelo!
support_title: Apoyar este proyecto
support_label: "Apoyar %{investment}"
confirm_group:
one: "Sólo puedes apoyar proyectos en %{count} distrito. Si sigues adelante no podrás cambiar la elección de este distrito. ¿Estás seguro/a?"
other: "Sólo puedes apoyar proyectos en %{count} distritos. Si sigues adelante no podrás cambiar la elección de este distrito. ¿Estás seguro/a?"
supports:
zero: Sin apoyos
one: 1 apoyo
other: "%{count} apoyos"
give_support: Apoyar
header:
check_ballot: Revisar y confirmar mis votos
different_heading_assigned: "Ya apoyaste proyectos de otra sección del presupuesto: %{heading_link}"
change_ballot: "Si cambias de opinión puedes borrar tus votos en %{check_ballot} y volver a empezar."
check_ballot_link: "revisar y confirmar mis votos"
price: "Presupuesto total"
votes:
already_supported: "Ya has apoyado este proyecto de gasto. ¡Compártelo!"
confirm_group:
one: "Sólo puedes apoyar proyectos en %{count} distrito. Si sigues adelante no podrás cambiar la elección de este distrito. ¿Estás seguro/a?"
other: "Sólo puedes apoyar proyectos en %{count} distritos. Si sigues adelante no podrás cambiar la elección de este distrito. ¿Estás seguro/a?"
remove_support: "Eliminar apoyo"
remove_support_label: "Eliminar tu apoyo a %{investment}"
support: "Apoyar"
support_label: "Apoyar %{investment}"
investments_list:
investment:
price: "Coste"
@@ -185,7 +188,6 @@ es:
supports_info:
different: "Puedes apoyar tantos proyectos diferentes como quieras."
next: "Apoya proyectos que te gustaría ver en la siguiente fase."
remember: "<strong>¡Recuerda!</strong> Solo puedes emitir tu apoyo <strong>una vez</strong> por cada proyecto y cada apoyo es <strong>irreversible</strong>."
scrolling: "Sigue desplazándote para ver todas las ideas"
share: "Puedes compartir los proyectos que has apoyado en las redes sociales y ¡atraer más atención y apoyos para ellos!"
supported:

View File

@@ -14,6 +14,7 @@ es:
proposal_notification: "Tu mensaje ha sido enviado correctamente."
budget_investment: "Proyecto de gasto creado correctamente."
signature_sheet: "Hoja de firmas creada correctamente"
support: "Proyecto de gasto apoyado correctamente"
topic: "Tema creado correctamente."
valuator_group: "Grupo de evaluadores creado correctamente"
save_changes:

View File

@@ -2,12 +2,13 @@ resources :budgets, only: [:show, :index] do
resources :groups, controller: "budgets/groups", only: [:show]
resources :investments, controller: "budgets/investments" do
member do
post :vote
put :flag
put :unflag
end
collection { get :suggest }
resources :votes, controller: "budgets/investments/votes", only: [:create, :destroy]
end
resource :ballot, only: :show, controller: "budgets/ballots" do

View File

@@ -39,8 +39,9 @@ namespace :management do
end
resources :investments, only: [:index, :new, :create, :show, :destroy], controller: "budgets/investments" do
post :vote, on: :member
get :print, on: :collection
resources :votes, controller: "budgets/investments/votes", only: [:create, :destroy]
end
end
end

View File

@@ -4,9 +4,7 @@ describe Budgets::Investments::VotesComponent, type: :component do
describe "vote link" do
context "when investment shows votes" do
let(:investment) { create(:budget_investment, title: "Renovate sidewalks in Main Street") }
let(:component) do
Budgets::Investments::VotesComponent.new(investment, investment_votes: [], vote_url: "/")
end
let(:component) { Budgets::Investments::VotesComponent.new(investment) }
before { allow(investment).to receive(:should_show_votes?).and_return(true) }
@@ -25,8 +23,21 @@ describe Budgets::Investments::VotesComponent, type: :component do
render_inline component
expect(page).to have_button count: 1, disabled: :all
expect(page).to have_button "Support", disabled: true
end
it "shows the button to remove support when users have supported the investment" do
user = create(:user)
user.up_votes(investment)
allow(controller).to receive(:current_user).and_return(user)
render_inline component
expect(page).to have_button count: 1, disabled: :all
expect(page).to have_button "Remove your support"
expect(page).to have_button "Remove your support to Renovate sidewalks in Main Street"
end
end
end
end

View File

@@ -248,9 +248,12 @@ describe Abilities::Common do
it { should_not be_able_to(:create, investment_in_selecting_budget) }
it { should_not be_able_to(:create, investment_in_balloting_budget) }
it { should be_able_to(:vote, investment_in_selecting_budget) }
it { should_not be_able_to(:vote, investment_in_accepting_budget) }
it { should_not be_able_to(:vote, investment_in_balloting_budget) }
it { should be_able_to(:create, user.votes.build(votable: investment_in_selecting_budget)) }
it { should_not be_able_to(:create, user.votes.build(votable: investment_in_accepting_budget)) }
it { should_not be_able_to(:create, user.votes.build(votable: investment_in_balloting_budget)) }
it { should be_able_to(:destroy, create(:vote, voter: user, votable: investment_in_selecting_budget)) }
it { should_not be_able_to(:destroy, create(:vote, voter: user, votable: investment_in_accepting_budget)) }
it { should_not be_able_to(:destroy, create(:vote, voter: user, votable: investment_in_balloting_budget)) }
it { should_not be_able_to(:destroy, investment_in_accepting_budget) }
it { should_not be_able_to(:destroy, investment_in_reviewing_budget) }

View File

@@ -432,6 +432,42 @@ describe "Budgets" do
expect(page).to have_content "It's time to support projects!"
expect(page).to have_content "So far you've supported 3 projects."
end
scenario "Show supports only if the support has not been removed" do
voter = create(:user, :level_two)
budget = create(:budget, phase: "selecting")
investment = create(:budget_investment, :selected, budget: budget)
login_as(voter)
visit budget_path(budget)
expect(page).to have_content "So far you've supported 0 projects."
visit budget_investment_path(budget, investment)
within("#budget_investment_#{investment.id}_votes") do
click_button "Support"
expect(page).to have_content "You have already supported this investment project."
end
visit budget_path(budget)
expect(page).to have_content "So far you've supported 1 project."
visit budget_investment_path(budget, investment)
within("#budget_investment_#{investment.id}_votes") do
click_button "Remove your support"
expect(page).to have_content "No supports"
end
visit budget_path(budget)
expect(page).to have_content "So far you've supported 0 projects."
end
end
context "In Drafting phase" do

View File

@@ -1177,6 +1177,48 @@ describe "Budget Investments" do
expect(page).to have_content "SUPPORTS"
end
end
scenario "Remove a support from show view" do
investment = create(:budget_investment, budget: budget)
login_as(author)
visit budget_investment_path(budget, investment)
within("aside") do
expect(page).to have_content "No supports"
click_button "Support"
expect(page).to have_content "1 support"
expect(page).to have_content "You have already supported this investment project."
click_button "Remove your support"
expect(page).to have_content "No supports"
expect(page).to have_button "Support"
end
end
scenario "Remove a support from index view" do
investment = create(:budget_investment, budget: budget)
login_as(author)
visit budget_investments_path(budget)
within("#budget_investment_#{investment.id}") do
expect(page).to have_content "No supports"
click_button "Support"
expect(page).to have_content "1 support"
expect(page).to have_content "You have already supported this investment project."
click_button "Remove your support"
expect(page).to have_content "No supports"
expect(page).to have_button "Support"
end
end
end
context "Evaluating Phase" do

View File

@@ -127,6 +127,8 @@ describe "Votes" do
"Share it!"
end
expect(page).to have_content "Investment supported successfully"
visit budget_investments_path(budget, heading_id: san_francisco.id)
within("#budget_investment_#{san_francisco_investment.id}") do

View File

@@ -329,9 +329,60 @@ describe "Budget Investments" do
expect(page).to have_content "1 support"
expect(page).to have_content "You have already supported this investment project. Share it!"
expect(page).to have_content "Investment supported successfully"
expect(page).to have_content "CONSUL\nMANAGEMENT"
end
scenario "Remove support on behalf of someone else in index view" do
create(:budget_investment, heading: heading)
login_managed_user(user)
login_as_manager(manager)
visit management_budget_investments_path(budget)
click_button "Support"
expect(page).to have_content "1 support"
expect(page).to have_content "You have already supported this investment project. Share it!"
expect(page).not_to have_button "Support"
click_button "Remove your support"
expect(page).to have_content "No supports"
expect(page).to have_button "Support"
expect(page).not_to have_button "Remove your support"
end
scenario "Remove support on behalf of someone else in show view" do
create(:budget_investment, heading: heading, title: "Don't support me!")
login_managed_user(user)
login_as_manager(manager)
visit management_budget_investments_path(budget)
click_link "Don't support me!"
expect(page).to have_css "h1", exact_text: "Don't support me!"
click_button "Support"
expect(page).to have_content "1 support"
expect(page).to have_content "You have already supported this investment project. Share it!"
expect(page).not_to have_button "Support"
click_button "Remove your support"
expect(page).to have_content "No supports"
expect(page).to have_button "Support"
expect(page).not_to have_button "Remove your support"
refresh
expect(page).to have_content "No supports"
expect(page).to have_button "Support"
expect(page).not_to have_button "Remove your support"
end
scenario "Should not allow unverified users to vote proposals" do
login_managed_user(create(:user))
create(:budget_investment, budget: budget)