Refactor participatory budgets in draft mode

Previously the draft mode was a phase of the PB, but that had some
limitations.

Now the phase drafting disappears and therefore the PB can have the
status published or not published (in draft mode).

That will give more flexibility in order to navigate through the
different phases and see how it looks for administrators before
publishing the PB and everybody can see.

By default, the PB is always created in draft mode, so it gives you
the flexibility to adjust and modify anything before publishing it.
This commit is contained in:
Julian Herrero
2020-02-12 15:19:14 +07:00
committed by Javi Martín
parent 0b2b8b557d
commit 28caabecdf
21 changed files with 248 additions and 88 deletions

View File

@@ -657,6 +657,7 @@ code {
.admin-content > header {
align-items: flex-start;
display: flex;
flex-wrap: wrap;
a {
@include regular-button;

View File

@@ -0,0 +1,37 @@
.admin .drafting {
margin-bottom: 2 * $line-height / 3;
margin-left: auto;
@include breakpoint(large) {
align-items: flex-start;
display: flex;
.callout {
flex: 1;
margin-bottom: 0;
}
}
@include breakpoint(small medium only) {
text-align: right;
.callout {
text-align: left;
}
}
.preview-link {
@include has-fa-icon(eye, regular);
@include hollow-button;
}
.publish-link {
@include regular-button;
margin-bottom: 0;
}
.preview-link,
.publish-link {
margin-left: $line-height / 2;
}
}

View File

@@ -0,0 +1,16 @@
<div class="drafting">
<% if can? :publish, budget %>
<div class="callout warning">
<strong><%= t("admin.budgets.edit.drafting") %></strong>
</div>
<% end %>
<%= link_to t("admin.budgets.edit.preview"), budget_path(budget), class: "preview-link", target: "_blank" %>
<% if can? :publish, budget %>
<%= link_to t("admin.budgets.edit.publish"),
publish_admin_budget_path(budget),
method: :patch, class: "publish-link",
data: { confirm: t("admin.actions.confirm") } %>
<% end %>
</div>

View File

@@ -0,0 +1,8 @@
class Admin::Budgets::DraftingComponent < ApplicationComponent
delegate :can?, to: :controller
attr_reader :budget
def initialize(budget)
@budget = budget
end
end

View File

@@ -15,6 +15,7 @@ class Admin::BudgetsController < Admin::BaseController
end
def show
render :edit
end
def new
@@ -23,6 +24,11 @@ class Admin::BudgetsController < Admin::BaseController
def edit
end
def publish
@budget.publish!
redirect_to edit_admin_budget_path(@budget), notice: t("admin.budgets.publish.notice")
end
def calculate_winners
return unless @budget.balloting_process?
@@ -42,9 +48,9 @@ class Admin::BudgetsController < Admin::BaseController
end
def create
@budget = Budget.new(budget_params)
@budget = Budget.new(budget_params.merge(published: false))
if @budget.save
redirect_to admin_budget_path(@budget), notice: t("admin.budgets.create.notice")
redirect_to edit_admin_budget_path(@budget), notice: t("admin.budgets.create.notice")
else
render :new
end

View File

@@ -64,7 +64,7 @@ module BudgetsHelper
end
def budget_published?(budget)
!budget.drafting? || current_user&.administrator?
budget.published? || current_user&.administrator?
end
def current_budget_map_locations

View File

@@ -62,6 +62,7 @@ module Abilities
can :manage, Dashboard::Action
can [:index, :read, :new, :create, :update, :destroy, :calculate_winners], Budget
can :publish, Budget, id: Budget.drafting.ids
can [:read, :create, :update, :destroy], Budget::Group
can [:read, :create, :update, :destroy], Budget::Heading
can [:hide, :admin_update, :toggle_selection], Budget::Investment

View File

@@ -43,7 +43,8 @@ class Budget < ApplicationRecord
after_create :generate_phases
scope :drafting, -> { where(phase: "drafting") }
scope :published, -> { where(published: true) }
scope :drafting, -> { where.not(id: published) }
scope :informing, -> { where(phase: "informing") }
scope :accepting, -> { where(phase: "accepting") }
scope :reviewing, -> { where(phase: "reviewing") }
@@ -59,7 +60,7 @@ class Budget < ApplicationRecord
scope :open, -> { where.not(phase: "finished") }
def self.current
where.not(phase: "drafting").order(:created_at).last
published.order(:created_at).last
end
def current_phase
@@ -86,8 +87,12 @@ class Budget < ApplicationRecord
80
end
def publish!
update!(published: true)
end
def drafting?
phase == "drafting"
!published?
end
def informing?

View File

@@ -1,6 +1,6 @@
class Budget
class Phase < ApplicationRecord
PHASE_KINDS = %w[drafting informing accepting reviewing selecting valuating publishing_prices balloting
PHASE_KINDS = %w[informing accepting reviewing selecting valuating publishing_prices balloting
reviewing_ballots finished].freeze
PUBLISHED_PRICES_PHASES = %w[publishing_prices balloting reviewing_ballots finished].freeze
SUMMARY_MAX_LENGTH = 1000

View File

@@ -1,5 +1,8 @@
<%= back_link_to admin_budgets_path %>
<h2><%= t("admin.budgets.edit.title") %></h2>
<header>
<h2><%= t("admin.budgets.edit.title") %></h2>
<%= render Admin::Budgets::DraftingComponent.new(@budget) %>
</header>
<%= render "/admin/budgets/form" %>

View File

@@ -1,5 +0,0 @@
<%= back_link_to admin_budgets_path %>
<h2><%= @budget.name %></h2>
<%= render "form" %>

View File

@@ -82,8 +82,13 @@ en:
notice: New participatory budget created successfully!
update:
notice: Participatory budget updated successfully
publish:
notice: "Participatory budget published successfully"
edit:
title: Edit Participatory budget
drafting: "This participatory budget is in draft mode, only administrators can see it in the public site. Once it's published it cannot be changed to draft mode again."
preview: "Preview budget"
publish: "Publish budget"
delete: Delete budget
phase: Phase
dates: Dates

View File

@@ -82,8 +82,13 @@ es:
notice: '¡Presupuestos participativos creados con éxito!'
update:
notice: Presupuestos participativos actualizados
publish:
notice: "Presupuestos participativos actualizados"
edit:
title: Editar presupuestos participativos
drafting: "Este presupuesto participativo está en modo borrador, solo los administradores pueden verlo desde la parte pública de la página. Una vez se haya publicado, no se podrá volver a poner en modo borrador otra vez."
preview: "Previsualizar presupuesto"
publish: "Publicar presupuesto"
delete: Eliminar presupuesto
phase: Fase
dates: Fechas

View File

@@ -53,6 +53,7 @@ namespace :admin do
resources :budgets do
member do
patch :publish
put :calculate_winners
end

View File

@@ -0,0 +1,5 @@
class AddPublishedStatusToBudgets < ActiveRecord::Migration[5.2]
def change
add_column :budgets, :published, :boolean
end
end

View File

@@ -360,6 +360,7 @@ ActiveRecord::Schema.define(version: 2021_01_23_100638) do
t.text "description_publishing_prices"
t.text "description_informing"
t.string "voting_style", default: "knapsack"
t.boolean "published"
end
create_table "campaigns", id: :serial, force: :cascade do |t|

View File

@@ -2,6 +2,7 @@ FactoryBot.define do
factory :budget do
sequence(:name) { |n| "#{Faker::Lorem.word} #{n}" }
currency_symbol { "" }
published { true }
phase { "accepting" }
description_drafting { "This budget is drafting" }
description_informing { "This budget is informing" }
@@ -15,7 +16,7 @@ FactoryBot.define do
description_finished { "This budget is finished" }
trait :drafting do
phase { "drafting" }
published { false }
end
trait :informing do

View File

@@ -2,11 +2,10 @@ require "rails_helper"
describe Budget::Phase do
let(:budget) { create(:budget) }
let(:first_phase) { budget.phases.drafting }
let(:second_phase) { budget.phases.informing }
let(:third_phase) { budget.phases.accepting }
let(:fourth_phase) { budget.phases.reviewing }
let(:final_phase) { budget.phases.finished }
let(:informing_phase) { budget.phases.informing }
let(:accepting_phase) { budget.phases.accepting }
let(:reviewing_phase) { budget.phases.reviewing }
let(:finished_phase) { budget.phases.finished }
it_behaves_like "globalizable", :budget_phase
@@ -31,54 +30,53 @@ describe Budget::Phase do
describe "#dates_range_valid?" do
it "is valid when start & end dates are different & consecutive" do
first_phase.assign_attributes(starts_at: Date.current, ends_at: Date.tomorrow)
informing_phase.assign_attributes(starts_at: Date.current, ends_at: Date.tomorrow)
expect(first_phase).to be_valid
expect(informing_phase).to be_valid
end
it "is not valid when dates are equal" do
first_phase.assign_attributes(starts_at: Date.current, ends_at: Date.current)
informing_phase.assign_attributes(starts_at: Date.current, ends_at: Date.current)
expect(first_phase).not_to be_valid
expect(informing_phase).not_to be_valid
end
it "is not valid when start date is later than end date" do
first_phase.assign_attributes(starts_at: Date.tomorrow, ends_at: Date.current)
informing_phase.assign_attributes(starts_at: Date.tomorrow, ends_at: Date.current)
expect(first_phase).not_to be_valid
expect(informing_phase).not_to be_valid
end
end
describe "#prev_phase_dates_valid?" do
let(:error) do
"Start date must be later than the start date of the previous enabled phase"\
" (Draft (Not visible to the public))"
"Start date must be later than the start date of the previous enabled phase (Information)"
end
it "is invalid when start date is same as previous enabled phase start date" do
second_phase.assign_attributes(starts_at: second_phase.prev_enabled_phase.starts_at)
accepting_phase.assign_attributes(starts_at: accepting_phase.prev_enabled_phase.starts_at)
expect(second_phase).not_to be_valid
expect(second_phase.errors.messages[:starts_at]).to include(error)
expect(accepting_phase).not_to be_valid
expect(accepting_phase.errors.messages[:starts_at]).to include(error)
end
it "is invalid when start date is earlier than previous enabled phase start date" do
second_phase.assign_attributes(starts_at: second_phase.prev_enabled_phase.starts_at - 1.day)
accepting_phase.assign_attributes(starts_at: accepting_phase.prev_enabled_phase.starts_at - 1.day)
expect(second_phase).not_to be_valid
expect(second_phase.errors.messages[:starts_at]).to include(error)
expect(accepting_phase).not_to be_valid
expect(accepting_phase.errors.messages[:starts_at]).to include(error)
end
it "is valid when start date is in between previous enabled phase start & end dates" do
second_phase.assign_attributes(starts_at: second_phase.prev_enabled_phase.starts_at + 1.day)
accepting_phase.assign_attributes(starts_at: accepting_phase.prev_enabled_phase.starts_at + 1.day)
expect(second_phase).to be_valid
expect(accepting_phase).to be_valid
end
it "is valid when start date is later than previous enabled phase end date" do
second_phase.assign_attributes(starts_at: second_phase.prev_enabled_phase.ends_at + 1.day)
accepting_phase.assign_attributes(starts_at: accepting_phase.prev_enabled_phase.ends_at + 1.day)
expect(second_phase).to be_valid
expect(accepting_phase).to be_valid
end
end
@@ -88,29 +86,29 @@ describe Budget::Phase do
end
it "is invalid when end date is same as next enabled phase end date" do
second_phase.assign_attributes(ends_at: second_phase.next_enabled_phase.ends_at)
informing_phase.assign_attributes(ends_at: informing_phase.next_enabled_phase.ends_at)
expect(second_phase).not_to be_valid
expect(second_phase.errors.messages[:ends_at]).to include(error)
expect(informing_phase).not_to be_valid
expect(informing_phase.errors.messages[:ends_at]).to include(error)
end
it "is invalid when end date is later than next enabled phase end date" do
second_phase.assign_attributes(ends_at: second_phase.next_enabled_phase.ends_at + 1.day)
informing_phase.assign_attributes(ends_at: informing_phase.next_enabled_phase.ends_at + 1.day)
expect(second_phase).not_to be_valid
expect(second_phase.errors.messages[:ends_at]).to include(error)
expect(informing_phase).not_to be_valid
expect(informing_phase.errors.messages[:ends_at]).to include(error)
end
it "is valid when end date is in between next enabled phase start & end dates" do
second_phase.assign_attributes(ends_at: second_phase.next_enabled_phase.ends_at - 1.day)
informing_phase.assign_attributes(ends_at: informing_phase.next_enabled_phase.ends_at - 1.day)
expect(second_phase).to be_valid
expect(informing_phase).to be_valid
end
it "is valid when end date is earlier than next enabled phase start date" do
second_phase.assign_attributes(ends_at: second_phase.next_enabled_phase.starts_at - 1.day)
informing_phase.assign_attributes(ends_at: informing_phase.next_enabled_phase.starts_at - 1.day)
expect(second_phase).to be_valid
expect(informing_phase).to be_valid
end
end
end
@@ -128,50 +126,50 @@ describe Budget::Phase do
end
describe "#adjust_date_ranges" do
let(:prev_enabled_phase) { second_phase.prev_enabled_phase }
let(:next_enabled_phase) { second_phase.next_enabled_phase }
let(:prev_enabled_phase) { accepting_phase.prev_enabled_phase }
let(:next_enabled_phase) { accepting_phase.next_enabled_phase }
describe "when enabled" do
it "adjusts previous enabled phase end date to its own start date" do
expect(prev_enabled_phase.ends_at).to eq(second_phase.starts_at)
expect(prev_enabled_phase.ends_at).to eq(accepting_phase.starts_at)
end
it "adjusts next enabled phase start date to its own end date" do
expect(next_enabled_phase.starts_at).to eq(second_phase.ends_at)
expect(next_enabled_phase.starts_at).to eq(accepting_phase.ends_at)
end
end
describe "when being enabled" do
before do
second_phase.update!(enabled: false,
accepting_phase.update!(enabled: false,
starts_at: Date.current,
ends_at: Date.current + 2.days)
end
it "adjusts previous enabled phase end date to its own start date" do
expect { second_phase.update(enabled: true) }
expect { accepting_phase.update(enabled: true) }
.to change { prev_enabled_phase.ends_at.to_date }.to(Date.current)
end
it "adjusts next enabled phase start date to its own end date" do
expect do
second_phase.update(enabled: true)
accepting_phase.update(enabled: true)
end.to change { next_enabled_phase.starts_at.to_date }.to(Date.current + 2.days)
end
end
describe "when disabled" do
before do
second_phase.update!(enabled: false)
accepting_phase.update!(enabled: false)
end
it "doesn't change previous enabled phase end date" do
expect { second_phase.update(starts_at: Date.current, ends_at: Date.current + 2.days) }
expect { accepting_phase.update(starts_at: Date.current, ends_at: Date.current + 2.days) }
.not_to change { prev_enabled_phase.ends_at }
end
it "doesn't change next enabled phase start date" do
expect { second_phase.update(starts_at: Date.current, ends_at: Date.current + 2.days) }
expect { accepting_phase.update(starts_at: Date.current, ends_at: Date.current + 2.days) }
.not_to change { next_enabled_phase.starts_at }
end
end
@@ -179,7 +177,7 @@ describe Budget::Phase do
describe "when being disabled" do
it "doesn't adjust previous enabled phase end date to its own start date" do
expect do
second_phase.update(enabled: false,
accepting_phase.update(enabled: false,
starts_at: Date.current,
ends_at: Date.current + 2.days)
end.not_to change { prev_enabled_phase.ends_at }
@@ -187,7 +185,7 @@ describe Budget::Phase do
it "adjusts next enabled phase start date to its own start date" do
expect do
second_phase.update(enabled: false,
accepting_phase.update(enabled: false,
starts_at: Date.current,
ends_at: Date.current + 2.days)
end.to change { next_enabled_phase.starts_at.to_date }.to(Date.current)
@@ -197,22 +195,25 @@ describe Budget::Phase do
describe "next & prev enabled phases" do
before do
second_phase.update(enabled: false)
accepting_phase.update!(enabled: false)
%w[selecting reviewing_ballots balloting publishing_prices valuating].each do |phase|
budget.phases.send(phase).update(enabled: false)
end
end
describe "#next_enabled_phase" do
it "returns the right next enabled phase" do
expect(first_phase.reload.next_enabled_phase).to eq(third_phase)
expect(third_phase.reload.next_enabled_phase).to eq(fourth_phase)
expect(final_phase.reload.next_enabled_phase).to eq(nil)
expect(informing_phase.reload.next_enabled_phase).to eq(reviewing_phase)
expect(reviewing_phase.reload.next_enabled_phase).to eq(finished_phase)
expect(finished_phase.reload.next_enabled_phase).to eq(nil)
end
end
describe "#prev_enabled_phase" do
it "returns the right previous enabled phase" do
expect(first_phase.reload.prev_enabled_phase).to eq(nil)
expect(third_phase.reload.prev_enabled_phase).to eq(first_phase)
expect(fourth_phase.reload.prev_enabled_phase).to eq(third_phase)
expect(informing_phase.reload.prev_enabled_phase).to eq(nil)
expect(reviewing_phase.reload.prev_enabled_phase).to eq(informing_phase)
expect(finished_phase.reload.prev_enabled_phase).to eq(reviewing_phase)
end
end
end

View File

@@ -32,6 +32,36 @@ describe Budget do
expect(Budget.valuating_or_later).to be_empty
end
end
describe ".drafting" do
it "returns unpublished budgets" do
undefined = create(:budget, published: nil)
drafting = create(:budget, published: false)
expect(Budget.drafting).to match_array([undefined, drafting])
end
it "does not return published budgets" do
create(:budget, published: true)
expect(Budget.drafting).to be_empty
end
end
describe ".published" do
it "does not return unpublished budgets" do
create(:budget, published: nil)
create(:budget, published: false)
expect(Budget.published).to be_empty
end
it "returns published budgets" do
published = create(:budget, published: true)
expect(Budget.published).to eq [published]
end
end
end
describe "name" do
@@ -96,9 +126,6 @@ describe Budget do
end
it "produces auxiliary methods" do
budget.phase = "drafting"
expect(budget).to be_drafting
budget.phase = "accepting"
expect(budget).to be_accepting
@@ -248,7 +275,6 @@ describe Budget do
end
describe "#generate_phases" do
let(:drafting_phase) { budget.phases.drafting }
let(:informing_phase) { budget.phases.informing }
let(:accepting_phase) { budget.phases.accepting }
let(:reviewing_phase) { budget.phases.reviewing }
@@ -262,7 +288,6 @@ describe Budget do
it "generates all phases linked in correct order" do
expect(budget.phases.count).to eq(Budget::Phase::PHASE_KINDS.count)
expect(drafting_phase.next_phase).to eq(informing_phase)
expect(informing_phase.next_phase).to eq(accepting_phase)
expect(accepting_phase.next_phase).to eq(reviewing_phase)
expect(reviewing_phase.next_phase).to eq(selecting_phase)
@@ -273,8 +298,7 @@ describe Budget do
expect(reviewing_ballots_phase.next_phase).to eq(finished_phase)
expect(finished_phase.next_phase).to eq(nil)
expect(drafting_phase.prev_phase).to eq(nil)
expect(informing_phase.prev_phase).to eq(drafting_phase)
expect(informing_phase.prev_phase).to eq(nil)
expect(accepting_phase.prev_phase).to eq(informing_phase)
expect(reviewing_phase.prev_phase).to eq(accepting_phase)
expect(selecting_phase.prev_phase).to eq(reviewing_phase)

View File

@@ -15,19 +15,20 @@ describe "Admin budgets", :admin do
let!(:budget) { create(:budget, slug: "budget_slug") }
scenario "finds budget by slug" do
visit admin_budget_path("budget_slug")
expect(page).to have_content(budget.name)
visit edit_admin_budget_path("budget_slug")
expect(page).to have_content("Edit Participatory budget")
end
scenario "raises an error if budget slug is not found" do
expect do
visit admin_budget_path("wrong_budget")
visit edit_admin_budget_path("wrong_budget")
end.to raise_error ActiveRecord::RecordNotFound
end
scenario "raises an error if budget id is not found" do
expect do
visit admin_budget_path(0)
visit edit_admin_budget_path(0)
end.to raise_error ActiveRecord::RecordNotFound
end
end
@@ -107,8 +108,8 @@ describe "Admin budgets", :admin do
click_button "Create Budget"
expect(page).to have_content "New participatory budget created successfully!"
expect(page).to have_content "M30 - Summer campaign"
expect(Budget.last.voting_style).to eq "knapsack"
expect(page).to have_field "Name", with: "M30 - Summer campaign"
expect(page).to have_select "Final voting style", selected: "Knapsack"
end
scenario "Create budget - Approval voting", :js do
@@ -161,6 +162,49 @@ describe "Admin budgets", :admin do
end
end
context "Create", :js do
scenario "A new budget is always created in draft mode" do
visit admin_budgets_path
click_link "Create new budget"
fill_in "Name", with: "M30 - Summer campaign"
select "Accepting projects", from: "budget[phase]"
click_button "Create Budget"
expect(page).to have_content "New participatory budget created successfully!"
expect(page).to have_content "This participatory budget is in draft mode"
expect(page).to have_link "Preview budget"
expect(page).to have_link "Publish budget"
end
end
context "Publish", :js do
let(:budget) { create(:budget, :drafting) }
scenario "Can preview budget before it is published" do
visit edit_admin_budget_path(budget)
within_window(window_opened_by { click_link "Preview budget" }) do
expect(page).to have_current_path budget_path(budget)
end
end
scenario "Can preview a budget after it is published" do
visit edit_admin_budget_path(budget)
accept_confirm { click_link "Publish budget" }
expect(page).to have_content "Participatory budget published successfully"
expect(page).not_to have_content "This participatory budget is in draft mode"
expect(page).not_to have_link "Publish budget"
within_window(window_opened_by { click_link "Preview budget" }) do
expect(page).to have_current_path budget_path(budget)
end
end
end
context "Destroy" do
let!(:budget) { create(:budget) }
let(:heading) { create(:budget_heading, budget: budget) }
@@ -242,7 +286,7 @@ describe "Admin budgets", :admin do
end
scenario "Changing name for current locale will update the slug if budget is in draft phase", :js do
budget.update!(phase: "drafting")
budget.update!(published: false)
old_slug = budget.slug
visit edit_admin_budget_path(budget)

View File

@@ -168,7 +168,7 @@ describe "Budgets" do
scenario "Not show investment links earlier of balloting " do
budget = create(:budget)
create(:budget_heading, budget: budget)
phases_without_links = ["drafting", "informing"]
phases_without_links = ["informing"]
not_allowed_phase_list = Budget::Phase::PHASE_KINDS -
phases_without_links -
allowed_phase_list
@@ -205,9 +205,10 @@ describe "Budgets" do
scenario "Index shows only published phases" do
budget.update!(phase: :finished)
phases = budget.phases
phases.drafting.update!(starts_at: "30-12-2017", ends_at: "31-12-2017", enabled: true,
description: "Description of drafting phase",
summary: "<p>This is the summary for drafting phase</p>")
phases.informing.update!(starts_at: "30-12-2017", ends_at: "31-12-2017", enabled: true,
description: "Description of informing phase",
summary: "<p>This is the summary for informing phase</p>")
phases.accepting.update!(starts_at: "01-01-2018", ends_at: "10-01-2018", enabled: true,
description: "Description of accepting phase",
@@ -243,8 +244,6 @@ describe "Budgets" do
visit budgets_path
expect(page).not_to have_content "This is the summary for drafting phase"
expect(page).not_to have_content "December 30, 2017 - December 31, 2017"
expect(page).not_to have_content "This is the summary for reviewing phase"
expect(page).not_to have_content "January 11, 2018 - January 20, 2018"
expect(page).not_to have_content "This is the summary for valuating phase"
@@ -254,6 +253,8 @@ describe "Budgets" do
expect(page).not_to have_content "This is the summary for reviewing_ballots phase"
expect(page).not_to have_content "March 11, 2018 - March 20, 2018"
expect(page).to have_content "This is the summary for informing phase"
expect(page).to have_content "December 30, 2017 - December 31, 2017"
expect(page).to have_content "This is the summary for accepting phase"
expect(page).to have_content "January 01, 2018 - January 20, 2018"
expect(page).to have_content "This is the summary for selecting phase"
@@ -478,7 +479,7 @@ describe "Budgets" do
before do
logout
budget.update!(phase: "drafting")
budget.update!(published: false)
create(:budget)
end