Add headings step to budget creation

Co-Authored-By: decabeza <alberto@decabeza.es>
This commit is contained in:
Julian Herrero
2020-03-22 04:54:10 +01:00
committed by Javi Martín
parent 0a2c70cbfe
commit f8d6ba12d7
27 changed files with 537 additions and 78 deletions

View File

@@ -0,0 +1,50 @@
.budget-group-switcher {
margin-bottom: $line-height;
p {
margin-bottom: 0;
}
> .menu > li {
> button {
align-items: center;
border: $admin-border;
border-radius: $button-radius;
display: inline-flex;
padding: rem-calc(11) rem-calc(16);
&:active {
color: inherit;
}
&::after {
@include css-triangle($dropdownmenu-arrow-size, currentcolor, down);
margin-left: 0.2em;
}
}
}
.menu.is-dropdown-submenu {
margin: 0;
min-width: 100%;
padding: 0;
li {
margin-bottom: 0;
}
a {
cursor: default;
padding: rem-calc(11) rem-calc(16);
width: 100%;
&:focus,
&:hover {
@include brand-background;
text-decoration: none;
outline: none;
}
}
}
}

View File

@@ -18,7 +18,7 @@
<td>
<%= render Admin::TableActionsComponent.new(group) do |actions| %>
<%= actions.link_to t("admin.budget_groups.headings_manage"),
admin_budget_group_headings_path(budget, group),
headings_path(actions, group),
class: "headings-link" %>
<% end %>
</td>

View File

@@ -10,4 +10,8 @@ class Admin::BudgetGroups::GroupsComponent < ApplicationComponent
def budget
@budget ||= groups.first.budget
end
def headings_path(table_actions_component, group)
send("#{table_actions_component.namespace}_budget_group_headings_path", group.budget, group)
end
end

View File

@@ -0,0 +1,19 @@
<div class="budget-creation-step">
<button type="button" class="add" aria-expanded="<%= show_form? %>">
<%= t("admin.#{i18n_namespace_with_budget}.index.new_button") %>
</button>
<%= content %>
<button type="button" class="cancel delete"><%= t("links.form.cancel_button") %></button>
<% if next_step_path %>
<%= link_to t("admin.budgets_wizard.#{i18n_namespace}.continue"),
next_step_path,
class: "next-step" %>
<% else %>
<p class="next-step">
<%= t("admin.budgets_wizard.#{i18n_namespace}.continue") %>
</p>
<% end %>
</div>

View File

@@ -0,0 +1,22 @@
class Admin::BudgetsWizard::CreationStepComponent < ApplicationComponent
attr_reader :record, :next_step_path
def initialize(record, next_step_path)
@record = record
@next_step_path = next_step_path
end
private
def show_form?
record.errors.any?
end
def i18n_namespace
i18n_namespace_with_budget.gsub("budget_", "")
end
def i18n_namespace_with_budget
record.class.table_name
end
end

View File

@@ -8,6 +8,6 @@ class Admin::BudgetsWizard::CreationTimelineComponent < ApplicationComponent
private
def steps
%w[budget groups]
%w[budget groups headings]
end
end

View File

@@ -1,19 +1,3 @@
<div class="budget-creation-step">
<button type="button" class="add" aria-expanded="<%= show_form? %>">
<%= t("admin.budget_groups.index.new_button") %>
</button>
<%= render Admin::BudgetsWizard::CreationStepComponent.new(group, next_step_path) do %>
<%= render "/admin/budget_groups/form", group: group, path: form_path, action: "create" %>
<button type="button" class="cancel delete"><%= t("links.form.cancel_button") %></button>
<% if next_step_path %>
<%= link_to t("admin.budgets_wizard.groups.continue"),
next_step_path,
class: "next-step" %>
<% else %>
<p class="next-step">
<%= t("admin.budgets_wizard.groups.continue") %>
</p>
<% end %>
</div>
<% end %>

View File

@@ -12,16 +12,12 @@ class Admin::BudgetsWizard::Groups::CreationStepComponent < ApplicationComponent
group.budget
end
def show_form?
group.errors.any?
end
def form_path
admin_budgets_wizard_budget_groups_path(budget)
end
def next_step_path
admin_budget_group_headings_path(budget, next_step_group) if next_step_enabled?
admin_budgets_wizard_budget_group_headings_path(budget, next_step_group) if next_step_enabled?
end
def next_step_enabled?

