Merge pull request #1198 from consul/admin-budget

Admin budget
This commit is contained in:
Enrique García
2016-09-01 13:05:05 +02:00
committed by GitHub
29 changed files with 568 additions and 6 deletions

View File

@@ -36,12 +36,21 @@ body.admin {
input[type="text"], textarea {
width: 100%;
}
.input-group input[type="text"] {
border-radius: 0;
margin-bottom: 0 !important;
}
}
table {
th {
text-align: left;
&.with-button {
line-height: $line-height*2;
}
}
tr {

View File

@@ -0,0 +1,15 @@
class Admin::BudgetGroupsController < Admin::BaseController
def create
@budget = Budget.find params[:budget_id]
@budget.groups.create(budget_group_params)
@groups = @budget.groups.includes(:headings)
end
private
def budget_group_params
params.require(:budget_group).permit(:name)
end
end

View File

@@ -0,0 +1,16 @@
class Admin::BudgetHeadingsController < Admin::BaseController
def create
@budget = Budget.find params[:budget_id]
@budget_group = @budget.groups.find params[:budget_group_id]
@budget_group.headings.create(budget_heading_params)
@headings = @budget_group.headings
end
private
def budget_heading_params
params.require(:budget_heading).permit(:name, :price, :geozone_id)
end
end

View File

@@ -0,0 +1,34 @@
class Admin::BudgetsController < Admin::BaseController
has_filters %w{open finished}, only: :index
load_and_authorize_resource
def index
@budgets = Budget.send(@current_filter).order(created_at: :desc).page(params[:page])
end
def show
@budget = Budget.includes(groups: :headings).find(params[:id])
end
def new
@budget = Budget.new
end
def create
@budget = Budget.new(budget_params)
if @budget.save
redirect_to admin_budget_path(@budget), notice: t('admin.budgets.create.notice')
else
render :new
end
end
private
def budget_params
params.require(:budget).permit(:name, :description, :phase, :currency_symbol)
end
end

View File

@@ -0,0 +1,11 @@
module BudgetsHelper
def budget_phases_select_options
Budget::VALID_PHASES.map { |ph| [ t("budget.phase.#{ph}"), ph ] }
end
def budget_currency_symbol_select_options
Budget::CURRENCY_SYMBOLS.map { |cs| [ cs, cs ] }
end
end

View File

@@ -8,4 +8,9 @@ module GeozonesHelper
Geozone.all.order(name: :asc).collect { |g| [ g.name, g.id ] }
end
def geozone_name_from_id(g_id)
@all_geozones ||= Geozone.all.collect{ |g| [ g.id, g.name ] }.to_h
@all_geozones[g_id] || t("geozones.none")
end
end

View File

@@ -42,7 +42,9 @@ module Abilities
can [:read, :update, :valuate, :destroy, :summary], SpendingProposal
can [:create, :update], Budget
can [:index, :read, :new, :create, :update, :destroy], Budget
can [:read, :create, :update, :destroy], Budget::Group
can [:read, :create, :update, :destroy], Budget::Heading
can [:hide, :update], Budget::Investment
can :valuate, Budget::Investment, budget: { valuating: true }
can :create, Budget::ValuatorAssignment

View File

@@ -1,7 +1,9 @@
class Budget < ActiveRecord::Base
VALID_PHASES = %W{on_hold accepting selecting balloting finished}
CURRENCY_SYMBOLS = %W{€ $ £ ¥}
validates :name, presence: true
validates :phase, inclusion: { in: VALID_PHASES }
has_many :investments, dependent: :destroy
@@ -10,6 +12,9 @@ class Budget < ActiveRecord::Base
has_many :headings, through: :groups
has_many :investments, through: :headings
scope :open, -> { where.not(phase: "finished") }
scope :finished, -> { where(phase: "finished") }
def on_hold?
phase == "on_hold"
end

View File

@@ -35,6 +35,14 @@
</li>
<% end %>
<%# if feature?(:budgets) %>
<li <%= "class=active" if controller_name == "budgets" %>>
<%= link_to admin_budgets_path do %>
<span class="icon-budget"></span><%= t("admin.menu.budgets") %>
<% end %>
</li>
<%# end %>
<li <%= "class=active" if controller_name == "banners" %>>
<%= link_to admin_banners_path do %>
<span class="icon-eye"></span><%= t("admin.menu.banner") %>

View File

@@ -0,0 +1,2 @@
$("#<%= dom_id(@budget) %>_groups").html('<%= j render("admin/budgets/groups", groups: @groups) %>');
App.Forms.toggleLink();

View File

@@ -0,0 +1,2 @@
$("#<%= dom_id(@budget_group) %>").html('<%= j render("admin/budgets/group", group: @budget_group, headings: @headings) %>');
App.Forms.toggleLink();

View File

@@ -0,0 +1,76 @@
<div class="small-12 column">
<table>
<thead>
<tr>
<th colspan="3" class="with-button">
<%= group.name %>
<%= link_to t("admin.budgets.form.add_heading"), "#", class: "button float-right js-toggle-link", data: { "toggle-selector" => "#group-#{group.id}-new-heading-form" } %>
</th>
</tr>
<% if headings.blank? %>
<tbody>
<tr>
<td colspan="3">
<div class="callout primary">
<%= t("admin.budgets.form.no_heading") %>
</div>
</td>
</tr>
<% else %>
<tr>
<th><%= t("admin.budgets.form.table_heading") %></th>
<th><%= t("admin.budgets.form.table_amount") %></th>
<th><%= t("admin.budgets.form.table_geozone") %></th>
</tr>
</thead>
<tbody>
<% end %>
<!-- new heading form -->
<tr id="group-<%= group.id %>-new-heading-form" style="display:none">
<td colspan="3">
<%= form_for [:admin, @budget, group, Budget::Heading.new], remote: true do |f| %>
<label><%= t("admin.budgets.form.heading") %></label>
<%= f.text_field :name,
label: false,
maxlength: 50,
placeholder: t("admin.budgets.form.heading") %>
<div class="row">
<div class="small-12 medium-6 column">
<label><%= t("admin.budgets.form.amount") %></label>
<%= f.text_field :price,
label: false,
maxlength: 8,
placeholder: t("admin.budgets.form.amount") %>
</div>
<div class="small-12 medium-6 column">
<label><%= t("admin.budgets.form.geozone") %></label>
<%= f.select :geozone_id, geozone_select_options, {include_blank: t("geozones.none"), label: false} %>
</div>
</div>
<%= f.submit t("admin.budgets.form.save_heading"), class: "button success" %>
<% end %>
</td>
</tr>
<!-- /. new heading form -->
<!-- headings list -->
<% headings.each do |heading| %>
<tr>
<td>
<%= heading.name %>
</td>
<td>
<%= heading.price %>
</td>
<td>
<%= geozone_name_from_id heading.geozone_id %>
</td>
</tr>
<% end %>
<!-- /. headings list -->
</tbody>
</table>
</div>

View File

@@ -0,0 +1,34 @@
<div class="small-12 column">
<h3 class="inline-block"><%= t('admin.budgets.show.groups') %></h3>
<% if groups.blank? %>
<div class="callout primary">
<%= t("admin.budgets.form.no_groups") %>
<strong><%= link_to t("admin.budgets.form.add_group"), "#",
class: "js-toggle-link",
data: { "toggle-selector" => "#new-group-form" } %></strong>
</div>
<% else %>
<%= link_to t("admin.budgets.form.add_group"), "#", class: "button float-right js-toggle-link", data: { "toggle-selector" => "#new-group-form" } %>
<% end %>
<%= form_for [:admin, @budget, Budget::Group.new], html: {id: "new-group-form", style: "display:none"}, remote: true do |f| %>
<div class="input-group">
<span class="input-group-label">
<label><%= f.label :name,t("admin.budgets.form.group") %></label>
</span>
<%= f.text_field :name,
label: false,
maxlength: 50,
placeholder: t("admin.budgets.form.group") %>
<div class="input-group-button">
<%= f.submit t("admin.budgets.form.create_group"), class: "button success" %>
</div>
</div>
<% end %>
<% groups.each do |group| %>
<div class="row" id="<%= dom_id(group) %>">
<%= render "admin/budgets/group", group: group, headings: group.headings %>
</div>
<% end %>
</div>

View File

@@ -0,0 +1,25 @@
<h2 class="inline-block"><%= t("admin.budgets.index.title") %></h2>
<%= link_to t("admin.budgets.index.new_link"),
new_admin_budget_path,
class: "button float-right margin-right" %>
<%= render 'shared/filter_subnav', i18n_namespace: "admin.budgets.index" %>
<h3><%= page_entries_info @budgets %></h3>
<table>
<% @budgets.each do |budget| %>
<tr id="<%= dom_id(budget) %>" class="budget">
<td>
<%= link_to budget.name, admin_budget_path(budget) %>
</td>
<td class="small">
<%= t("budget.phase.#{budget.phase}") %>
</td>
</tr>
<% end %>
</table>
<%= paginate @budgets %>

View File

@@ -0,0 +1,29 @@
<div class="row">
<div class="small-12 medium-9 column">
<h2><%= t("admin.budgets.new.title") %></h2>
<%= form_for [:admin, @budget] do |f| %>
<%= f.label :name, t("admin.budgets.new.name") %>
<%= f.text_field :name,
label: false,
maxlength: 30,
placeholder: t("admin.budgets.new.name") %>
<%= f.label :description, t("admin.budgets.new.description") %>
<%= f.text_area :description, rows: 3, maxlength: 6000, label: false, placeholder: t("admin.budgets.new.description") %>
<div class="row">
<div class="small-12 medium-9 column">
<%= f.label :description, t("admin.budgets.new.phase") %>
<%= f.select :phase, budget_phases_select_options, {label: false} %>
</div>
<div class="small-12 medium-3 column">
<%= f.label :description, t("admin.budgets.new.currency") %>
<%= f.select :currency_symbol, budget_currency_symbol_select_options, {label: false} %>
</div>
</div>
<%= f.submit t("admin.budgets.new.create"), class: "button success" %>
<% end %>
</div>
</div>

View File

@@ -0,0 +1,16 @@
<div class="row">
<div class="small-12 medium-9 column">
<h2><%= @budget.name %></h2>
<%= simple_format(text_with_links(@budget.description), {}, sanitize: false) %>
<p>
<strong><%= t('admin.budgets.show.phase') %>:</strong> <%= t("budget.phase.#{@budget.phase}") %> |
<strong><%= t('admin.budgets.show.currency') %>:</strong> <%= @budget.currency_symbol %>
</p>
</div>
</div>
<div id="<%= dom_id @budget %>_groups" class="row">
<%= render "groups", groups: @budget.groups %>
</div>

View File

@@ -5,13 +5,13 @@
</li>
<% end %>
<% if current_user.moderator? || current_user.administrator? %>
<% if current_user.administrator? || current_user.moderator? %>
<li>
<%= link_to t("layouts.header.moderation"), moderation_root_path %>
</li>
<% end %>
<% if feature?(:spending_proposals) && (current_user.valuator? || current_user.administrator?) %>
<% if feature?(:spending_proposals) && (current_user.administrator? || current_user.valuator?) %>
<li>
<%= link_to t("layouts.header.valuation"), valuation_root_path %>
</li>

View File

@@ -112,6 +112,7 @@ ignore_unused:
- 'admin.banners.index.filters.*'
- 'admin.debates.index.filter*'
- 'admin.proposals.index.filter*'
- 'admin.budgets.index.filter*'
- 'admin.spending_proposals.index.filter*'
- 'admin.organizations.index.filter*'
- 'admin.users.index.filter*'

View File

@@ -4,6 +4,9 @@ en:
activity:
one: "activity"
other: "activities"
budget:
one: "Participatory budget"
other: "Participatory budgets"
comment:
one: "Comment"
other: "Comments"

View File

@@ -4,6 +4,9 @@ es:
activity:
one: "actividad"
other: "actividades"
budget:
one: "Presupuesto participativo"
other: "Presupuestos participativos"
comment:
one: "Comentario"
other: "Comentarios"

View File

@@ -32,8 +32,6 @@ en:
editing: Edit banner
form:
submit_button: Save changes
errors:
form:
errors:
form:
error:
@@ -60,6 +58,40 @@ en:
on_users: Users
title: Moderator activity
type: Type
budgets:
index:
title: Participatory budgets
new_link: Create new
filters:
open: Open
finished: Finished
create:
notice: New participatory budget created successfully!
new:
title: New participatory budget
create: Create budget
name: Budget's name
description: Description
phase: Phase
currency: Currency
show:
phase: Current phase
currency: Currency
groups: Groups of budget headings
form:
group: Group's name
no_groups: No groups created yet. Each user will be able to vote in only one heading per group.
add_group: Add new group
create_group: Create group
heading: Heading's name
add_heading: Add heading
amount: Amount
save_heading: Save heading
no_heading: This group has no assigned heading.
geozone: Scope of operation
table_heading: Heading
table_amount: Amount
table_geozone: Scope of operation
comments:
index:
filter: Filter
@@ -96,6 +128,7 @@ en:
activity: Moderator activity
admin: Admin menu
banner: Manage banners
budgets: Participatory budgets
debate_topics: Debate topics
hidden_comments: Hidden comments
hidden_debates: Hidden debates

View File

@@ -58,6 +58,40 @@ es:
on_users: Usuarios
title: Actividad de los Moderadores
type: Tipo
budgets:
index:
title: Presupuestos participativos
new_link: Crear nuevo
filters:
open: Abiertos
finished: Terminados
create:
notice: ¡Nueva campaña de presupuestos participativos creada con éxito!
new:
title: Nuevo presupuesto ciudadano
create: Crear presupuesto
name: Nombre del presupuesto
description: Descripción
phase: Fase
currency: Divisa
show:
phase: Fase actual
currency: Divisa
groups: Grupos de partidas presupuestarias
form:
group: Nombre del grupo
no_groups: No hay grupos creados todavía. Cada usuario podrá votar en una sola partida de cada grupo.
add_group: Añadir nuevo grupo
create_group: Crear grupo
heading: Nombre de la partida
add_heading: Añadir partida
amount: Cantidad
save_heading: Guardar partida
no_heading: Este grupo no tiene ninguna partida asignada.
geozone: Ámbito de actuación
table_heading: Partida
table_amount: Cantidad
table_geozone: Ámbito de actuación
comments:
index:
filter: Filtro
@@ -94,6 +128,7 @@ es:
activity: Actividad de moderadores
admin: Menú de administración
banner: Gestionar banners
budgets: Presupuestos participativos
debate_topics: Temas de debate
hidden_comments: Comentarios ocultos
hidden_debates: Debates ocultos

View File

@@ -33,6 +33,13 @@ en:
application:
close: Close
menu: Menu
budget:
phase:
on_hold: On hold
accepting: Accepting proposals
selecting: Selecting
balloting: Balloting
finished: Finished
comments:
comment:
admin: Administrator

View File

@@ -33,6 +33,13 @@ es:
application:
close: Cerrar
menu: Menú
budget:
phase:
on_hold: En pausa
accepting: Aceptando propuestas
selecting: Fase de selección
balloting: Fase de Votación
finished: Terminado
comments:
comment:
admin: Administrador

View File

@@ -153,6 +153,13 @@ Rails.application.routes.draw do
get :summary, on: :collection
end
resources :budgets do
resources :budget_groups do
resources :budget_headings do
end
end
end
resources :banners, only: [:index, :new, :create, :edit, :update, :destroy] do
collection { get :search}
end

View File

@@ -210,10 +210,10 @@ ActiveRecord::Schema.define(version: 20160617172616) do
t.string "visit_id"
t.datetime "hidden_at"
t.integer "flags_count", default: 0
t.datetime "ignored_flag_at"
t.integer "cached_votes_total", default: 0
t.integer "cached_votes_up", default: 0
t.integer "cached_votes_down", default: 0
t.datetime "ignored_flag_at"
t.integer "comments_count", default: 0
t.datetime "confirmed_hide_at"
t.integer "cached_anonymous_votes_total", default: 0

View File

@@ -193,6 +193,10 @@ FactoryGirl.define do
currency_symbol ""
phase 'on_hold'
trait :accepting do
phase 'accepting'
end
trait :selecting do
phase 'selecting'
end

View File

@@ -0,0 +1,158 @@
require 'rails_helper'
feature 'Admin budgets' do
background do
admin = create(:administrator)
login_as(admin.user)
end
context 'Feature flag' do
xscenario 'Disabled with a feature flag' do
Setting['feature.budgets'] = nil
expect{ visit admin_budgets_path }.to raise_exception(FeatureFlags::FeatureDisabled)
end
end
context 'Index' do
scenario 'Displaying budgets' do
budget = create(:budget)
visit admin_budgets_path
expect(page).to have_content(budget.name)
expect(page).to have_content(I18n.t("budget.phase.#{budget.phase}"))
end
scenario 'Filters by phase' do
budget1 = create(:budget)
budget2 = create(:budget, :accepting)
budget3 = create(:budget, :selecting)
budget4 = create(:budget, :balloting)
budget5 = create(:budget, :finished)
visit admin_budgets_path
expect(page).to have_content(budget1.name)
expect(page).to have_content(budget2.name)
expect(page).to have_content(budget3.name)
expect(page).to have_content(budget4.name)
expect(page).to_not have_content(budget5.name)
click_link 'Finished'
expect(page).to_not have_content(budget1.name)
expect(page).to_not have_content(budget2.name)
expect(page).to_not have_content(budget3.name)
expect(page).to_not have_content(budget4.name)
expect(page).to have_content(budget5.name)
click_link 'Open'
expect(page).to have_content(budget1.name)
expect(page).to have_content(budget2.name)
expect(page).to have_content(budget3.name)
expect(page).to have_content(budget4.name)
expect(page).to_not have_content(budget5.name)
end
scenario 'Current filter is properly highlighted' do
filters_links = {'open' => 'Open', 'finished' => 'Finished'}
visit admin_budgets_path
expect(page).to_not have_link(filters_links.values.first)
filters_links.keys.drop(1).each { |filter| expect(page).to have_link(filters_links[filter]) }
filters_links.each_pair do |current_filter, link|
visit admin_budgets_path(filter: current_filter)
expect(page).to_not have_link(link)
(filters_links.keys - [current_filter]).each do |filter|
expect(page).to have_link(filters_links[filter])
end
end
end
end
context 'New' do
scenario 'Create budget' do
visit admin_budgets_path
click_link 'Create new'
fill_in 'budget_name', with: 'M30 - Summer campaign'
fill_in 'budget_description', with: 'Budgeting for summer 2017 maintenance and improvements of the road M-30'
select 'Accepting proposals', from: 'budget[phase]'
click_button 'Create budget'
expect(page).to have_content 'New participatory budget created successfully!'
expect(page).to have_content 'M30 - Summer campaign'
end
scenario 'Name is mandatory' do
visit new_admin_budget_path
click_button 'Create budget'
expect(page).to_not have_content 'New participatory budget created successfully!'
expect(page).to have_css("label.error", text: "Budget's name")
end
end
context 'Manage groups and headings' do
scenario 'Create group', :js do
create(:budget, name: 'Yearly participatory budget')
visit admin_budgets_path
click_link 'Yearly participatory budget'
expect(page).to have_content 'No groups created yet.'
click_link 'Add new group'
fill_in 'budget_group_name', with: 'General improvments'
click_button 'Create group'
expect(page).to have_content 'Yearly participatory budget'
expect(page).to_not have_content 'No groups created yet.'
visit admin_budgets_path
click_link 'Yearly participatory budget'
expect(page).to have_content 'Yearly participatory budget'
expect(page).to_not have_content 'No groups created yet.'
end
scenario 'Create heading', :js do
budget = create(:budget, name: 'Yearly participatory budget')
group = create(:budget_group, budget: budget, name: 'Districts improvments')
visit admin_budget_path(budget)
within("#budget_group_#{group.id}") do
expect(page).to have_content 'This group has no assigned heading.'
click_link 'Add heading'
fill_in 'budget_heading_name', with: 'District 9 reconstruction'
fill_in 'budget_heading_price', with: '6785'
click_button 'Save heading'
end
expect(page).to_not have_content 'This group has no assigned heading.'
visit admin_budget_path(budget)
within("#budget_group_#{group.id}") do
expect(page).to_not have_content 'This group has no assigned heading.'
expect(page).to have_content 'District 9 reconstruction'
expect(page).to have_content '6785'
expect(page).to have_content 'All city'
end
end
end
end

View File

@@ -31,4 +31,19 @@ describe GeozonesHelper do
end
end
describe "#geozone_name_from_id" do
it "returns geozone name if present" do
g1 = create(:geozone, name: "AAA")
g2 = create(:geozone, name: "BBB")
expect(geozone_name_from_id(g1.id)).to eq "AAA"
expect(geozone_name_from_id(g2.id)).to eq "BBB"
end
it "returns default string for no geozone if geozone is blank" do
expect(geozone_name_from_id(nil)).to eq "All city"
end
end
end