diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index cd8241fca..196d0be17 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -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 { diff --git a/app/controllers/admin/budget_groups_controller.rb b/app/controllers/admin/budget_groups_controller.rb new file mode 100644 index 000000000..18f5a6b12 --- /dev/null +++ b/app/controllers/admin/budget_groups_controller.rb @@ -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 \ No newline at end of file diff --git a/app/controllers/admin/budget_headings_controller.rb b/app/controllers/admin/budget_headings_controller.rb new file mode 100644 index 000000000..3c8ccafa0 --- /dev/null +++ b/app/controllers/admin/budget_headings_controller.rb @@ -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 \ No newline at end of file diff --git a/app/controllers/admin/budgets_controller.rb b/app/controllers/admin/budgets_controller.rb new file mode 100644 index 000000000..144b43a7f --- /dev/null +++ b/app/controllers/admin/budgets_controller.rb @@ -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 diff --git a/app/helpers/budgets_helper.rb b/app/helpers/budgets_helper.rb new file mode 100644 index 000000000..d281ef182 --- /dev/null +++ b/app/helpers/budgets_helper.rb @@ -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 \ No newline at end of file diff --git a/app/helpers/geozones_helper.rb b/app/helpers/geozones_helper.rb index bfc5f9105..ce03e0579 100644 --- a/app/helpers/geozones_helper.rb +++ b/app/helpers/geozones_helper.rb @@ -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 diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 42e4ecbd3..f3f0b8f9b 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -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 diff --git a/app/models/budget.rb b/app/models/budget.rb index ea22a4615..18f30ba7c 100644 --- a/app/models/budget.rb +++ b/app/models/budget.rb @@ -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 diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index 7ea48b2c6..d1feaeb1e 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -35,6 +35,14 @@ <% end %> + <%# if feature?(:budgets) %> +
  • > + <%= link_to admin_budgets_path do %> + <%= t("admin.menu.budgets") %> + <% end %> +
  • + <%# end %> +
  • > <%= link_to admin_banners_path do %> <%= t("admin.menu.banner") %> diff --git a/app/views/admin/budget_groups/create.js.erb b/app/views/admin/budget_groups/create.js.erb new file mode 100644 index 000000000..cb926a7c6 --- /dev/null +++ b/app/views/admin/budget_groups/create.js.erb @@ -0,0 +1,2 @@ +$("#<%= dom_id(@budget) %>_groups").html('<%= j render("admin/budgets/groups", groups: @groups) %>'); +App.Forms.toggleLink(); \ No newline at end of file diff --git a/app/views/admin/budget_headings/create.js.erb b/app/views/admin/budget_headings/create.js.erb new file mode 100644 index 000000000..5d8eefb2d --- /dev/null +++ b/app/views/admin/budget_headings/create.js.erb @@ -0,0 +1,2 @@ +$("#<%= dom_id(@budget_group) %>").html('<%= j render("admin/budgets/group", group: @budget_group, headings: @headings) %>'); +App.Forms.toggleLink(); \ No newline at end of file diff --git a/app/views/admin/budgets/_group.html.erb b/app/views/admin/budgets/_group.html.erb new file mode 100644 index 000000000..3660fa0c1 --- /dev/null +++ b/app/views/admin/budgets/_group.html.erb @@ -0,0 +1,76 @@ +
    + + + + + + + <% if headings.blank? %> + + + + + <% else %> + + + + + + + + <% end %> + + + + + + + + <% headings.each do |heading| %> + + + + + + <% end %> + + +
    + <%= 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" } %> +
    +
    + <%= t("admin.budgets.form.no_heading") %> +
    +
    <%= t("admin.budgets.form.table_heading") %><%= t("admin.budgets.form.table_amount") %><%= t("admin.budgets.form.table_geozone") %>
    + <%= heading.name %> + + <%= heading.price %> + + <%= geozone_name_from_id heading.geozone_id %> +
    +
    \ No newline at end of file diff --git a/app/views/admin/budgets/_groups.html.erb b/app/views/admin/budgets/_groups.html.erb new file mode 100644 index 000000000..ba785ee0c --- /dev/null +++ b/app/views/admin/budgets/_groups.html.erb @@ -0,0 +1,34 @@ +
    +

    <%= t('admin.budgets.show.groups') %>

    + <% if groups.blank? %> +
    + <%= t("admin.budgets.form.no_groups") %> + <%= link_to t("admin.budgets.form.add_group"), "#", + class: "js-toggle-link", + data: { "toggle-selector" => "#new-group-form" } %> +
    + <% 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| %> +
    + + + + <%= f.text_field :name, + label: false, + maxlength: 50, + placeholder: t("admin.budgets.form.group") %> +
    + <%= f.submit t("admin.budgets.form.create_group"), class: "button success" %> +
    +
    + <% end %> + + <% groups.each do |group| %> +
    + <%= render "admin/budgets/group", group: group, headings: group.headings %> +
    + <% end %> +
    \ No newline at end of file diff --git a/app/views/admin/budgets/index.html.erb b/app/views/admin/budgets/index.html.erb new file mode 100644 index 000000000..193b6a7ef --- /dev/null +++ b/app/views/admin/budgets/index.html.erb @@ -0,0 +1,25 @@ +

    <%= t("admin.budgets.index.title") %>

    + +<%= 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" %> + + +

    <%= page_entries_info @budgets %>

    + + + <% @budgets.each do |budget| %> + + + + + <% end %> +
    + <%= link_to budget.name, admin_budget_path(budget) %> + + <%= t("budget.phase.#{budget.phase}") %> +
    + +<%= paginate @budgets %> \ No newline at end of file diff --git a/app/views/admin/budgets/new.html.erb b/app/views/admin/budgets/new.html.erb new file mode 100644 index 000000000..9ca0f34a5 --- /dev/null +++ b/app/views/admin/budgets/new.html.erb @@ -0,0 +1,29 @@ +
    +
    +

    <%= t("admin.budgets.new.title") %>

    + + <%= 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") %> + +
    +
    + <%= f.label :description, t("admin.budgets.new.phase") %> + <%= f.select :phase, budget_phases_select_options, {label: false} %> +
    +
    + <%= f.label :description, t("admin.budgets.new.currency") %> + <%= f.select :currency_symbol, budget_currency_symbol_select_options, {label: false} %> +
    +
    + <%= f.submit t("admin.budgets.new.create"), class: "button success" %> + <% end %> +
    +
    \ No newline at end of file diff --git a/app/views/admin/budgets/show.html.erb b/app/views/admin/budgets/show.html.erb new file mode 100644 index 000000000..847aa2f60 --- /dev/null +++ b/app/views/admin/budgets/show.html.erb @@ -0,0 +1,16 @@ +
    +
    +

    <%= @budget.name %>

    + + <%= simple_format(text_with_links(@budget.description), {}, sanitize: false) %> + +

    + <%= t('admin.budgets.show.phase') %>: <%= t("budget.phase.#{@budget.phase}") %> | + <%= t('admin.budgets.show.currency') %>: <%= @budget.currency_symbol %> +

    +
    +
    + +
    + <%= render "groups", groups: @budget.groups %> +
    \ No newline at end of file diff --git a/app/views/shared/_admin_login_items.html.erb b/app/views/shared/_admin_login_items.html.erb index 6ba10bbca..013320f38 100644 --- a/app/views/shared/_admin_login_items.html.erb +++ b/app/views/shared/_admin_login_items.html.erb @@ -5,13 +5,13 @@
  • <% end %> - <% if current_user.moderator? || current_user.administrator? %> + <% if current_user.administrator? || current_user.moderator? %>
  • <%= link_to t("layouts.header.moderation"), moderation_root_path %>
  • <% end %> - <% if feature?(:spending_proposals) && (current_user.valuator? || current_user.administrator?) %> + <% if feature?(:spending_proposals) && (current_user.administrator? || current_user.valuator?) %>
  • <%= link_to t("layouts.header.valuation"), valuation_root_path %>
  • diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 250ed18f9..e24d3af0c 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -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*' diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml index 07791e53c..5a7a591bd 100644 --- a/config/locales/activerecord.en.yml +++ b/config/locales/activerecord.en.yml @@ -4,6 +4,9 @@ en: activity: one: "activity" other: "activities" + budget: + one: "Participatory budget" + other: "Participatory budgets" comment: one: "Comment" other: "Comments" diff --git a/config/locales/activerecord.es.yml b/config/locales/activerecord.es.yml index d5b7f0005..ccd0240e8 100644 --- a/config/locales/activerecord.es.yml +++ b/config/locales/activerecord.es.yml @@ -4,6 +4,9 @@ es: activity: one: "actividad" other: "actividades" + budget: + one: "Presupuesto participativo" + other: "Presupuestos participativos" comment: one: "Comentario" other: "Comentarios" diff --git a/config/locales/admin.en.yml b/config/locales/admin.en.yml index 2f73f64d8..78a3a6d6c 100755 --- a/config/locales/admin.en.yml +++ b/config/locales/admin.en.yml @@ -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 diff --git a/config/locales/admin.es.yml b/config/locales/admin.es.yml index 5aada1ce1..496a0208d 100644 --- a/config/locales/admin.es.yml +++ b/config/locales/admin.es.yml @@ -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 diff --git a/config/locales/en.yml b/config/locales/en.yml index 05153d714..fae75307f 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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 diff --git a/config/locales/es.yml b/config/locales/es.yml index 9391da3bf..195a80766 100755 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -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 diff --git a/config/routes.rb b/config/routes.rb index 22518f8c8..8cf94ea23 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index ee5b3ab6f..df58b15cb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 diff --git a/spec/factories.rb b/spec/factories.rb index 263900ecd..bca0c51e3 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -193,6 +193,10 @@ FactoryGirl.define do currency_symbol "€" phase 'on_hold' + trait :accepting do + phase 'accepting' + end + trait :selecting do phase 'selecting' end diff --git a/spec/features/admin/budgets_spec.rb b/spec/features/admin/budgets_spec.rb new file mode 100644 index 000000000..123ca43e8 --- /dev/null +++ b/spec/features/admin/budgets_spec.rb @@ -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 \ No newline at end of file diff --git a/spec/helpers/geozones_helper_spec.rb b/spec/helpers/geozones_helper_spec.rb index 605a774a6..0c0c13d70 100644 --- a/spec/helpers/geozones_helper_spec.rb +++ b/spec/helpers/geozones_helper_spec.rb @@ -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