Merge pull request #821 from consul/spending_proposals
Spending proposals
This commit is contained in:
23
app/controllers/admin/spending_proposals_controller.rb
Normal file
23
app/controllers/admin/spending_proposals_controller.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
class Admin::SpendingProposalsController < Admin::BaseController
|
||||
has_filters %w{unresolved accepted rejected}, only: :index
|
||||
|
||||
load_and_authorize_resource
|
||||
|
||||
def index
|
||||
@spending_proposals = @spending_proposals.includes([:geozone]).send(@current_filter).order(created_at: :desc).page(params[:page])
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def accept
|
||||
@spending_proposal.accept
|
||||
redirect_to request.query_parameters.merge(action: :index)
|
||||
end
|
||||
|
||||
def reject
|
||||
@spending_proposal.reject
|
||||
redirect_to request.query_parameters.merge(action: :index)
|
||||
end
|
||||
|
||||
end
|
||||
@@ -30,4 +30,4 @@ class Moderation::UsersController < Moderation::BaseController
|
||||
Activity.log(current_user, :block, @user)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
32
app/controllers/spending_proposals_controller.rb
Normal file
32
app/controllers/spending_proposals_controller.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
class SpendingProposalsController < ApplicationController
|
||||
before_action :authenticate_user!, except: [:index]
|
||||
|
||||
load_and_authorize_resource
|
||||
|
||||
def index
|
||||
end
|
||||
|
||||
def new
|
||||
@spending_proposal = SpendingProposal.new
|
||||
@featured_tags = ActsAsTaggableOn::Tag.where(featured: true)
|
||||
end
|
||||
|
||||
def create
|
||||
@spending_proposal = SpendingProposal.new(spending_proposal_params)
|
||||
@spending_proposal.author = current_user
|
||||
|
||||
if @spending_proposal.save_with_captcha
|
||||
redirect_to spending_proposals_path, notice: t('flash.actions.create.notice', resource_name: t("activerecord.models.spending_proposal", count: 1))
|
||||
else
|
||||
@featured_tags = ActsAsTaggableOn::Tag.where(featured: true)
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def spending_proposal_params
|
||||
params.require(:spending_proposal).permit(:title, :description, :external_url, :geozone_id, :terms_of_service, :captcha, :captcha_key)
|
||||
end
|
||||
|
||||
end
|
||||
11
app/helpers/geozones_helper.rb
Normal file
11
app/helpers/geozones_helper.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
module GeozonesHelper
|
||||
|
||||
def geozone_name(geozonable)
|
||||
geozonable.geozone ? geozonable.geozone.name : t("geozones.none")
|
||||
end
|
||||
|
||||
def geozone_select_options
|
||||
Geozone.all.order(name: :asc).collect { |g| [ g.name, g.id ] }
|
||||
end
|
||||
|
||||
end
|
||||
@@ -34,6 +34,8 @@ module Abilities
|
||||
can [:search, :create, :index, :destroy], ::Moderator
|
||||
|
||||
can :manage, Annotation
|
||||
|
||||
can :manage, SpendingProposal
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,6 +17,8 @@ module Abilities
|
||||
proposal.editable_by?(user)
|
||||
end
|
||||
|
||||
can :read, SpendingProposal
|
||||
|
||||
can :create, Comment
|
||||
can :create, Debate
|
||||
can :create, Proposal
|
||||
@@ -38,6 +40,7 @@ module Abilities
|
||||
if user.level_two_or_three_verified?
|
||||
can :vote, Proposal
|
||||
can :vote_featured, Proposal
|
||||
can :create, SpendingProposal
|
||||
end
|
||||
|
||||
can :create, Annotation
|
||||
|
||||
@@ -6,6 +6,7 @@ module Abilities
|
||||
can :read, Debate
|
||||
can :read, Proposal
|
||||
can :read, Comment
|
||||
can :read, SpendingProposal
|
||||
can :read, Legislation
|
||||
can :read, User
|
||||
can [:search, :read], Annotation
|
||||
|
||||
@@ -13,7 +13,7 @@ module Sanitizable
|
||||
end
|
||||
|
||||
def sanitize_tag_list
|
||||
self.tag_list = TagSanitizer.new.sanitize_tag_list(self.tag_list)
|
||||
self.tag_list = TagSanitizer.new.sanitize_tag_list(self.tag_list) if self.class.taggable?
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
3
app/models/geozone.rb
Normal file
3
app/models/geozone.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class Geozone < ActiveRecord::Base
|
||||
validates :name, presence: true
|
||||
end
|
||||
44
app/models/spending_proposal.rb
Normal file
44
app/models/spending_proposal.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
class SpendingProposal < ActiveRecord::Base
|
||||
include Measurable
|
||||
include Sanitizable
|
||||
|
||||
apply_simple_captcha
|
||||
|
||||
RESOLUTIONS = ["accepted", "rejected"]
|
||||
|
||||
belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id'
|
||||
belongs_to :geozone
|
||||
|
||||
validates :title, presence: true
|
||||
validates :author, presence: true
|
||||
validates :description, presence: true
|
||||
|
||||
validates :title, length: { in: 4..SpendingProposal.title_max_length }
|
||||
validates :description, length: { maximum: SpendingProposal.description_max_length }
|
||||
validates :resolution, inclusion: { in: RESOLUTIONS, allow_nil: true }
|
||||
validates :terms_of_service, acceptance: { allow_nil: false }, on: :create
|
||||
|
||||
scope :accepted, -> { where(resolution: "accepted") }
|
||||
scope :rejected, -> { where(resolution: "rejected") }
|
||||
scope :unresolved, -> { where(resolution: nil) }
|
||||
|
||||
def accept
|
||||
update_attribute(:resolution, "accepted")
|
||||
end
|
||||
|
||||
def reject
|
||||
update_attribute(:resolution, "rejected")
|
||||
end
|
||||
|
||||
def accepted?
|
||||
resolution == "accepted"
|
||||
end
|
||||
|
||||
def rejected?
|
||||
resolution == "rejected"
|
||||
end
|
||||
|
||||
def unresolved?
|
||||
resolution.blank?
|
||||
end
|
||||
end
|
||||
@@ -32,6 +32,13 @@
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
<li <%= "class=active" if controller_name == "spending_proposals" %>>
|
||||
<%= link_to admin_spending_proposals_path do %>
|
||||
<i class="icon-proposals"></i>
|
||||
<%= t("admin.menu.spending_proposals") %>
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
<li <%= "class=active" if controller_name == "users" %>>
|
||||
<%= link_to admin_users_path do %>
|
||||
<i class="icon-eye"></i>
|
||||
|
||||
36
app/views/admin/spending_proposals/index.html.erb
Normal file
36
app/views/admin/spending_proposals/index.html.erb
Normal file
@@ -0,0 +1,36 @@
|
||||
<h2><%= t("admin.spending_proposals.index.title") %></h2>
|
||||
|
||||
<%= render 'shared/filter_subnav', i18n_namespace: "admin.spending_proposals.index" %>
|
||||
|
||||
<h3><%= page_entries_info @spending_proposals %></h3>
|
||||
|
||||
<table>
|
||||
<% @spending_proposals.each do |spending_proposal| %>
|
||||
<tr id="<%= dom_id(spending_proposal) %>">
|
||||
<td>
|
||||
<strong><%= link_to spending_proposal.title, admin_spending_proposal_path(spending_proposal) %></strong>
|
||||
</td>
|
||||
<td>
|
||||
<%= geozone_name(spending_proposal) %>
|
||||
</td>
|
||||
<td>
|
||||
<% unless spending_proposal.accepted? %>
|
||||
<%= link_to t("admin.spending_proposals.actions.accept"),
|
||||
accept_admin_spending_proposal_path(spending_proposal, request.query_parameters),
|
||||
method: :put,
|
||||
data: { confirm: t("admin.actions.confirm") },
|
||||
class: "button radius tiny success no-margin" %>
|
||||
<% end %>
|
||||
<% unless spending_proposal.rejected? %>
|
||||
<%= link_to t("admin.spending_proposals.actions.reject"),
|
||||
reject_admin_spending_proposal_path(spending_proposal, request.query_parameters),
|
||||
method: :put,
|
||||
data: { confirm: t("admin.actions.confirm") },
|
||||
class: "button radius tiny warning right" %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
|
||||
<%= paginate @spending_proposals %>
|
||||
28
app/views/admin/spending_proposals/show.html.erb
Normal file
28
app/views/admin/spending_proposals/show.html.erb
Normal file
@@ -0,0 +1,28 @@
|
||||
<h2><%= @spending_proposal.title %></h2>
|
||||
|
||||
<%= safe_html_with_links @spending_proposal.description.html_safe %>
|
||||
|
||||
<% if @spending_proposal.external_url.present? %>
|
||||
<p><%= text_with_links @spending_proposal.external_url %></p>
|
||||
<% end %>
|
||||
|
||||
<p><%= t("admin.spending_proposals.show.by") %>: <%= link_to @spending_proposal.author.name, admin_user_path(@spending_proposal.author) %></p>
|
||||
<p><%= t("admin.spending_proposals.show.geozone") %>: <%= geozone_name(@spending_proposal) %></p>
|
||||
<p><%= l @spending_proposal.created_at, format: :datetime %></p>
|
||||
|
||||
<p>
|
||||
<% unless @spending_proposal.accepted? %>
|
||||
<%= link_to t("admin.spending_proposals.actions.accept"),
|
||||
accept_admin_spending_proposal_path(@spending_proposal),
|
||||
method: :put,
|
||||
data: { confirm: t("admin.actions.confirm") },
|
||||
class: "button radius tiny success no-margin" %>
|
||||
<% end %>
|
||||
<% unless @spending_proposal.rejected? %>
|
||||
<%= link_to t("admin.spending_proposals.actions.reject"),
|
||||
reject_admin_spending_proposal_path(@spending_proposal),
|
||||
method: :put,
|
||||
data: { confirm: t("admin.actions.confirm") },
|
||||
class: "button radius tiny warning" %>
|
||||
<% end %>
|
||||
</p>
|
||||
@@ -33,14 +33,16 @@
|
||||
</section>
|
||||
|
||||
<section class="subnavigation row">
|
||||
|
||||
<div class="small-12 medium-9 column">
|
||||
<%= link_to t("layouts.header.debates"), debates_path, class: ("active" if current_page?(controller: "/debates")) %>
|
||||
<%= link_to t("layouts.header.proposals"), proposals_path, class: ("active" if current_page?(controller: "/proposals")) %>
|
||||
<%= link_to t("layouts.header.more_information"), page_path('more_information'), class: ("active" if current_page?("/more_information")) %>
|
||||
<%= link_to t("layouts.header.external_link_blog_url"), target: "_blank" do %>
|
||||
<%= t("layouts.header.external_link_blog") %>
|
||||
<small><i class="icon-external"></i></small>
|
||||
<% end %>
|
||||
<%= link_to t("layouts.header.debates"), debates_path, class: ("active" if controller_name == "debates") %>
|
||||
<%= link_to t("layouts.header.proposals"), proposals_path, class: ("active" if controller_name == "proposals") %>
|
||||
<%= link_to t("layouts.header.spending_proposals"), spending_proposals_path, class: ("active" if controller_name == "spending_proposals") %>
|
||||
<%= link_to t("layouts.header.more_information"), page_path('more_information'), class: ("active" if current_page?("/more_information")) %>
|
||||
<%= link_to t("layouts.header.external_link_blog_url"), target: "_blank" do %>
|
||||
<%= t("layouts.header.external_link_blog") %>
|
||||
<small><i class="icon-external"></i></small>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="small-12 medium-3 column">
|
||||
<%= yield :header_addon %>
|
||||
|
||||
@@ -29,6 +29,12 @@
|
||||
<% end %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to page_path('spending_proposals_info') do %>
|
||||
<%= t('pages.more_information.titles.spending_proposals_info') %>
|
||||
<span><%= t('pages.more_information.description.spending_proposals_info') %></span>
|
||||
<% end %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to page_path('participation_world') do %>
|
||||
<%= t('pages.more_information.titles.participation_world') %>
|
||||
<span><%= t('pages.more_information.description.participation_world') %></span>
|
||||
|
||||
20
app/views/pages/spending_proposals_info.html.erb
Normal file
20
app/views/pages/spending_proposals_info.html.erb
Normal file
@@ -0,0 +1,20 @@
|
||||
<div class="page row-full">
|
||||
<div class="row">
|
||||
<div class="menu small-12 medium-3 column">
|
||||
<i class="icon-angle-left left"></i>
|
||||
<%= link_to t("spending_proposals.new.back_link"), "/more_information", class: 'left back' %>
|
||||
<ul class="clear">
|
||||
<li>
|
||||
<a href="#i">Explicación detallada del proceso</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="text small-12 medium-9 column">
|
||||
<h1>¿Cómo funcionan los presupuestos ciudadanos?</h1>
|
||||
|
||||
<h2 id="i">Explicación detallada del proceso</h2>
|
||||
<p>Próximamente se podrá encontrar aquí una descripción del proceso de participación ciudadana en los presupuestos.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,6 +79,4 @@
|
||||
<%= f.submit(class: "button radius", value: t("proposals.#{action_name}.form.submit_button")) %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
<% end %>
|
||||
@@ -8,7 +8,7 @@
|
||||
<% if local_assigns[:message].present? %>
|
||||
<%= message %>
|
||||
<% else %>
|
||||
<%= t("form.not_saved", resource: t("form.#{resource.class.to_s.downcase}")) %>
|
||||
<%= t("form.not_saved", resource: t("form.#{resource.class.to_s.underscore}")) %>
|
||||
<% end %>
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
46
app/views/spending_proposals/_form.html.erb
Normal file
46
app/views/spending_proposals/_form.html.erb
Normal file
@@ -0,0 +1,46 @@
|
||||
<%= form_for(@spending_proposal, url: form_url) do |f| %>
|
||||
<%= render 'shared/errors', resource: @spending_proposal %>
|
||||
|
||||
<div class="row">
|
||||
<div class="small-12 column">
|
||||
<%= 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 %>
|
||||
</div>
|
||||
|
||||
<div class="ckeditor small-12 column">
|
||||
<%= f.label :description, t("spending_proposals.form.description") %>
|
||||
<%= f.cktext_area :description, maxlength: SpendingProposal.description_max_length, ckeditor: { language: I18n.locale }, label: false %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 column">
|
||||
<%= f.label :external_url, t("spending_proposals.form.external_url") %>
|
||||
<%= f.text_field :external_url, placeholder: t("spending_proposals.form.external_url"), label: false %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 column">
|
||||
<%= f.label :geozone_id, t("spending_proposals.form.geozone") %>
|
||||
<%= f.select :geozone_id, geozone_select_options, {include_blank: t("geozones.none"), label: false} %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 column">
|
||||
<% if @spending_proposal.new_record? %>
|
||||
<%= f.label :terms_of_service do %>
|
||||
<%= f.check_box :terms_of_service, label: false %>
|
||||
<span class="checkbox">
|
||||
<%= t("form.accept_terms",
|
||||
policy: link_to(t("form.policy"), "/privacy", target: "blank"),
|
||||
conditions: link_to(t("form.conditions"), "/conditions", target: "blank")).html_safe %>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 column">
|
||||
<%= f.simple_captcha input_html: { required: false } %>
|
||||
</div>
|
||||
|
||||
<div class="actions small-12 column">
|
||||
<%= f.submit(class: "button radius", value: t("spending_proposals.form.submit_buttons.#{action_name}")) %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
16
app/views/spending_proposals/index.html.erb
Normal file
16
app/views/spending_proposals/index.html.erb
Normal file
@@ -0,0 +1,16 @@
|
||||
<% provide :title do %><%= t('spending_proposals.index.title') %><% end %>
|
||||
<div class="page row-full">
|
||||
<div class="row">
|
||||
<div class="more-information text small-12 medium-8 column">
|
||||
<h1><%= t('spending_proposals.index.title') %></h1>
|
||||
|
||||
<p><%= t('spending_proposals.index.text') %></p>
|
||||
|
||||
<% if can? :create, SpendingProposal %>
|
||||
<%= link_to t('spending_proposals.index.create_link'), new_spending_proposal_path, class: 'button radius' %>
|
||||
<% else %>
|
||||
<p><%= t('spending_proposals.index.verified_only', verify_account: link_to(t('spending_proposals.index.verify_account'), verification_path)).html_safe %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
27
app/views/spending_proposals/new.html.erb
Normal file
27
app/views/spending_proposals/new.html.erb
Normal file
@@ -0,0 +1,27 @@
|
||||
<div class="spending-proposal-new row">
|
||||
|
||||
<div class="small-12 medium-9 column">
|
||||
<%= link_to spending_proposals_path, class: "left back" do %>
|
||||
<i class="icon-angle-left left"></i>
|
||||
<%= t("spending_proposals.new.back_link") %>
|
||||
<% end %>
|
||||
<h1><%= t("spending_proposals.new.start_new") %></h1>
|
||||
<div class="alert-box info radius">
|
||||
<%= link_to "/spending_proposals_info", target: "_blank" do %>
|
||||
<%= t("spending_proposals.new.more_info")%>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= render "spending_proposals/form", form_url: spending_proposals_url %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-3 column">
|
||||
<i class="icon-spending-proposals right"></i>
|
||||
<h2><%= t("spending_proposals.new.recommendations_title") %></h2>
|
||||
<ul class="recommendations">
|
||||
<li><%= t("spending_proposals.new.recommendation_one") %></li>
|
||||
<li><%= t("spending_proposals.new.recommendation_two") %></li>
|
||||
<li><%= t("spending_proposals.new.recommendation_three") %></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -109,6 +109,7 @@ ignore_unused:
|
||||
- 'admin.comments.index.filter*'
|
||||
- 'admin.debates.index.filter*'
|
||||
- 'admin.proposals.index.filter*'
|
||||
- 'admin.spending_proposals.index.filter*'
|
||||
- 'admin.organizations.index.filter*'
|
||||
- 'admin.users.index.filter*'
|
||||
- 'admin.activity.show.filter*'
|
||||
|
||||
@@ -37,6 +37,9 @@ en:
|
||||
proposal:
|
||||
one: "Citizen proposal"
|
||||
other: "Citizen proposals"
|
||||
spending_proposal:
|
||||
one: "Spending proposal"
|
||||
other: "Spending proposals"
|
||||
attributes:
|
||||
comment:
|
||||
body: "Comment"
|
||||
|
||||
@@ -37,6 +37,9 @@ es:
|
||||
proposal:
|
||||
one: "Propuesta ciudadana"
|
||||
other: "Propuestas ciudadanas"
|
||||
spending_proposal:
|
||||
one: "Propuesta de gasto"
|
||||
other: "Propuestas de gasto"
|
||||
attributes:
|
||||
comment:
|
||||
body: "Comentario"
|
||||
|
||||
@@ -16,6 +16,7 @@ en:
|
||||
hidden_debates: "Hidden debates"
|
||||
hidden_comments: "Hidden comments"
|
||||
hidden_users: "Hidden users"
|
||||
spending_proposals: "Spending proposals"
|
||||
incomplete_verifications: "Incomplete verifications"
|
||||
organizations: "Organisations"
|
||||
officials: "Officials"
|
||||
@@ -91,6 +92,20 @@ en:
|
||||
all: "All"
|
||||
with_confirmed_hide: "Confirmed"
|
||||
without_confirmed_hide: "Pending"
|
||||
spending_proposals:
|
||||
actions:
|
||||
accept: Accept
|
||||
reject: Reject
|
||||
index:
|
||||
title: "Spending proposals for participatory budgeting"
|
||||
filter: "Filter"
|
||||
filters:
|
||||
unresolved: "Unresolved"
|
||||
accepted: "Accepted"
|
||||
rejected: "Rejected"
|
||||
show:
|
||||
geozone: "Scope"
|
||||
by: "Sent by"
|
||||
users:
|
||||
index:
|
||||
title: "Hidden users"
|
||||
|
||||
@@ -16,6 +16,7 @@ es:
|
||||
hidden_debates: "Debates ocultos"
|
||||
hidden_comments: "Comentarios ocultos"
|
||||
hidden_users: "Usuarios bloqueados"
|
||||
spending_proposals: "Propuestas de gasto"
|
||||
incomplete_verifications: "Verificaciones incompletas"
|
||||
organizations: "Organizaciones"
|
||||
officials: "Cargos públicos"
|
||||
@@ -91,6 +92,20 @@ es:
|
||||
all: "Todas"
|
||||
with_confirmed_hide: "Confirmadas"
|
||||
without_confirmed_hide: "Pendientes"
|
||||
spending_proposals:
|
||||
actions:
|
||||
accept: Aceptar
|
||||
reject: Rechazar
|
||||
index:
|
||||
title: "Propuestas de gasto para presupuestos participativos"
|
||||
filter: "Filtro"
|
||||
filters:
|
||||
unresolved: "Sin resolver"
|
||||
accepted: "Aceptadas"
|
||||
rejected: "Rechazadas"
|
||||
show:
|
||||
geozone: "Ámbito"
|
||||
by: "Enviada por"
|
||||
users:
|
||||
index:
|
||||
title: "Usuarios bloqueados"
|
||||
|
||||
@@ -29,6 +29,7 @@ en:
|
||||
more_information: "More information"
|
||||
debates: "Debates"
|
||||
proposals: "Proposals"
|
||||
spending_proposals: "Spending proposals"
|
||||
new_notifications:
|
||||
one: "You have a new notification"
|
||||
other: "You have %{count} new notifications"
|
||||
@@ -75,6 +76,7 @@ en:
|
||||
user: "Account"
|
||||
debate: "Debate"
|
||||
proposal: "Proposal"
|
||||
spending_proposal: "Spending proposal"
|
||||
verification::sms: "Telephone"
|
||||
verification::letter: "the verification"
|
||||
application:
|
||||
@@ -244,6 +246,31 @@ en:
|
||||
update:
|
||||
form:
|
||||
submit_button: "Save changes"
|
||||
spending_proposals:
|
||||
index:
|
||||
title: "Participatory budgeting"
|
||||
text: "Here you can send spending proposals to be considered in the frame of the annual participatory budgeting."
|
||||
create_link: "Create spending proposal"
|
||||
verified_only: "Only verified users can create spending proposals, %{verify_account}."
|
||||
verify_account: "verify your account"
|
||||
new:
|
||||
back_link: Back
|
||||
start_new: "Create spending proposal"
|
||||
more_info: "How do participatory budgeting works?"
|
||||
recommendations_title: "Recommendations for creating a spending proposal"
|
||||
recommendation_one: "It's mandatory that the proposal makes reference to a budgetable action."
|
||||
recommendation_two: "Any proposal or comment suggesting illegal action will be deleted."
|
||||
recommendation_three: "Try to go into details when describing your spending proposal so the reviewing team undertands your points."
|
||||
form:
|
||||
title: "Spending proposal title"
|
||||
description: "Description"
|
||||
external_url: "Link to additional documentation"
|
||||
geozone: "Scope of operation"
|
||||
submit_buttons:
|
||||
new: Create
|
||||
create: Create
|
||||
geozones:
|
||||
none: "All city"
|
||||
comments:
|
||||
show:
|
||||
return_to_commentable: "Go back to "
|
||||
@@ -332,6 +359,7 @@ en:
|
||||
user: "the secret code does not match the image"
|
||||
debate: "the secret code does not match the image"
|
||||
proposal: "the secret code does not match the image"
|
||||
spendingproposal: "the secret code does not match the image"
|
||||
shared:
|
||||
author_info:
|
||||
author_deleted: "User deleted"
|
||||
|
||||
@@ -29,6 +29,7 @@ es:
|
||||
more_information: "Más información"
|
||||
debates: "Debates"
|
||||
proposals: "Propuestas"
|
||||
spending_proposals: "Presupuestos ciudadanos"
|
||||
new_notifications:
|
||||
one: "Tienes una nueva notificación"
|
||||
other: "Tienes %{count} notificaciones nuevas"
|
||||
@@ -75,6 +76,7 @@ es:
|
||||
user: "la cuenta"
|
||||
debate: "el debate"
|
||||
proposal: "la propuesta"
|
||||
spending_proposal: "la propuesta de gasto"
|
||||
verification::sms: "el teléfono"
|
||||
verification::letter: "la verificación"
|
||||
application:
|
||||
@@ -207,7 +209,7 @@ es:
|
||||
proposal_responsible_name: "Nombre y apellidos de la persona que hace esta propuesta"
|
||||
proposal_responsible_name_note: "(individualmente o como representante de un colectivo; no se mostrará públicamente)"
|
||||
tags_label: "Temas"
|
||||
tags_instructions: "Etiqueta esta propuesta. Puedes elegir entre nuestras propuestas o introducir las que desees."
|
||||
tags_instructions: "Etiqueta esta propuesta. Puedes elegir entre nuestras sugerencias o introducir las que desees."
|
||||
tags_placeholder: "Escribe las etiquetas que desees separadas por una coma (',')"
|
||||
show:
|
||||
back_link: "Volver"
|
||||
@@ -244,6 +246,31 @@ es:
|
||||
update:
|
||||
form:
|
||||
submit_button: "Guardar cambios"
|
||||
spending_proposals:
|
||||
index:
|
||||
title: "Presupuestos participativos"
|
||||
text: "Desde esta sección podrás sugerir propuestas de gasto que irán asociadas a las partidas de presupuestos ciudadanos. El requisito principal es que sean propuestas presupuestables."
|
||||
create_link: "Enviar propuesta de gasto"
|
||||
verified_only: "Sólo los usuarios verificados pueden crear propuestas de gasto, %{verify_account}."
|
||||
verify_account: "verifica tu cuenta"
|
||||
new:
|
||||
back_link: Volver
|
||||
start_new: "Crear una propuesta de gasto"
|
||||
more_info: "¿Cómo funcionan los presupuestos participativos?"
|
||||
recommendations_title: "Recomendaciones para crear una propuesta de gasto"
|
||||
recommendation_one: "Es fundamental que haga referencia a una actuación presupuestable."
|
||||
recommendation_two: "Cualquier propuesta o comentario que implique acciones ilegales será eliminada."
|
||||
recommendation_three: "Intenta detallar lo máximo posible la propuesta para que el equipo de gobierno encargado de estudiarla tenga las menor dudas posibles."
|
||||
form:
|
||||
title: "Título de la propuesta de gasto"
|
||||
description: "Descripción detallada"
|
||||
external_url: "Enlace a documentación adicional"
|
||||
geozone: "Ámbito de actuación"
|
||||
submit_buttons:
|
||||
new: Crear
|
||||
create: Crear
|
||||
geozones:
|
||||
none: "Toda la ciudad"
|
||||
comments:
|
||||
show:
|
||||
return_to_commentable: "Volver a "
|
||||
@@ -332,6 +359,7 @@ es:
|
||||
user: "el código secreto no coincide con la imagen"
|
||||
debate: "el código secreto no coincide con la imagen"
|
||||
proposal: "el código secreto no coincide con la imagen"
|
||||
spendingproposal: "el código secreto no coincide con la imagen"
|
||||
shared:
|
||||
author_info:
|
||||
author_deleted: Usuario eliminado
|
||||
|
||||
@@ -35,6 +35,7 @@ en:
|
||||
how_to_use: "Use it in your local government"
|
||||
participation: "Madrid Participation and Transparency y Transparencia - coming news"
|
||||
proposals_info: "How does citizen proposals work?"
|
||||
spending_proposals_info: "How does participatory budgeting work?"
|
||||
participation_world: "Direct citizen participation in the world"
|
||||
participation_facts: "Facts about citizen participation and direct democracy"
|
||||
faq: "Solution to tecnical problemas (FAQ)"
|
||||
@@ -44,6 +45,7 @@ en:
|
||||
how_to_use: "Use it freely or help us to improve it, it is free software"
|
||||
participation: "Citizen participation, transparency and open government"
|
||||
proposals_info: "Create your own proposals"
|
||||
spending_proposals_info: "Create your own spending proposals"
|
||||
participation_world: "Systems of citizen participation that exist in the world"
|
||||
participation_facts: "To lose your fear"
|
||||
faq: "Frecuently asked question about tecnical problems"
|
||||
|
||||
@@ -35,6 +35,7 @@ es:
|
||||
how_to_use: "Utilízalo en tu municipio"
|
||||
participation: "Participación y Transparencia en Madrid - Próximas novedades"
|
||||
proposals_info: "¿Cómo funcionan las propuestas ciudadanas?"
|
||||
spending_proposals_info: "¿Cómo funcionan los presupuestos participativos?"
|
||||
participation_world: "Participación ciudadana directa en el mundo"
|
||||
participation_facts: "Hechos sobre participación ciudadana y democracia directa"
|
||||
faq: "Soluciones a problemas técnicos (FAQ)"
|
||||
@@ -44,6 +45,7 @@ es:
|
||||
how_to_use: "Utilízalo libremente o ayúdanos a mejorarlo, es software libre"
|
||||
participation: "Participación Ciudadana, Transparencia y Gobierno Abierto"
|
||||
proposals_info: "Crea tus propias propuestas"
|
||||
spending_proposals_info: "Envía tus propuestas de gasto"
|
||||
participation_world: "Sistemas de participación ciudadana que ya existen en el mundo"
|
||||
participation_facts: "Para perderle el miedo"
|
||||
faq: "Preguntas frecuentes sobre problemas técnicos"
|
||||
|
||||
@@ -61,6 +61,8 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
resources :spending_proposals, only: [:index, :new, :create]
|
||||
|
||||
resources :legislations, only: [:show]
|
||||
|
||||
resources :annotations do
|
||||
@@ -120,6 +122,13 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
resources :spending_proposals, only: [:index, :show] do
|
||||
member do
|
||||
put :accept
|
||||
put :reject
|
||||
end
|
||||
end
|
||||
|
||||
resources :comments, only: :index do
|
||||
member do
|
||||
put :restore
|
||||
|
||||
@@ -14,6 +14,9 @@ Setting.create(key: 'max_votes_for_proposal_edit', value: '1000')
|
||||
Setting.create(key: 'proposal_code_prefix', value: 'MAD')
|
||||
Setting.create(key: 'votes_for_proposal_success', value: '100')
|
||||
|
||||
puts "Creating Geozones"
|
||||
('A'..'Z').each{ |i| Geozone.create(name: "District #{i}") }
|
||||
|
||||
puts "Creating Users"
|
||||
|
||||
def create_user(email, username = Faker::Name.name)
|
||||
@@ -180,6 +183,25 @@ end
|
||||
Flag.flag(flagger, proposal)
|
||||
end
|
||||
|
||||
puts "Creating Spending Proposals"
|
||||
|
||||
resolutions = ["accepted", "rejected", nil]
|
||||
|
||||
(1..30).each do |i|
|
||||
geozone = Geozone.reorder("RANDOM()").first
|
||||
author = User.reorder("RANDOM()").first
|
||||
description = "<p>#{Faker::Lorem.paragraphs.join('</p><p>')}</p>"
|
||||
spending_proposal = SpendingProposal.create!(author: author,
|
||||
title: Faker::Lorem.sentence(3).truncate(60),
|
||||
external_url: Faker::Internet.url,
|
||||
description: description,
|
||||
created_at: rand((Time.now - 1.week) .. Time.now),
|
||||
resolution: resolutions.sample,
|
||||
geozone: [geozone, nil].sample,
|
||||
terms_of_service: "1")
|
||||
puts " #{spending_proposal.title}"
|
||||
end
|
||||
|
||||
puts "Creating Legislation"
|
||||
|
||||
Legislation.create!(title: 'Participatory Democracy', body: 'In order to achieve...')
|
||||
|
||||
12
db/migrate/20151218114205_create_spending_proposals.rb
Normal file
12
db/migrate/20151218114205_create_spending_proposals.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
class CreateSpendingProposals < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :spending_proposals do |t|
|
||||
t.string :title
|
||||
t.text :description
|
||||
t.integer :author_id
|
||||
t.string :external_url
|
||||
|
||||
t.timestamps null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
class AddSpendingProposalsCounterToTags < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :tags, :spending_proposals_count, :integer, default: 0
|
||||
add_index :tags, :spending_proposals_count
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddSpendingProposalsIndexes < ActiveRecord::Migration
|
||||
def change
|
||||
add_index :spending_proposals, :author_id
|
||||
end
|
||||
end
|
||||
11
db/migrate/20160105121132_create_geozones.rb
Normal file
11
db/migrate/20160105121132_create_geozones.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
class CreateGeozones < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :geozones do |t|
|
||||
t.string :name
|
||||
t.string :html_map_coordinates
|
||||
t.string :external_code
|
||||
|
||||
t.timestamps null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
class AddGeozoneToSpendingProposal < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :spending_proposals, :geozone_id, :integer, default: nil
|
||||
add_index :spending_proposals, :geozone_id
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
class AddResolutionToSpendingProposals < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :spending_proposals, :resolution, :string, default: nil
|
||||
add_index :spending_proposals, :resolution
|
||||
end
|
||||
end
|
||||
37
db/schema.rb
37
db/schema.rb
@@ -171,6 +171,14 @@ ActiveRecord::Schema.define(version: 20160108133501) do
|
||||
add_index "flags", ["user_id", "flaggable_type", "flaggable_id"], name: "access_inappropiate_flags", using: :btree
|
||||
add_index "flags", ["user_id"], name: "index_flags_on_user_id", using: :btree
|
||||
|
||||
create_table "geozones", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.string "html_map_coordinates"
|
||||
t.string "external_code"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "identities", force: :cascade do |t|
|
||||
t.integer "user_id"
|
||||
t.string "provider"
|
||||
@@ -191,7 +199,7 @@ ActiveRecord::Schema.define(version: 20160108133501) do
|
||||
create_table "locks", force: :cascade do |t|
|
||||
t.integer "user_id"
|
||||
t.integer "tries", default: 0
|
||||
t.datetime "locked_until", default: '2000-01-01 07:01:01', null: false
|
||||
t.datetime "locked_until", default: '2000-01-01 00:01:01', null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
@@ -273,6 +281,21 @@ ActiveRecord::Schema.define(version: 20160108133501) do
|
||||
|
||||
add_index "simple_captcha_data", ["key"], name: "idx_key", using: :btree
|
||||
|
||||
create_table "spending_proposals", force: :cascade do |t|
|
||||
t.string "title"
|
||||
t.text "description"
|
||||
t.integer "author_id"
|
||||
t.string "external_url"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "geozone_id"
|
||||
t.string "resolution"
|
||||
end
|
||||
|
||||
add_index "spending_proposals", ["author_id"], name: "index_spending_proposals_on_author_id", using: :btree
|
||||
add_index "spending_proposals", ["geozone_id"], name: "index_spending_proposals_on_geozone_id", using: :btree
|
||||
add_index "spending_proposals", ["resolution"], name: "index_spending_proposals_on_resolution", using: :btree
|
||||
|
||||
create_table "taggings", force: :cascade do |t|
|
||||
t.integer "tag_id"
|
||||
t.integer "taggable_id"
|
||||
@@ -287,16 +310,18 @@ ActiveRecord::Schema.define(version: 20160108133501) do
|
||||
add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
|
||||
|
||||
create_table "tags", force: :cascade do |t|
|
||||
t.string "name", limit: 40
|
||||
t.integer "taggings_count", default: 0
|
||||
t.boolean "featured", default: false
|
||||
t.integer "debates_count", default: 0
|
||||
t.integer "proposals_count", default: 0
|
||||
t.string "name", limit: 40
|
||||
t.integer "taggings_count", default: 0
|
||||
t.boolean "featured", default: false
|
||||
t.integer "debates_count", default: 0
|
||||
t.integer "proposals_count", default: 0
|
||||
t.integer "spending_proposals_count", default: 0
|
||||
end
|
||||
|
||||
add_index "tags", ["debates_count"], name: "index_tags_on_debates_count", using: :btree
|
||||
add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree
|
||||
add_index "tags", ["proposals_count"], name: "index_tags_on_proposals_count", using: :btree
|
||||
add_index "tags", ["spending_proposals_count"], name: "index_tags_on_spending_proposals_count", using: :btree
|
||||
|
||||
create_table "users", force: :cascade do |t|
|
||||
t.string "email", default: ""
|
||||
|
||||
@@ -180,6 +180,14 @@ FactoryGirl.define do
|
||||
end
|
||||
end
|
||||
|
||||
factory :spending_proposal do
|
||||
sequence(:title) { |n| "Spending Proposal #{n} title" }
|
||||
description 'Spend money on this'
|
||||
external_url 'http://external_documention.org'
|
||||
terms_of_service '1'
|
||||
association :author, factory: :user
|
||||
end
|
||||
|
||||
factory :vote do
|
||||
association :votable, factory: :debate
|
||||
association :voter, factory: :user
|
||||
@@ -295,4 +303,7 @@ FactoryGirl.define do
|
||||
association :notifiable, factory: :proposal
|
||||
end
|
||||
|
||||
factory :geozone do
|
||||
sequence(:name) { |n| "District #{n}" }
|
||||
end
|
||||
end
|
||||
|
||||
140
spec/features/admin/spending_proposals_spec.rb
Normal file
140
spec/features/admin/spending_proposals_spec.rb
Normal file
@@ -0,0 +1,140 @@
|
||||
require 'rails_helper'
|
||||
|
||||
feature 'Admin spending proposals' do
|
||||
|
||||
background do
|
||||
admin = create(:administrator)
|
||||
login_as(admin.user)
|
||||
end
|
||||
|
||||
scenario 'Index shows spending proposals' do
|
||||
spending_proposal = create(:spending_proposal)
|
||||
visit admin_spending_proposals_path
|
||||
|
||||
expect(page).to have_content(spending_proposal.title)
|
||||
end
|
||||
|
||||
scenario 'Accept from index' do
|
||||
spending_proposal = create(:spending_proposal)
|
||||
visit admin_spending_proposals_path
|
||||
|
||||
click_link 'Accept'
|
||||
|
||||
expect(page).to_not have_content(spending_proposal.title)
|
||||
|
||||
click_link 'Accepted'
|
||||
expect(page).to have_content(spending_proposal.title)
|
||||
|
||||
expect(spending_proposal.reload).to be_accepted
|
||||
end
|
||||
|
||||
scenario 'Reject from index' do
|
||||
spending_proposal = create(:spending_proposal)
|
||||
visit admin_spending_proposals_path
|
||||
|
||||
click_link 'Reject'
|
||||
|
||||
expect(page).to_not have_content(spending_proposal.title)
|
||||
|
||||
click_link('Rejected')
|
||||
expect(page).to have_content(spending_proposal.title)
|
||||
|
||||
expect(spending_proposal.reload).to be_rejected
|
||||
end
|
||||
|
||||
scenario "Current filter is properly highlighted" do
|
||||
visit admin_spending_proposals_path
|
||||
expect(page).to_not have_link('Unresolved')
|
||||
expect(page).to have_link('Accepted')
|
||||
expect(page).to have_link('Rejected')
|
||||
|
||||
visit admin_spending_proposals_path(filter: 'unresolved')
|
||||
expect(page).to_not have_link('Unresolved')
|
||||
expect(page).to have_link('Accepted')
|
||||
expect(page).to have_link('Rejected')
|
||||
|
||||
visit admin_spending_proposals_path(filter: 'accepted')
|
||||
expect(page).to have_link('Unresolved')
|
||||
expect(page).to_not have_link('Accepted')
|
||||
expect(page).to have_link('Rejected')
|
||||
|
||||
visit admin_spending_proposals_path(filter: 'rejected')
|
||||
expect(page).to have_link('Accepted')
|
||||
expect(page).to have_link('Unresolved')
|
||||
expect(page).to_not have_link('Rejected')
|
||||
end
|
||||
|
||||
scenario "Filtering proposals" do
|
||||
create(:spending_proposal, title: "Recent spending proposal")
|
||||
create(:spending_proposal, title: "Good spending proposal", resolution: "accepted")
|
||||
create(:spending_proposal, title: "Bad spending proposal", resolution: "rejected")
|
||||
|
||||
visit admin_spending_proposals_path(filter: 'unresolved')
|
||||
expect(page).to have_content('Recent spending proposal')
|
||||
expect(page).to_not have_content('Good spending proposal')
|
||||
expect(page).to_not have_content('Bad spending proposal')
|
||||
|
||||
visit admin_spending_proposals_path(filter: 'accepted')
|
||||
expect(page).to have_content('Good spending proposal')
|
||||
expect(page).to_not have_content('Recent spending proposal')
|
||||
expect(page).to_not have_content('Bad spending proposal')
|
||||
|
||||
visit admin_spending_proposals_path(filter: 'rejected')
|
||||
expect(page).to have_content('Bad spending proposal')
|
||||
expect(page).to_not have_content('Good spending proposal')
|
||||
expect(page).to_not have_content('Recent spending proposal')
|
||||
end
|
||||
|
||||
scenario "Action links remember the pagination setting and the filter" do
|
||||
per_page = Kaminari.config.default_per_page
|
||||
(per_page + 2).times { create(:spending_proposal, resolution: "accepted") }
|
||||
|
||||
visit admin_spending_proposals_path(filter: 'accepted', page: 2)
|
||||
|
||||
click_on('Reject', match: :first, exact: true)
|
||||
|
||||
expect(current_url).to include('filter=accepted')
|
||||
expect(current_url).to include('page=2')
|
||||
end
|
||||
|
||||
scenario 'Show' do
|
||||
spending_proposal = create(:spending_proposal, geozone: create(:geozone))
|
||||
visit admin_spending_proposals_path
|
||||
|
||||
click_link spending_proposal.title
|
||||
|
||||
expect(page).to have_content(spending_proposal.title)
|
||||
expect(page).to have_content(spending_proposal.description)
|
||||
expect(page).to have_content(spending_proposal.author.name)
|
||||
expect(page).to have_content(spending_proposal.geozone.name)
|
||||
end
|
||||
|
||||
scenario 'Accept from show' do
|
||||
spending_proposal = create(:spending_proposal)
|
||||
visit admin_spending_proposal_path(spending_proposal)
|
||||
|
||||
click_link 'Accept'
|
||||
|
||||
expect(page).to_not have_content(spending_proposal.title)
|
||||
|
||||
click_link 'Accepted'
|
||||
expect(page).to have_content(spending_proposal.title)
|
||||
|
||||
expect(spending_proposal.reload).to be_accepted
|
||||
end
|
||||
|
||||
scenario 'Reject from show' do
|
||||
spending_proposal = create(:spending_proposal)
|
||||
visit admin_spending_proposal_path(spending_proposal)
|
||||
|
||||
click_link 'Reject'
|
||||
|
||||
expect(page).to_not have_content(spending_proposal.title)
|
||||
|
||||
click_link('Rejected')
|
||||
expect(page).to have_content(spending_proposal.title)
|
||||
|
||||
expect(spending_proposal.reload).to be_rejected
|
||||
end
|
||||
|
||||
end
|
||||
66
spec/features/spending_proposals_spec.rb
Normal file
66
spec/features/spending_proposals_spec.rb
Normal file
@@ -0,0 +1,66 @@
|
||||
require 'rails_helper'
|
||||
|
||||
feature 'Spending proposals' do
|
||||
|
||||
let(:author) { create(:user, :level_two) }
|
||||
|
||||
scenario 'Index' do
|
||||
visit spending_proposals_path
|
||||
|
||||
expect(page).to_not have_link('Create spending proposal', href: new_spending_proposal_path)
|
||||
expect(page).to have_link('verify your account')
|
||||
|
||||
login_as(author)
|
||||
|
||||
visit spending_proposals_path
|
||||
|
||||
expect(page).to have_link('Create spending proposal', href: new_spending_proposal_path)
|
||||
expect(page).to_not have_link('verify your account')
|
||||
end
|
||||
|
||||
scenario 'Create' do
|
||||
login_as(author)
|
||||
|
||||
visit new_spending_proposal_path
|
||||
fill_in 'spending_proposal_title', with: 'Build a skyscraper'
|
||||
fill_in 'spending_proposal_description', with: 'I want to live in a high tower over the clouds'
|
||||
fill_in 'spending_proposal_external_url', with: 'http://http://skyscraperpage.com/'
|
||||
fill_in 'spending_proposal_captcha', with: correct_captcha_text
|
||||
select 'All city', from: 'spending_proposal_geozone_id'
|
||||
check 'spending_proposal_terms_of_service'
|
||||
|
||||
click_button 'Create'
|
||||
|
||||
expect(page).to have_content 'Spending proposal created successfully'
|
||||
end
|
||||
|
||||
scenario 'Captcha is required for proposal creation' do
|
||||
login_as(author)
|
||||
|
||||
visit new_spending_proposal_path
|
||||
fill_in 'spending_proposal_title', with: 'Build a skyscraper'
|
||||
fill_in 'spending_proposal_description', with: 'I want to live in a high tower over the clouds'
|
||||
fill_in 'spending_proposal_external_url', with: 'http://http://skyscraperpage.com/'
|
||||
fill_in 'spending_proposal_captcha', with: 'wrongText'
|
||||
check 'spending_proposal_terms_of_service'
|
||||
|
||||
click_button 'Create'
|
||||
|
||||
expect(page).to_not have_content 'Spending proposal created successfully'
|
||||
expect(page).to have_content '1 error'
|
||||
|
||||
fill_in 'spending_proposal_captcha', with: correct_captcha_text
|
||||
click_button 'Create'
|
||||
|
||||
expect(page).to have_content 'Spending proposal created successfully'
|
||||
end
|
||||
|
||||
scenario 'Errors on create' do
|
||||
login_as(author)
|
||||
|
||||
visit new_spending_proposal_path
|
||||
click_button 'Create'
|
||||
expect(page).to have_content error_message
|
||||
end
|
||||
|
||||
end
|
||||
35
spec/helpers/geozones_helper_spec.rb
Normal file
35
spec/helpers/geozones_helper_spec.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe GeozonesHelper do
|
||||
|
||||
describe "#geozones_name" do
|
||||
let(:geozone) { create :geozone }
|
||||
|
||||
|
||||
it "returns geozone name if present" do
|
||||
spending_proposal = create(:spending_proposal, geozone: geozone)
|
||||
expect(geozone_name(spending_proposal)).to eq geozone.name
|
||||
end
|
||||
|
||||
it "returns default string for no geozone if geozone is blank" do
|
||||
spending_proposal = create(:spending_proposal, geozone: nil)
|
||||
expect(geozone_name(spending_proposal)).to eq "All city"
|
||||
end
|
||||
end
|
||||
|
||||
describe "#geozone_select_options" do
|
||||
it "returns array of ids and names ordered by name" do
|
||||
g1 = create(:geozone, name: "AAA")
|
||||
g3 = create(:geozone, name: "CCC")
|
||||
g2 = create(:geozone, name: "BBB")
|
||||
|
||||
select_options = geozone_select_options
|
||||
|
||||
expect(select_options.size).to eq 3
|
||||
expect(select_options.first).to eq [g1.name, g1.id]
|
||||
expect(select_options[1]).to eq [g2.name, g2.id]
|
||||
expect(select_options.last).to eq [g3.name, g3.id]
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -51,4 +51,6 @@ describe "Abilities::Administrator" do
|
||||
it { should_not be_able_to(:comment_as_moderator, proposal) }
|
||||
|
||||
it { should be_able_to(:manage, Annotation) }
|
||||
|
||||
it { should be_able_to(:manage, SpendingProposal) }
|
||||
end
|
||||
|
||||
@@ -28,6 +28,9 @@ describe "Abilities::Common" do
|
||||
it { should_not be_able_to(:vote, Proposal) }
|
||||
it { should_not be_able_to(:vote_featured, Proposal) }
|
||||
|
||||
it { should be_able_to(:index, SpendingProposal) }
|
||||
it { should_not be_able_to(:create, SpendingProposal) }
|
||||
|
||||
it { should_not be_able_to(:comment_as_administrator, debate) }
|
||||
it { should_not be_able_to(:comment_as_moderator, debate) }
|
||||
it { should_not be_able_to(:comment_as_administrator, proposal) }
|
||||
@@ -84,6 +87,8 @@ describe "Abilities::Common" do
|
||||
|
||||
it { should be_able_to(:vote, Proposal) }
|
||||
it { should be_able_to(:vote_featured, Proposal) }
|
||||
|
||||
it { should be_able_to(:create, SpendingProposal) }
|
||||
end
|
||||
|
||||
describe "when level 3 verified" do
|
||||
@@ -91,5 +96,7 @@ describe "Abilities::Common" do
|
||||
|
||||
it { should be_able_to(:vote, Proposal) }
|
||||
it { should be_able_to(:vote_featured, Proposal) }
|
||||
|
||||
it { should be_able_to(:create, SpendingProposal) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -23,4 +23,7 @@ describe "Abilities::Everyone" do
|
||||
it { should_not be_able_to(:unflag, Proposal) }
|
||||
|
||||
it { should be_able_to(:show, Comment) }
|
||||
|
||||
it { should be_able_to(:index, SpendingProposal) }
|
||||
it { should_not be_able_to(:create, SpendingProposal) }
|
||||
end
|
||||
|
||||
14
spec/models/geozone_spec.rb
Normal file
14
spec/models/geozone_spec.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Geozone, type: :model do
|
||||
let(:geozone) { build(:geozone) }
|
||||
|
||||
it "should be valid" do
|
||||
expect(geozone).to be_valid
|
||||
end
|
||||
|
||||
it "should not be valid without a name" do
|
||||
geozone.name = nil
|
||||
expect(geozone).to_not be_valid
|
||||
end
|
||||
end
|
||||
140
spec/models/spending_proposal_spec.rb
Normal file
140
spec/models/spending_proposal_spec.rb
Normal file
@@ -0,0 +1,140 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe SpendingProposal do
|
||||
let(:spending_proposal) { build(:spending_proposal) }
|
||||
|
||||
it "should be valid" do
|
||||
expect(spending_proposal).to be_valid
|
||||
end
|
||||
|
||||
it "should not be valid without an author" do
|
||||
spending_proposal.author = nil
|
||||
expect(spending_proposal).to_not be_valid
|
||||
end
|
||||
|
||||
describe "#title" do
|
||||
it "should not be valid without a title" do
|
||||
spending_proposal.title = nil
|
||||
expect(spending_proposal).to_not be_valid
|
||||
end
|
||||
|
||||
it "should not be valid when very short" do
|
||||
spending_proposal.title = "abc"
|
||||
expect(spending_proposal).to_not be_valid
|
||||
end
|
||||
|
||||
it "should not be valid when very long" do
|
||||
spending_proposal.title = "a" * 81
|
||||
expect(spending_proposal).to_not be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe "#description" do
|
||||
it "should be sanitized" do
|
||||
spending_proposal.description = "<script>alert('danger');</script>"
|
||||
spending_proposal.valid?
|
||||
expect(spending_proposal.description).to eq("alert('danger');")
|
||||
end
|
||||
|
||||
it "should not be valid when very long" do
|
||||
spending_proposal.description = "a" * 6001
|
||||
expect(spending_proposal).to_not be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe "resolution status" do
|
||||
it "should be valid" do
|
||||
spending_proposal.resolution = "accepted"
|
||||
expect(spending_proposal).to be_valid
|
||||
spending_proposal.resolution = "rejected"
|
||||
expect(spending_proposal).to be_valid
|
||||
spending_proposal.resolution = "wrong"
|
||||
expect(spending_proposal).to_not be_valid
|
||||
end
|
||||
|
||||
it "can be accepted" do
|
||||
spending_proposal.accept
|
||||
expect(spending_proposal.reload.resolution).to eq("accepted")
|
||||
end
|
||||
|
||||
it "can be rejected" do
|
||||
spending_proposal.reject
|
||||
expect(spending_proposal.reload.resolution).to eq("rejected")
|
||||
end
|
||||
|
||||
describe "#accepted?" do
|
||||
it "should be true if resolution equals 'accepted'" do
|
||||
spending_proposal.resolution = "accepted"
|
||||
expect(spending_proposal.accepted?).to eq true
|
||||
end
|
||||
|
||||
it "should be false otherwise" do
|
||||
spending_proposal.resolution = "rejected"
|
||||
expect(spending_proposal.accepted?).to eq false
|
||||
spending_proposal.resolution = nil
|
||||
expect(spending_proposal.accepted?).to eq false
|
||||
end
|
||||
end
|
||||
|
||||
describe "#rejected?" do
|
||||
it "should be true if resolution equals 'rejected'" do
|
||||
spending_proposal.resolution = "rejected"
|
||||
expect(spending_proposal.rejected?).to eq true
|
||||
end
|
||||
|
||||
it "should be false otherwise" do
|
||||
spending_proposal.resolution = "accepted"
|
||||
expect(spending_proposal.rejected?).to eq false
|
||||
spending_proposal.resolution = nil
|
||||
expect(spending_proposal.rejected?).to eq false
|
||||
end
|
||||
end
|
||||
|
||||
describe "#unresolved?" do
|
||||
it "should be true if resolution is blank" do
|
||||
spending_proposal.resolution = nil
|
||||
expect(spending_proposal.unresolved?).to eq true
|
||||
end
|
||||
|
||||
it "should be false otherwise" do
|
||||
spending_proposal.resolution = "accepted"
|
||||
expect(spending_proposal.unresolved?).to eq false
|
||||
spending_proposal.resolution = "rejected"
|
||||
expect(spending_proposal.unresolved?).to eq false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "scopes" do
|
||||
before(:each) do
|
||||
2.times { create(:spending_proposal, resolution: "accepted") }
|
||||
2.times { create(:spending_proposal, resolution: "rejected") }
|
||||
2.times { create(:spending_proposal, resolution: nil) }
|
||||
end
|
||||
|
||||
describe "unresolved" do
|
||||
it "should return all spending proposals without resolution" do
|
||||
unresolved = SpendingProposal.all.unresolved
|
||||
expect(unresolved.size).to eq(2)
|
||||
unresolved.each {|u| expect(u.resolution).to be_nil}
|
||||
end
|
||||
end
|
||||
|
||||
describe "accepted" do
|
||||
it "should return all accepted spending proposals" do
|
||||
accepted = SpendingProposal.all.accepted
|
||||
expect(accepted.size).to eq(2)
|
||||
accepted.each {|a| expect(a.resolution).to eq("accepted")}
|
||||
end
|
||||
end
|
||||
|
||||
describe "rejected" do
|
||||
it "should return all rejected spending proposals" do
|
||||
rejected = SpendingProposal.all.rejected
|
||||
expect(rejected.size).to eq(2)
|
||||
rejected.each {|r| expect(r.resolution).to eq("rejected")}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
Reference in New Issue
Block a user