View File

@@ -0,0 +1,3 @@
<%= render Admin::BudgetsWizard::CreationStepComponent.new(heading, next_step_path) do %>
<%= render "/admin/budget_headings/form", heading: heading, path: form_path, action: "create" %>
<% end %>

View File

@@ -0,0 +1,25 @@
class Admin::BudgetsWizard::Headings::CreationStepComponent < ApplicationComponent
attr_reader :heading
def initialize(heading)
@heading = heading
end
private
def budget
heading.budget
end
def form_path
admin_budgets_wizard_budget_group_headings_path(heading.group.budget, heading.group)
end
def next_step_path
admin_budget_path(budget) if next_step_enabled?
end
def next_step_enabled?
budget.headings.any?
end
end

View File

@@ -0,0 +1,7 @@
<%= back_link_to admin_budgets_wizard_budget_group_headings_path(budget, group) %>
<%= header %>
<%= render Admin::BudgetsWizard::CreationTimelineComponent.new("headings") %>
<%= render "/admin/budget_headings/form", heading: heading, path: form_path, action: "submit" %>

View File

@@ -0,0 +1,26 @@
class Admin::BudgetsWizard::Headings::EditComponent < ApplicationComponent
include Header
attr_reader :heading
def initialize(heading)
@heading = heading
end
def budget
heading.budget
end
def group
heading.group
end
def title
heading.name
end
private
def form_path
admin_budgets_wizard_budget_group_heading_path(budget, group, heading)
end
end

View File

@@ -0,0 +1,21 @@
<div class="budget-group-switcher">
<% if other_groups.one? %>
<p>
<%= t("admin.budget_headings.group_switcher.currently_showing", group: group.name) %>
<%= link_to t("admin.budget_headings.group_switcher.the_other_group", group: other_groups.first.name),
headings_path(other_groups.first) %>
</p>
<% else %>
<p><%= t("admin.budget_headings.group_switcher.currently_showing", group: group.name) %></p>
<ul class="dropdown menu" data-dropdown-menu data-disable-hover="true" data-click-open="true">
<li class="has-submenu">
<button type="button"><%= t("admin.budget_headings.group_switcher.different_group") %></button>
<ul class="menu" data-submenu>
<% other_groups.each do |other_group| %>
<li><%= link_to other_group.name, headings_path(other_group) %></li>
<% end %>
</ul>
</li>
</ul>
<% end %>
</div>

View File

@@ -0,0 +1,25 @@
class Admin::BudgetsWizard::Headings::GroupSwitcherComponent < ApplicationComponent
attr_reader :group
def initialize(group)
@group = group
end
def render?
other_groups.any?
end
private
def budget
group.budget
end
def other_groups
@other_groups ||= budget.groups.sort_by_name - [group]
end
def headings_path(group)
admin_budgets_wizard_budget_group_headings_path(budget, group)
end
end

View File

@@ -0,0 +1,10 @@
<%= back_link_to admin_budgets_wizard_budget_groups_path(budget), t("admin.budget_headings.index.back") %>
<%= header %>
<%= render Admin::Budgets::HelpComponent.new("budget_headings") %>
<%= render Admin::BudgetsWizard::CreationTimelineComponent.new("headings") %>
<%= render Admin::BudgetsWizard::Headings::GroupSwitcherComponent.new(group) %>
<%= render Admin::BudgetHeadings::HeadingsComponent.new(headings) %>
<%= render Admin::BudgetsWizard::Headings::CreationStepComponent.new(new_heading) %>

View File

@@ -0,0 +1,21 @@
class Admin::BudgetsWizard::Headings::IndexComponent < ApplicationComponent
include Header
attr_reader :headings, :new_heading
def initialize(headings, new_heading)
@headings = headings
@new_heading = new_heading
end
def budget
group.budget
end
def group
new_heading.group
end
def title
t("admin.budget_headings.index.title", budget: budget.name, group: group.name)
end
end

View File

