diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 280dd0c4d..640adf299 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -77,6 +77,10 @@ class ApplicationController < ActionController::Base
@proposal_votes = current_user ? current_user.proposal_votes(proposals) : {}
end
+ def set_spending_proposal_votes(spending_proposals)
+ @spending_proposal_votes = current_user ? current_user.spending_proposal_votes(spending_proposals) : {}
+ end
+
def set_comment_flags(comments)
@comment_flags = current_user ? current_user.comment_flags(comments) : {}
end
diff --git a/app/controllers/spending_proposals_controller.rb b/app/controllers/spending_proposals_controller.rb
index 76f230575..20add5c24 100644
--- a/app/controllers/spending_proposals_controller.rb
+++ b/app/controllers/spending_proposals_controller.rb
@@ -9,6 +9,8 @@ class SpendingProposalsController < ApplicationController
feature_flag :spending_proposals
+ respond_to :html, :js
+
def index
end
@@ -16,6 +18,10 @@ class SpendingProposalsController < ApplicationController
@spending_proposal = SpendingProposal.new
end
+ def show
+ set_spending_proposal_votes(@spending_proposal)
+ end
+
def create
@spending_proposal = SpendingProposal.new(spending_proposal_params)
@spending_proposal.author = current_user
@@ -34,6 +40,12 @@ class SpendingProposalsController < ApplicationController
redirect_to user_path(current_user, filter: 'spending_proposals'), notice: t('flash.actions.destroy.spending_proposal')
end
+ def vote
+ @spending_proposal.register_vote(current_user, 'yes')
+ set_spending_proposal_votes(@spending_proposal)
+ end
+
+
private
def spending_proposal_params
diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb
index 92eacea52..2438feadb 100644
--- a/app/models/abilities/common.rb
+++ b/app/models/abilities/common.rb
@@ -43,6 +43,7 @@ module Abilities
if user.level_two_or_three_verified?
can :vote, Proposal
can :vote_featured, Proposal
+ can :vote, SpendingProposal
can :create, SpendingProposal
can :destroy, SpendingProposal, author_id: user.id
end
diff --git a/app/models/spending_proposal.rb b/app/models/spending_proposal.rb
index a084a800e..561e6ab52 100644
--- a/app/models/spending_proposal.rb
+++ b/app/models/spending_proposal.rb
@@ -4,6 +4,7 @@ class SpendingProposal < ActiveRecord::Base
include Taggable
apply_simple_captcha
+ acts_as_votable
belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id'
belongs_to :geozone
@@ -80,6 +81,10 @@ class SpendingProposal < ActiveRecord::Base
valuation_finished
end
+ def total_votes
+ cached_votes_up
+ end
+
def code
"#{id}" + (administrator.present? ? "-A#{administrator.id}" : "")
end
@@ -89,4 +94,14 @@ class SpendingProposal < ActiveRecord::Base
update(unfeasible_email_sent_at: Time.now)
end
+ def votable_by?(user)
+ user && user.level_two_or_three_verified?
+ end
+
+ def register_vote(user, vote_value)
+ if votable_by?(user)
+ vote_by(voter: user, vote: vote_value)
+ end
+ end
+
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 3b58fb607..bc4b0f120 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -83,6 +83,11 @@ class User < ActiveRecord::Base
voted.each_with_object({}) { |v, h| h[v.votable_id] = v.value }
end
+ def spending_proposal_votes(spending_proposals)
+ voted = votes.for_spending_proposals(spending_proposals)
+ 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 }
diff --git a/app/views/_votes.html.erb/_form.html.erb b/app/views/_votes.html.erb/_form.html.erb
new file mode 100644
index 000000000..0d6ef0402
--- /dev/null
+++ b/app/views/_votes.html.erb/_form.html.erb
@@ -0,0 +1,51 @@
+<%= form_for(@spending_proposal, url: form_url) do |f| %>
+ <%= render 'shared/errors', resource: @spending_proposal %>
+
+
+
+ <%= f.label :title, t("spending_proposals.form.title") %>
+ <%= f.text_field :title, maxlength: SpendingProposal.title_max_length, placeholder: t("spending_proposals.form.title"), label: false %>
+
+
+
+ <%= f.label :description, t("spending_proposals.form.description") %>
+ <%= f.cktext_area :description, maxlength: SpendingProposal.description_max_length, ckeditor: { language: I18n.locale }, label: false %>
+
+
+
+ <%= f.label :external_url, t("spending_proposals.form.external_url") %>
+ <%= f.text_field :external_url, placeholder: t("spending_proposals.form.external_url"), label: false %>
+
+
+
+ <%= f.label :geozone_id, t("spending_proposals.form.geozone") %>
+ <%= f.select :geozone_id, geozone_select_options, {include_blank: t("geozones.none"), label: false} %>
+
+
+
+ <%= f.label :association_name, t("spending_proposals.form.association_name_label") %>
+ <%= f.text_field :association_name, placeholder: t("spending_proposals.form.association_name"), label: false %>
+
+
+
+ <% if @spending_proposal.new_record? %>
+ <%= f.label :terms_of_service do %>
+ <%= f.check_box :terms_of_service, title: t('form.accept_terms_title'), label: false %>
+
+ <%= t("form.accept_terms",
+ policy: link_to(t("form.policy"), "/privacy", target: "blank"),
+ conditions: link_to(t("form.conditions"), "/conditions", target: "blank")).html_safe %>
+
+ <% end %>
+ <% end %>
+
+
+
+ <%= f.simple_captcha input_html: { required: false } %>
+
+
+
+ <%= f.submit(class: "button", value: t("spending_proposals.form.submit_buttons.#{action_name}")) %>
+
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/_votes.html.erb/index.html.erb b/app/views/_votes.html.erb/index.html.erb
new file mode 100644
index 000000000..00a1b6442
--- /dev/null
+++ b/app/views/_votes.html.erb/index.html.erb
@@ -0,0 +1,16 @@
+<% provide :title do %><%= t('spending_proposals.index.title') %><% end %>
+
+
+
+
<%= t('spending_proposals.index.title') %>
+
+
<%= t('spending_proposals.index.text_html') %>
+
+ <% if can? :create, SpendingProposal %>
+ <%= link_to t('spending_proposals.index.create_link'), new_spending_proposal_path, class: 'button' %>
+ <% else %>
+
<%= t('spending_proposals.index.verified_only', verify_account: link_to(t('spending_proposals.index.verify_account'), verification_path)).html_safe %>
+ <% end %>
+
+
+
\ No newline at end of file
diff --git a/app/views/_votes.html.erb/new.html.erb b/app/views/_votes.html.erb/new.html.erb
new file mode 100644
index 000000000..a8d35338f
--- /dev/null
+++ b/app/views/_votes.html.erb/new.html.erb
@@ -0,0 +1,27 @@
+
+
+
+ <%= link_to spending_proposals_path, class: "back" do %>
+
+ <%= t("spending_proposals.new.back_link") %>
+ <% end %>
+
<%= t("spending_proposals.new.start_new") %>
+
+ <%= link_to "/spending_proposals_info", title: t('shared.target_blank_html'), target: "_blank" do %>
+ <%= t("spending_proposals.new.more_info")%>
+ <% end %>
+
+ <%= render "spending_proposals/form", form_url: spending_proposals_url %>
+
+
+
+
+
<%= t("spending_proposals.new.recommendations_title") %>
+
+ - <%= t("spending_proposals.new.recommendation_one") %>
+ - <%= t("spending_proposals.new.recommendation_two") %>
+ - <%= t("spending_proposals.new.recommendation_three") %>
+
+
+
+
\ No newline at end of file
diff --git a/app/views/_votes.html.erb/show.html.erb b/app/views/_votes.html.erb/show.html.erb
new file mode 100644
index 000000000..1aaa3d805
--- /dev/null
+++ b/app/views/_votes.html.erb/show.html.erb
@@ -0,0 +1,34 @@
+<% provide :title do %><%= @spending_proposal.title %><% end %>
+
+
+
+
+
+
<%= @spending_proposal.title %>
+
+
+ <%= render '/shared/author_info', resource: @spending_proposal %>
+
+ •
+ <%= l @spending_proposal.created_at.to_date %>
+ •
+ <%= geozone_name(@spending_proposal) %>
+
+
+ <%= safe_html_with_links @spending_proposal.description.html_safe %>
+
+ <% if @spending_proposal.external_url.present? %>
+
+ <%= text_with_links @spending_proposal.external_url %>
+
+ <% end %>
+
+
+
+
+ <%= render 'votes',
+ { spending_proposal: @spending_proposal, vote_url: vote_spending_proposal_path(@spending_proposal, value: 'yes') } %>
+
+
+
+
\ No newline at end of file
diff --git a/app/views/spending_proposals/_votes.html.erb b/app/views/spending_proposals/_votes.html.erb
new file mode 100644
index 000000000..652df8954
--- /dev/null
+++ b/app/views/spending_proposals/_votes.html.erb
@@ -0,0 +1,47 @@
+
+
+
+ <%= t("spending_proposals.spending_proposal.supports", count: spending_proposal.total_votes) %>
+
+
+
+ <% if voted_for?(@spending_proposal_votes, spending_proposal) %>
+
+ <%= t("spending_proposals.spending_proposal.already_supported") %>
+
+ <% else %>
+ <%= link_to vote_url,
+ class: "button button-support small expanded",
+ title: t('spending_proposals.spending_proposal.support_title'), method: "post", remote: true do %>
+ <%= t("spending_proposals.spending_proposal.support") %>
+ <% end %>
+ <% end %>
+
+
+ <% if user_signed_in? && current_user.organization? %>
+
+
+ <%= t("votes.organizations") %>
+
+
+ <% elsif user_signed_in? && !spending_proposal.votable_by?(current_user)%>
+
+
+ <%= t("votes.verified_only",
+ verify_account: link_to(t("votes.verify_account"), verification_path )).html_safe %>
+
+
+ <% elsif !user_signed_in? %>
+
+ <%= t("votes.unauthenticated",
+ signin: link_to(t("votes.signin"), new_user_session_path),
+ signup: link_to(t("votes.signup"), new_user_registration_path)).html_safe %>
+
+ <% end %>
+
+ <% if voted_for?(@spending_proposal_votes, spending_proposal) && setting['twitter_handle'] %>
+
+ <%= social_share_button_tag(spending_proposal.title, url: spending_proposal_url(spending_proposal), via: setting['twitter_handle']) %>
+
+ <% end %>
+
diff --git a/app/views/spending_proposals/show.html.erb b/app/views/spending_proposals/show.html.erb
index 987118a6a..1aaa3d805 100644
--- a/app/views/spending_proposals/show.html.erb
+++ b/app/views/spending_proposals/show.html.erb
@@ -25,5 +25,10 @@
+
+ <%= render 'votes',
+ { spending_proposal: @spending_proposal, vote_url: vote_spending_proposal_path(@spending_proposal, value: 'yes') } %>
+
+
\ No newline at end of file
diff --git a/app/views/spending_proposals/vote.js.erb b/app/views/spending_proposals/vote.js.erb
new file mode 100644
index 000000000..4fa350aac
--- /dev/null
+++ b/app/views/spending_proposals/vote.js.erb
@@ -0,0 +1 @@
+$("#<%= dom_id(@spending_proposal) %>_votes").html('<%= j render("spending_proposals/votes", spending_proposal: @spending_proposal) %>');
\ No newline at end of file
diff --git a/config/initializers/vote_extensions.rb b/config/initializers/vote_extensions.rb
index 50fb60c9f..345cb8f01 100644
--- a/config/initializers/vote_extensions.rb
+++ b/config/initializers/vote_extensions.rb
@@ -7,6 +7,10 @@ ActsAsVotable::Vote.class_eval do
where(votable_type: 'Proposal', votable_id: proposals)
end
+ def self.for_spending_proposals(spending_proposals)
+ where(votable_type: 'SpendingProposal', votable_id: spending_proposals)
+ end
+
def value
vote_flag
end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 1f752a9f2..08e289109 100755
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -426,6 +426,38 @@ en:
recommendations_title: How to create a spending proposal
start_new: Create spending proposal
wrong_price_format: Only integer numbers
+ spending_proposal:
+ already_supported: You have already supported this. Share it!
+ comments:
+ one: 1 comment
+ other: "%{count} comments"
+ zero: No comments
+ proposal: Proposal
+ reason_for_supports_necessary: 2% of Census
+ support: Support
+ support_title: Support this proposal
+ supports:
+ one: 1 support
+ other: "%{count} supports"
+ zero: No supports
+ supports_necessary: "%{number} supports needed"
+ total_percent: 100%
+ show:
+ author_deleted: User deleted
+ back_link: Go back
+ code: 'Proposal code:'
+ comments:
+ one: 1 comment
+ other: "%{count} comments"
+ zero: No comments
+ comments_title: Comments
+ edit_proposal_link: Edit
+ flag: This proposal has been flagged as inappropriate by several users.
+ login_to_comment: You must %{signin} or %{signup} to leave a comment.
+ share: Share
+ update:
+ form:
+ submit_button: Save changes
stats:
index:
visits: Visits
diff --git a/config/routes.rb b/config/routes.rb
index 680f657ab..0d704be04 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -66,7 +66,11 @@ Rails.application.routes.draw do
end
scope '/participatory_budget' do
- resources :spending_proposals, only: [:index, :new, :create, :show, :destroy], path: 'investment_projects'
+ resources :spending_proposals, only: [:index, :new, :create, :show, :destroy], path: 'investment_projects' do
+ member do
+ post :vote
+ end
+ end
end
resources :stats, only: [:index]
diff --git a/db/migrate/20160329115418_add_votes_up_to_spending_proposals.rb b/db/migrate/20160329115418_add_votes_up_to_spending_proposals.rb
new file mode 100644
index 000000000..aa9397c2e
--- /dev/null
+++ b/db/migrate/20160329115418_add_votes_up_to_spending_proposals.rb
@@ -0,0 +1,5 @@
+class AddVotesUpToSpendingProposals < ActiveRecord::Migration
+ def change
+ add_column :spending_proposals, :cached_votes_up, :integer
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c6c5faa19..b5daf6abd 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160328152843) do
+ActiveRecord::Schema.define(version: 20160329115418) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -312,6 +312,7 @@ ActiveRecord::Schema.define(version: 20160328152843) do
t.integer "price_first_year", limit: 8
t.string "time_scope"
t.datetime "unfeasible_email_sent_at"
+ t.integer "cached_votes_up"
end
add_index "spending_proposals", ["author_id"], name: "index_spending_proposals_on_author_id", using: :btree
diff --git a/spec/features/votes_spec.rb b/spec/features/votes_spec.rb
index 3ccf6bcdb..c8164de2c 100644
--- a/spec/features/votes_spec.rb
+++ b/spec/features/votes_spec.rb
@@ -313,7 +313,7 @@ feature 'Votes' do
scenario 'Not logged user trying to vote comments in proposals', :js do
proposal = create(:proposal)
comment = create(:comment, commentable: proposal)
-
+
visit comment_path(comment)
within("#comment_#{comment.id}_reply") do
find("div.votes").hover
@@ -361,4 +361,64 @@ feature 'Votes' do
expect_message_only_verified_can_vote_proposals
end
end
+
+ feature 'Spending Proposals' do
+ background { login_as(@manuela) }
+
+ xscenario "Index shows user votes on proposals" do
+ proposal1 = create(:proposal)
+ proposal2 = create(:proposal)
+ proposal3 = create(:proposal)
+ create(:vote, voter: @manuela, votable: proposal1, vote_flag: true)
+
+ visit proposals_path
+
+ within("#proposals") do
+ within("#proposal_#{proposal1.id}_votes") do
+ expect(page).to have_content "You have already supported this proposal. Share it!"
+ end
+
+ within("#proposal_#{proposal2.id}_votes") do
+ expect(page).to_not have_content "You have already supported this proposal. Share it!"
+ end
+
+ within("#proposal_#{proposal3.id}_votes") do
+ expect(page).to_not have_content "You have already supported this proposal. Share it!"
+ end
+ end
+ end
+
+ feature 'Single spending proposal' do
+ background do
+ @proposal = create(:spending_proposal)
+ end
+
+ scenario 'Show no votes' do
+ visit spending_proposal_path(@proposal)
+ expect(page).to have_content "No supports"
+ end
+
+ scenario 'Trying to vote multiple times', :js do
+ visit spending_proposal_path(@proposal)
+
+ within('.supports') do
+ find('.in-favor a').click
+ expect(page).to have_content "1 support"
+
+ expect(page).to_not have_selector ".in-favor a"
+ end
+ end
+
+ scenario 'Create from proposal show', :focus, :js do
+ visit spending_proposal_path(@proposal)
+
+ within('.supports') do
+ find('.in-favor a').click
+
+ expect(page).to have_content "1 support"
+ expect(page).to have_content "You have already supported this. Share it!"
+ end
+ end
+ end
+ end
end