Move budget ballot actions to admin budget page

The buttons to create polls associated with a budget were too prominent,
appearing on the table as if they were as used as the link to manage
investments. Most CONSUL installations don't use physical booths, and
would probably wonder what that button is about.

We're moving it to a more discrete place, at the bottom of the budget
page. This way we can also split the action in two: on budgets not
having a poll, we display the button in a not-so-accessible position (at
the bottom of the page), since this button will only be used once per
budget at most. Once the poll has been created, it means this feature is
going to be used, so we display a link to manage ballots more
prominently at the top of the page. If the budget has finished the final
voting stage without creating a poll, we don't show either the link or
the button because this feature can no longer be used.

We're also adding some texts indicating what this feature is about,
since it's probably one of the least understood features in CONSUL
(probably because the interface is very confusing... but that's a
different story).

Since now from the budget page we can access every feature related to
the budget, we can remove the "preview" action from the budgets index
table, since this feature isn't that useful for budgets once they're
published.

Now the budgets table doesn't take as much space as it used to, although
it's still too wide to be handled properly on devices with a small
screen.
This commit is contained in:
Javi Martín
2021-08-31 00:04:08 +02:00
parent 8bb55f0d45
commit 1b407b0702
13 changed files with 114 additions and 66 deletions

View File

@@ -42,6 +42,10 @@
@include hollow-button; @include hollow-button;
} }
.ballots-link {
@include regular-button;
}
.destroy-link { .destroy-link {
@include regular-button($alert-color); @include regular-button($alert-color);
@@ -56,6 +60,7 @@
} }
.calculate-winners-link, .calculate-winners-link,
.ballots-link,
.destroy-link { .destroy-link {
margin-bottom: 0; margin-bottom: 0;
} }

View File

@@ -26,6 +26,11 @@
@include icon-on-top; @include icon-on-top;
} }
.ballots-link {
@include has-fa-icon(archive, solid);
color: $link;
}
.results-link { .results-link {
@include has-fa-icon(poll, solid); @include has-fa-icon(poll, solid);
} }

View File

@@ -1618,8 +1618,7 @@ $font-awesome-icons: (
color: $color-success; color: $color-success;
} }
.manage-link, .manage-link {
.ballots-link {
@include has-fa-icon(archive, solid); @include has-fa-icon(archive, solid);
color: $link; color: $link;
} }

View File

@@ -22,6 +22,10 @@ class Admin::Budgets::ActionsComponent < ApplicationComponent
hint: winners_hint, hint: winners_hint,
html: winners_action html: winners_action
}, },
ballots: {
hint: t("admin.budgets.actions.descriptions.ballots"),
html: ballots_action
},
destroy: { destroy: {
hint: destroy_hint, hint: destroy_hint,
html: destroy_action html: destroy_action
@@ -55,6 +59,27 @@ class Admin::Budgets::ActionsComponent < ApplicationComponent
end end
end end
def ballots_action
if budget.published? && !budget.balloting_finished? && !budget.poll.present?
action(:ballots,
text: t("admin.budgets.actions.ballots"),
path: create_budget_poll_path,
method: :post,
confirm: t("admin.budgets.actions.confirm.ballots"))
end
end
def create_budget_poll_path
balloting_phase = budget.phases.find_by(kind: "balloting")
admin_polls_path(poll: {
name: budget.name,
budget_id: budget.id,
starts_at: balloting_phase.starts_at,
ends_at: balloting_phase.ends_at
})
end
def descriptor_id(action_name) def descriptor_id(action_name)
"#{dom_id(budget, action_name)}_descriptor" "#{dom_id(budget, action_name)}_descriptor"
end end

View File

@@ -5,6 +5,12 @@
path: admin_budget_budget_investments_path(budget)) %> path: admin_budget_budget_investments_path(budget)) %>
<% end %> <% end %>
<% if budget.poll.present? %>
<%= action(:ballots,
text: t("admin.budgets.index.admin_ballots"),
path: admin_poll_booth_assignments_path(budget.poll)) %>
<% end %>
<% if can?(:read_results, budget) %> <% if can?(:read_results, budget) %>
<%= action(:results, text: results_text, path: budget_results_path(budget)) %> <%= action(:results, text: results_text, path: budget_results_path(budget)) %>
<% else %> <% else %>

View File