@@ -1,69 +1,20 @@
class Admin::BudgetHeadingsController < Admin::BaseController
include Translatable
include FeatureFlags
feature_flag :budgets
before_action :load_budget
before_action :load_group
before_action :load_heading, only: [:edit, :update, :destroy]
include Admin::BudgetHeadingsActions
def index
@headings = @group.headings.order(:id)
end
def new
@heading = @group.headings.new
end
def edit
end
def create
@heading = @group.headings.new(budget_heading_params)
if @heading.save
redirect_to headings_index, notice: t("admin.budget_headings.create.notice")
else
render :new
end
end
def update
if @heading.update(budget_heading_params)
redirect_to headings_index, notice: t("admin.budget_headings.update.notice")
else
render :edit
end
end
def destroy
if @heading.can_be_deleted?
@heading.destroy!
redirect_to headings_index, notice: t("admin.budget_headings.destroy.success_notice")
else
redirect_to headings_index, alert: t("admin.budget_headings.destroy.unable_notice")
end
end
private
def load_budget
@budget = Budget.find_by_slug_or_id! params[:budget_id]
end
def load_group
@group = @budget.groups.find_by_slug_or_id! params[:group_id]
end
def load_heading
@heading = @group.headings.find_by_slug_or_id! params[:id]
end
def headings_index
admin_budget_group_headings_path(@budget, @group)
end
def budget_heading_params
valid_attributes = [:price, :population, :allow_custom_content, :latitude, :longitude, :max_ballot_lines]
params.require(:budget_heading).permit(*valid_attributes, translation_params(Budget::Heading))
def new_action
:new
end
end

View File

@@ -0,0 +1,19 @@
class Admin::BudgetsWizard::HeadingsController < Admin::BaseController
include Admin::BudgetHeadingsActions
before_action :load_headings, only: [:index, :create]
def index
@heading = @group.headings.new
end
private
def headings_index
admin_budgets_wizard_budget_group_headings_path(@budget, @group)
end
def new_action
:index
end
end

View File

@@ -0,0 +1,66 @@
module Admin::BudgetHeadingsActions
extend ActiveSupport::Concern
included do
include Translatable
include FeatureFlags
feature_flag :budgets
before_action :load_budget
before_action :load_group
before_action :load_headings, only: :index
before_action :load_heading, only: [:edit, :update, :destroy]
end
def edit
end
def create
@heading = @group.headings.new(budget_heading_params)
if @heading.save
redirect_to headings_index, notice: t("admin.budget_headings.create.notice")
else
render new_action
end
end
def update
if @heading.update(budget_heading_params)
redirect_to headings_index, notice: t("admin.budget_headings.update.notice")
else
render :edit
end
end
def destroy
if @heading.can_be_deleted?
@heading.destroy!
redirect_to headings_index, notice: t("admin.budget_headings.destroy.success_notice")
else
redirect_to headings_index, alert: t("admin.budget_headings.destroy.unable_notice")
end
end
private
def load_budget
@budget = Budget.find_by_slug_or_id! params[:budget_id]
end
def load_group
@group = @budget.groups.find_by_slug_or_id! params[:group_id]
end
def load_headings
@headings = @group.headings.order(:id)
end
def load_heading
@heading = @group.headings.find_by_slug_or_id! params[:id]
end
def budget_heading_params
valid_attributes = [:price, :population, :allow_custom_content, :latitude, :longitude, :max_ballot_lines]
params.require(:budget_heading).permit(*valid_attributes, translation_params(Budget::Heading))
end
end

View File

@@ -0,0 +1 @@
<%= render Admin::BudgetsWizard::Headings::EditComponent.new(@heading) %>

View File

@@ -0,0 +1 @@
<%= render Admin::BudgetsWizard::Headings::IndexComponent.new(@headings, @heading) %>

View File

@@ -148,6 +148,7 @@ ignore_unused:
- "admin.budgets.index.filter*"
- "admin.budgets.edit.(administrators|valuators).*"
- "admin.budget_groups.index.*.help_block"
- "admin.budget_headings.index.*.help_block"
- "admin.budget_investments.index.filter*"
- "admin.organizations.index.filter*"
- "admin.hidden_users.index.filter*"

View File