@@ -2,18 +2,4 @@
<%= actions.action(:investments, <%= actions.action(:investments,
text: t("admin.budgets.index.budget_investments"), text: t("admin.budgets.index.budget_investments"),
path: admin_budget_budget_investments_path(budget)) %> path: admin_budget_budget_investments_path(budget)) %>
<% if budget.poll.present? %>
<%= actions.action(:ballots,
text: t("admin.budgets.index.admin_ballots"),
path: admin_poll_booth_assignments_path(budget.poll)) %>
<% else %>
<%= actions.action(:ballots,
text: t("admin.budgets.index.admin_ballots"),
path: create_budget_poll_path,
method: :post) %>
<% end %>
<%= actions.action(:preview,
text: t("admin.budgets.actions.preview"),
path: budget_path(budget),
target: "_blank") %>
<% end %> <% end %>

View File

@@ -7,17 +7,6 @@ class Admin::Budgets::TableActionsComponent < ApplicationComponent
private private
def create_budget_poll_path
balloting_phase = budget.phases.find_by(kind: "balloting")
admin_polls_path(poll: {
name: budget.name,
budget_id: budget.id,
starts_at: balloting_phase.starts_at,
ends_at: balloting_phase.ends_at
})
end
def actions_component def actions_component
Admin::TableActionsComponent.new( Admin::TableActionsComponent.new(
budget, budget,

View File

@@ -69,9 +69,12 @@ en:
no_activity: There are no moderators activity. no_activity: There are no moderators activity.
budgets: budgets:
actions: actions:
ballots: "Create booths"
confirm: confirm:
ballots: "Are you sure? This action is only necessary if you're going to use physical booths during the final voting phase. Citizens will still be able to vote via web with or without physical booths."
destroy: "Are you sure? This will delete the budget and all its associated groups and headings. This action cannot be undone." destroy: "Are you sure? This will delete the budget and all its associated groups and headings. This action cannot be undone."
descriptions: descriptions:
ballots: "This will let you manage physical ballots. Only use this option if you're going to use physical booths to collect ballots during the final voting phase."
calculate_winners: 'After calculating the results, only administrators will be able to access them. They will be public when the project reaches the "%{phase}" phase <strong>if the option "Show results" is enabled</strong> when editing the budget.' calculate_winners: 'After calculating the results, only administrators will be able to access them. They will be public when the project reaches the "%{phase}" phase <strong>if the option "Show results" is enabled</strong> when editing the budget.'
destroy: "This will delete the budget and all its associated groups and headers. This action cannot be undone." destroy: "This will delete the budget and all its associated groups and headers. This action cannot be undone."
edit: "Edit budget" edit: "Edit budget"

View File

@@ -69,9 +69,12 @@ es:
no_activity: No hay actividad de moderadores. no_activity: No hay actividad de moderadores.
budgets: budgets:
actions: actions:
ballots: "Crear urnas"
confirm: confirm:
ballots: "¿Seguro? Esta acción solo es necesaria si vas a utilizar urnas físicas durante la votación final. Los ciudadanos podrán votar mediante la web con o sin urnas físicas."
destroy: "¿Seguro? Esta acción borrará el presupuesto y todos sus grupos y partidas. Esta acción no se puede deshacer." destroy: "¿Seguro? Esta acción borrará el presupuesto y todos sus grupos y partidas. Esta acción no se puede deshacer."
descriptions: descriptions:
ballots: "Esta acción te permitirá gestionar las urnas de votación. Usa esta opción solamente si vas a utilizar urnas físicas para recoger votos durante la votación final."
calculate_winners: 'Tras calcular los resultados, solamente los administradores tendrán acceso a los mismos. Se harán públicos cuando el presupuesto llegue a la fase "%{phase}" <strong>si se habilita la opción "Mostrar resultados"</strong> al editar el presupuesto.' calculate_winners: 'Tras calcular los resultados, solamente los administradores tendrán acceso a los mismos. Se harán públicos cuando el presupuesto llegue a la fase "%{phase}" <strong>si se habilita la opción "Mostrar resultados"</strong> al editar el presupuesto.'
destroy: "Esta acción borrará el presupuesto y todos sus grupos y partidas. Esta acción no se puede deshacer." destroy: "Esta acción borrará el presupuesto y todos sus grupos y partidas. Esta acción no se puede deshacer."
edit: "Editar presupuesto" edit: "Editar presupuesto"

View File

@@ -0,0 +1,34 @@
require "rails_helper"
describe Admin::Budgets::ActionsComponent, controller: Admin::BaseController do
before { sign_in(create(:administrator).user) }
let(:budget) { create(:budget) }
let(:component) { Admin::Budgets::ActionsComponent.new(budget) }
describe "Create booths button" do
it "is rendered for budgets without polls" do
render_inline component
expect(page.find("form[action*='polls'][method='post']")).to have_button "Create booths"
end
it "is not rendered for finished budgets without polls" do
budget.update!(phase: "finished")
render_inline component
expect(page).not_to have_css "form[action*='polls']"
expect(page).not_to have_button "Create booths"
end
it "is not rendered for budgets with polls" do
budget.poll = create(:poll, budget: budget)
render_inline component
expect(page).not_to have_css "form[action*='polls']"
expect(page).not_to have_button "Create booths"
end
end
end

View File

@@ -97,4 +97,24 @@ describe Admin::Budgets::LinksComponent, controller: Admin::BaseController do
expect(page).not_to have_link "Investment projects" expect(page).not_to have_link "Investment projects"
end end
end end
describe "ballots link" do
let(:budget) { create(:budget) }
let(:component) { Admin::Budgets::LinksComponent.new(budget) }
it "is rendered for budgets with polls" do
budget.poll = create(:poll, budget: budget)
path = Rails.application.routes.url_helpers.admin_poll_booth_assignments_path(budget.poll)
render_inline component
expect(page).to have_link "Ballots", href: path
end
it "is not rendered for budgets without polls" do
render_inline component
expect(page).not_to have_link "Ballots"
end
end
end end

View File

@@ -4,29 +4,13 @@ describe Admin::Budgets::TableActionsComponent, controller: Admin::BaseControlle
let(:budget) { create(:budget) } let(:budget) { create(:budget) }
let(:component) { Admin::Budgets::TableActionsComponent.new(budget) } let(:component) { Admin::Budgets::TableActionsComponent.new(budget) }
it "renders actions to edit budget, manage investments and manage ballots" do it "renders actions to edit budget and manage investments" do
render_inline component render_inline component
expect(page).to have_link count: 3 expect(page).to have_link count: 2
expect(page).to have_link "Investment projects", href: /investments/ expect(page).to have_link "Investment projects", href: /investments/
expect(page).to have_link "Edit", href: /#{budget.id}\Z/ expect(page).to have_link "Edit", href: /#{budget.id}\Z/
expect(page).to have_link "Preview", href: /budgets/
expect(page).to have_button count: 1 expect(page).not_to have_button
expect(page).to have_button "Ballots"
end
it "renders button to create new poll for budgets without polls" do
render_inline component
expect(page).to have_css "form[action*='polls'][method='post']", exact_text: "Ballots"
end
it "renders link to manage ballots for budgets with polls" do
budget.poll = create(:poll, budget: budget)
render_inline component
expect(page).to have_link "Ballots", href: /booth_assignments/
end end
end end

View File

@@ -6,9 +6,9 @@ describe "Admin Budgets", :admin do
budget = create(:budget) budget = create(:budget)
balloting_phase = budget.phases.balloting balloting_phase = budget.phases.balloting
visit admin_budgets_path visit admin_budget_path(budget)
click_button "Ballots" accept_confirm { click_button "Create booths" }
expect(page).to have_current_path(/admin\/polls\/\d+/) expect(page).to have_current_path(/admin\/polls\/\d+/)
expect(page).to have_content(budget.name) expect(page).to have_content(budget.name)
@@ -17,28 +17,17 @@ describe "Admin Budgets", :admin do
end end
scenario "Create poll in current locale if the budget does not have a poll associated" do scenario "Create poll in current locale if the budget does not have a poll associated" do
create(:budget, budget = create(:budget,
name_en: "Budget for climate change", name_en: "Budget for climate change",
name_fr: "Budget pour le changement climatique") name_es: "Presupuesto por el cambio climático")
visit admin_budgets_path visit admin_budget_path(budget)
select "Français", from: "Language:" select "Español", from: "Language:"
click_button "Bulletins de ladmin" accept_confirm { click_button "Crear urnas" }
expect(page).to have_current_path(/admin\/polls\/\d+/) expect(page).to have_current_path(/admin\/polls\/\d+/)
expect(page).to have_content("Budget pour le changement climatique") expect(page).to have_content "Presupuesto por el cambio climático"
end
scenario "Display link to poll if the budget has a poll associated" do
budget = create(:budget)
poll = create(:poll, budget: budget)
visit admin_budgets_path
within "#budget_#{budget.id}" do
expect(page).to have_link "Ballots", href: admin_poll_booth_assignments_path(poll)
end
end end
end end