@@ -177,9 +177,14 @@ en:
create: "Create new heading"
edit: "Edit heading"
submit: "Save heading"
group_switcher:
currently_showing: "Showing headings from the %{group} group."
different_group: "Manage headings from a different group"
the_other_group: "Manage headings from the %{group} group."
index:
back: "Go back to groups"
help: "Headings are meant to divide the money of the participatory budget. Here you can add headings for this group and assign the amount of money that will be used for each heading."
new_button: "Add new heading"
title: "%{budget} / %{group} headings"
budget_phases:
edit:
@@ -290,10 +295,13 @@ en:
creation_timeline:
budget: Budget
groups: Groups
headings: Headings
budgets:
continue: "Continue to groups"
groups:
continue: "Continue to headings"
headings:
continue: "Continue to phases"
milestones:
index:
table_id: "ID"

View File

@@ -177,9 +177,14 @@ es:
create: "Crear nueva partida"
edit: "Editar partida"
submit: "Guardar partida"
group_switcher:
currently_showing: "Mostrando las partidas del grupo: %{group}"
different_group: "Ir a partidas de otro grupo"
the_other_group: "Ir a partidas del grupo %{group}."
index:
back: "Volver a grupos"
help: "Las partidas sirven para dividir el dinero del presupuesto participativo. Aquí puedes ir añadiendo partidas para cada grupo y establecer la cantidad de dinero que se gastará en cada partida."
new_button: "Añadir una partida nueva"
title: "Partidas de %{budget} / %{group}"
budget_phases:
edit:
@@ -290,10 +295,13 @@ es:
creation_timeline:
budget: Presupuesto
groups: Grupos
headings: Partidas
budgets:
continue: "Continuar a grupos"
groups:
continue: "Continuar a partidas"
headings:
continue: "Continuar a fases"
milestones:
index:
table_id: "ID"

View File

@@ -74,7 +74,9 @@ namespace :admin do
namespace :budgets_wizard do
resources :budgets, only: [:create, :new] do
resources :groups, only: [:index, :create, :edit, :update, :destroy]
resources :groups, only: [:index, :create, :edit, :update, :destroy] do
resources :headings, only: [:index, :create, :edit, :update, :destroy]
end
end
end

View File

@@ -0,0 +1,41 @@
require "rails_helper"
describe Admin::BudgetsWizard::Headings::GroupSwitcherComponent, type: :component do
it "is not rendered for budgets with one group" do
group = create(:budget_group, budget: create(:budget))
render_inline Admin::BudgetsWizard::Headings::GroupSwitcherComponent.new(group)
expect(page.text).to be_empty
expect(page).not_to have_css ".budget-group-switcher"
end
it "renders a link to switch group for budgets with two groups" do
budget = create(:budget)
group = create(:budget_group, budget: budget, name: "Parks")
create(:budget_group, budget: budget, name: "Recreation")
render_inline Admin::BudgetsWizard::Headings::GroupSwitcherComponent.new(group)
expect(page).to have_content "Showing headings from the Parks group"
expect(page).to have_link "Manage headings from the Recreation group."
expect(page).not_to have_css "ul"
end
it "renders a menu to switch group for budgets with more than two groups" do
budget = create(:budget)
group = create(:budget_group, budget: budget, name: "Parks")
create(:budget_group, budget: budget, name: "Recreation")
create(:budget_group, budget: budget, name: "Entertainment")
render_inline Admin::BudgetsWizard::Headings::GroupSwitcherComponent.new(group)
expect(page).to have_content "Showing headings from the Parks group"
expect(page).to have_button "Manage headings from a different group"
within "button + ul" do
expect(page).to have_link "Recreation"
expect(page).to have_link "Entertainment"
end
end
end

View File

@@ -0,0 +1,148 @@
require "rails_helper"
describe "Budgets wizard, headings step", :admin do
let(:budget) { create(:budget, :drafting) }
let(:group) { create(:budget_group, budget: budget, name: "Default group") }
describe "Index" do
scenario "back to a previous step" do
visit admin_budgets_wizard_budget_group_headings_path(budget, group)
within "#side_menu" do
expect(page).to have_css ".is-active", exact_text: "Participatory budgets"
end
click_link "Go back to groups"
expect(page).to have_css "tr", text: "Default group"
expect(page).to have_css ".creation-timeline"
end
scenario "change to another group" do
economy = create(:budget_group, budget: budget, name: "Economy")
health = create(:budget_group, budget: budget, name: "Health")
create(:budget_group, budget: budget, name: "Technology")
create(:budget_heading, group: economy, name: "Banking")
create(:budget_heading, group: health, name: "Hospitals")
visit admin_budgets_wizard_budget_group_headings_path(budget, economy)
within(".heading") do
expect(page).to have_content "Banking"
expect(page).not_to have_content "Hospitals"
end
expect(page).not_to have_link "Health"
click_button "Manage headings from a different group"
click_link "Health"
within(".heading") do
expect(page).to have_content "Hospitals"
expect(page).not_to have_content "Banking"
end
expect(page).to have_css ".creation-timeline"
end
end
describe "New" do
scenario "cancel creating a heading" do
visit admin_budgets_wizard_budget_group_headings_path(budget, group)
expect(page).not_to have_field "Heading name"
expect(page).not_to have_button "Cancel"
expect(page).to have_content "Continue to phases"
click_button "Add new heading"
expect(page).to have_field "Heading name"
expect(page).not_to have_button "Add new heading"
expect(page).not_to have_content "Continue to phases"
click_button "Cancel"
expect(page).to have_button "Add new heading"
expect(page).not_to have_field "Heading name"
expect(page).not_to have_button "Cancel"
expect(page).to have_content "Continue to phases"
end
scenario "submit the form with errors" do
visit admin_budgets_wizard_budget_group_headings_path(budget, group)
click_button "Add new heading"
click_button "Create new heading"
expect(page).not_to have_content "Heading created successfully!"
expect(page).to have_css(".is-invalid-label", text: "Heading name")
expect(page).to have_content "can't be blank"
expect(page).to have_button "Create new heading"
expect(page).to have_button "Cancel"
expect(page).not_to have_button "Add new heading"
expect(page).not_to have_content "Continue to phases"
end
end
describe "Edit" do
scenario "update heading" do
create(:budget_heading, group: group, name: "Heading wiht a typo")
visit admin_budgets_wizard_budget_group_headings_path(budget, group)
expect(page).to have_css ".creation-timeline"
within("tr", text: "Heading wiht a typo") { click_link "Edit" }
fill_in "Heading name", with: "Heading without typos"
click_button "Save heading"
expect(page).to have_content "Heading updated successfully"
expect(page).to have_css ".creation-timeline"
expect(page).to have_css "td", exact_text: "Heading without typos"
end
scenario "submit the form with errors and then without errors" do
heading = create(:budget_heading, group: group, name: "Heading wiht a typo")
visit edit_admin_budgets_wizard_budget_group_heading_path(budget, group, heading)
fill_in "Heading name", with: ""
click_button "Save heading"
expect(page).to have_css "#error_explanation"
expect(page).to have_css ".creation-timeline"
fill_in "Heading name", with: "Heading without typos"
click_button "Save heading"
expect(page).to have_content "Heading updated successfully"
expect(page).to have_css ".creation-timeline"
expect(page).to have_css "td", exact_text: "Heading without typos"
end
end
describe "Destroy" do
scenario "delete a heading without investments" do
create(:budget_heading, group: group, name: "Delete me!")
visit admin_budgets_wizard_budget_group_headings_path(budget, group)
within("tr", text: "Delete me!") { accept_confirm { click_link "Delete" } }
expect(page).to have_content "Heading deleted successfully"
expect(page).not_to have_content "Delete me!"
expect(page).to have_css ".creation-timeline"
end
scenario "try to delete a heading with investments" do
heading = create(:budget_heading, group: group, name: "Don't delete me!")
create(:budget_investment, heading: heading)
visit admin_budgets_wizard_budget_group_headings_path(budget, group)
within("tr", text: "Don't delete me!") { accept_confirm { click_link "Delete" } }
expect(page).to have_content "You cannot delete a Heading that has associated investments"
expect(page).to have_content "Don't delete me!"
expect(page).to have_css ".creation-timeline"
end
end
end