Merge pull request #2323 from consul/feature/budget_phases
Create Budget::Phases backend
This commit is contained in:
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
||||
- Added Capistrano task to automate maintenance mode https://github.com/consul/consul/pull/1932
|
||||
- Added actions to edit and delete a budget's headings https://github.com/consul/consul/pull/1917
|
||||
- Allow Budget Investments to be Related to other content https://github.com/consul/consul/pull/2311
|
||||
- New Budget::Phase model to add dates, enabling and more https://github.com/consul/consul/pull/2323
|
||||
|
||||
### Changed
|
||||
- Updated multiple minor & patch gem versions thanks to [Depfu](https://depfu.com)
|
||||
@@ -28,6 +29,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
||||
- Design Improvements https://github.com/consul/consul/pull/2327
|
||||
|
||||
### Deprecated
|
||||
- Budget's `description_*` columns will be erased from database in next release. Please run rake task `budgets:phases:generate_missing` to migrate them. Details at Warning section of https://github.com/consul/consul/pull/2323
|
||||
|
||||
### Removed
|
||||
- Spending Proposals urls from sitemap, that model is getting entirely deprecated soon.
|
||||
|
||||
@@ -54,7 +54,7 @@ class Admin::BudgetsController < Admin::BaseController
|
||||
private
|
||||
|
||||
def budget_params
|
||||
descriptions = Budget::PHASES.map{|p| "description_#{p}"}.map(&:to_sym)
|
||||
descriptions = Budget::Phase::PHASE_KINDS.map{|p| "description_#{p}"}.map(&:to_sym)
|
||||
valid_attributes = [:name, :phase, :currency_symbol] + descriptions
|
||||
params.require(:budget).permit(*valid_attributes)
|
||||
end
|
||||
|
||||
@@ -7,7 +7,7 @@ module BudgetsHelper
|
||||
end
|
||||
|
||||
def budget_phases_select_options
|
||||
Budget::PHASES.map { |ph| [ t("budgets.phase.#{ph}"), ph ] }
|
||||
Budget::Phase::PHASE_KINDS.map { |ph| [ t("budgets.phase.#{ph}"), ph ] }
|
||||
end
|
||||
|
||||
def budget_currency_symbol_select_options
|
||||
|
||||
@@ -3,14 +3,10 @@ class Budget < ActiveRecord::Base
|
||||
include Measurable
|
||||
include Sluggable
|
||||
|
||||
PHASES = %w(drafting accepting reviewing selecting valuating publishing_prices
|
||||
balloting reviewing_ballots finished).freeze
|
||||
PUBLISHED_PRICES_PHASES = %w(publishing_prices balloting reviewing_ballots finished).freeze
|
||||
|
||||
CURRENCY_SYMBOLS = %w(€ $ £ ¥).freeze
|
||||
|
||||
validates :name, presence: true, uniqueness: true
|
||||
validates :phase, inclusion: { in: PHASES }
|
||||
validates :phase, inclusion: { in: Budget::Phase::PHASE_KINDS }
|
||||
validates :currency_symbol, presence: true
|
||||
validates :slug, presence: true, format: /\A[a-z0-9\-_]+\z/
|
||||
|
||||
@@ -18,9 +14,12 @@ class Budget < ActiveRecord::Base
|
||||
has_many :ballots, dependent: :destroy
|
||||
has_many :groups, dependent: :destroy
|
||||
has_many :headings, through: :groups
|
||||
has_many :phases, class_name: Budget::Phase
|
||||
|
||||
before_validation :sanitize_descriptions
|
||||
|
||||
after_create :generate_phases
|
||||
|
||||
scope :drafting, -> { where(phase: "drafting") }
|
||||
scope :accepting, -> { where(phase: "accepting") }
|
||||
scope :reviewing, -> { where(phase: "reviewing") }
|
||||
@@ -30,18 +29,27 @@ class Budget < ActiveRecord::Base
|
||||
scope :balloting, -> { where(phase: "balloting") }
|
||||
scope :reviewing_ballots, -> { where(phase: "reviewing_ballots") }
|
||||
scope :finished, -> { where(phase: "finished") }
|
||||
|
||||
scope :open, -> { where.not(phase: "finished") }
|
||||
|
||||
def self.current
|
||||
where.not(phase: "drafting").last
|
||||
end
|
||||
|
||||
def description
|
||||
send("description_#{phase}").try(:html_safe)
|
||||
def current_phase
|
||||
phases.send(phase)
|
||||
end
|
||||
|
||||
def self.description_max_length
|
||||
2000
|
||||
def description
|
||||
description_for_phase(phase)
|
||||
end
|
||||
|
||||
def description_for_phase(phase)
|
||||
if phases.exists? && phases.send(phase).description.present?
|
||||
phases.send(phase).description
|
||||
else
|
||||
send("description_#{phase}").try(:html_safe)
|
||||
end
|
||||
end
|
||||
|
||||
def self.title_max_length
|
||||
@@ -85,7 +93,7 @@ class Budget < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def published_prices?
|
||||
PUBLISHED_PRICES_PHASES.include?(phase)
|
||||
Budget::Phase::PUBLISHED_PRICES_PHASES.include?(phase)
|
||||
end
|
||||
|
||||
def balloting_process?
|
||||
@@ -144,12 +152,25 @@ class Budget < ActiveRecord::Base
|
||||
|
||||
private
|
||||
|
||||
def sanitize_descriptions
|
||||
s = WYSIWYGSanitizer.new
|
||||
PHASES.each do |phase|
|
||||
sanitized = s.sanitize(send("description_#{phase}"))
|
||||
send("description_#{phase}=", sanitized)
|
||||
end
|
||||
def sanitize_descriptions
|
||||
s = WYSIWYGSanitizer.new
|
||||
Budget::Phase::PHASE_KINDS.each do |phase|
|
||||
sanitized = s.sanitize(send("description_#{phase}"))
|
||||
send("description_#{phase}=", sanitized)
|
||||
end
|
||||
end
|
||||
|
||||
def generate_phases
|
||||
Budget::Phase::PHASE_KINDS.each do |phase|
|
||||
Budget::Phase.create(
|
||||
budget: self,
|
||||
kind: phase,
|
||||
prev_phase: phases&.last,
|
||||
starts_at: phases&.last&.ends_at || Date.current,
|
||||
ends_at: (phases&.last&.ends_at || Date.current) + 1.month
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
85
app/models/budget/phase.rb
Normal file
85
app/models/budget/phase.rb
Normal file
@@ -0,0 +1,85 @@
|
||||
class Budget
|
||||
class Phase < ActiveRecord::Base
|
||||
PHASE_KINDS = %w(drafting accepting reviewing selecting valuating publishing_prices balloting
|
||||
reviewing_ballots finished).freeze
|
||||
PUBLISHED_PRICES_PHASES = %w(publishing_prices balloting reviewing_ballots finished).freeze
|
||||
DESCRIPTION_MAX_LENGTH = 2000
|
||||
|
||||
belongs_to :budget
|
||||
belongs_to :next_phase, class_name: 'Budget::Phase', foreign_key: :next_phase_id
|
||||
has_one :prev_phase, class_name: 'Budget::Phase', foreign_key: :next_phase_id
|
||||
|
||||
validates :budget, presence: true
|
||||
validates :kind, presence: true, uniqueness: { scope: :budget }, inclusion: { in: PHASE_KINDS }
|
||||
validates :description, length: { maximum: DESCRIPTION_MAX_LENGTH }
|
||||
validate :invalid_dates_range?
|
||||
validate :prev_phase_dates_valid?
|
||||
validate :next_phase_dates_valid?
|
||||
|
||||
before_validation :sanitize_description
|
||||
|
||||
after_save :adjust_date_ranges
|
||||
|
||||
scope :enabled, -> { where(enabled: true) }
|
||||
scope :drafting, -> { find_by_kind('drafting') }
|
||||
scope :accepting, -> { find_by_kind('accepting')}
|
||||
scope :reviewing, -> { find_by_kind('reviewing')}
|
||||
scope :selecting, -> { find_by_kind('selecting')}
|
||||
scope :valuating, -> { find_by_kind('valuating')}
|
||||
scope :publishing_prices, -> { find_by_kind('publishing_prices')}
|
||||
scope :balloting, -> { find_by_kind('balloting')}
|
||||
scope :reviewing_ballots, -> { find_by_kind('reviewing_ballots')}
|
||||
scope :finished, -> { find_by_kind('finished')}
|
||||
|
||||
def next_enabled_phase
|
||||
next_phase&.enabled? ? next_phase : next_phase&.next_enabled_phase
|
||||
end
|
||||
|
||||
def prev_enabled_phase
|
||||
prev_phase&.enabled? ? prev_phase : prev_phase&.prev_enabled_phase
|
||||
end
|
||||
|
||||
def adjust_date_ranges
|
||||
if enabled?
|
||||
next_enabled_phase&.update_column(:starts_at, ends_at)
|
||||
prev_enabled_phase&.update_column(:ends_at, starts_at)
|
||||
elsif enabled_changed?
|
||||
next_enabled_phase&.update_column(:starts_at, starts_at)
|
||||
end
|
||||
end
|
||||
|
||||
def invalid_dates_range?
|
||||
if starts_at.present? && ends_at.present? && starts_at >= ends_at
|
||||
errors.add(:starts_at, I18n.t('budgets.phases.errors.dates_range_invalid'))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prev_phase_dates_valid?
|
||||
if enabled? && starts_at.present? && prev_enabled_phase.present?
|
||||
prev_enabled_phase.assign_attributes(ends_at: starts_at)
|
||||
if prev_enabled_phase.invalid_dates_range?
|
||||
phase_name = I18n.t("budgets.phase.#{prev_enabled_phase.kind}")
|
||||
error = I18n.t('budgets.phases.errors.prev_phase_dates_invalid', phase_name: phase_name)
|
||||
errors.add(:starts_at, error)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def next_phase_dates_valid?
|
||||
if enabled? && ends_at.present? && next_enabled_phase.present?
|
||||
next_enabled_phase.assign_attributes(starts_at: ends_at)
|
||||
if next_enabled_phase.invalid_dates_range?
|
||||
phase_name = I18n.t("budgets.phase.#{next_enabled_phase.kind}")
|
||||
error = I18n.t('budgets.phases.errors.next_phase_dates_invalid', phase_name: phase_name)
|
||||
errors.add(:ends_at, error)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def sanitize_description
|
||||
self.description = WYSIWYGSanitizer.new.sanitize(description)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
<%= f.text_field :name, maxlength: Budget.title_max_length %>
|
||||
|
||||
<% Budget::PHASES.each do |phase| %>
|
||||
<% Budget::Phase::PHASE_KINDS.each do |phase| %>
|
||||
<div class="margin-top">
|
||||
<%= f.cktext_area "description_#{phase}", maxlength: Budget.description_max_length, ckeditor: { language: I18n.locale } %>
|
||||
<%= f.cktext_area "description_#{phase}", maxlength: Budget::Phase::DESCRIPTION_MAX_LENGTH, ckeditor: { language: I18n.locale } %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<% provide :title, t("budgets.results.page_title", budget: @budget.name) %>
|
||||
<% content_for :meta_description do %><%= @budget.description_finished %><% end %>
|
||||
<% content_for :meta_description do %><%= @budget.description_for_phase('finished') %><% end %>
|
||||
<% provide :social_media_meta_tags do %>
|
||||
<%= render "shared/social_media_meta_tags",
|
||||
social_url: budget_results_url(@budget),
|
||||
social_title: @budget.name,
|
||||
social_description: @budget.description_finished %>
|
||||
social_description: @budget.description_for_phase('finished') %>
|
||||
<% end %>
|
||||
<% content_for :canonical do %>
|
||||
<%= render "shared/canonical", href: budget_results_url(@budget) %>
|
||||
|
||||
@@ -158,3 +158,8 @@ en:
|
||||
accepted: "Accepted spending proposal: "
|
||||
discarded: "Discarded spending proposal: "
|
||||
incompatibles: Incompatibles
|
||||
phases:
|
||||
errors:
|
||||
dates_range_invalid: "Start date can't be equal or later than End date"
|
||||
prev_phase_dates_invalid: "Start date must be later than the start date of the previous enabled phase (%{phase_name})"
|
||||
next_phase_dates_invalid: "End date must be earlier than the end date of the next enabled phase (%{phase_name})"
|
||||
@@ -158,3 +158,8 @@ es:
|
||||
accepted: 'Propuesta de inversión aceptada: '
|
||||
discarded: 'Propuesta de inversión descartada: '
|
||||
incompatibles: Incompatibles
|
||||
phases:
|
||||
errors:
|
||||
dates_range_invalid: "La fecha de comienzo no puede ser igual o superior a la de finalización"
|
||||
prev_phase_dates_invalid: "La fecha de inicio debe ser posterior a la fecha de inicio de la anterior fase habilitada (%{phase_name})"
|
||||
next_phase_dates_invalid: "La fecha de fin debe ser anterior a la fecha de fin de la siguiente fase habilitada (%{phase_name}) "
|
||||
@@ -401,8 +401,8 @@ section "Creating Valuation Assignments" do
|
||||
end
|
||||
|
||||
section "Creating Budgets" do
|
||||
Budget::PHASES.each_with_index do |phase, i|
|
||||
descriptions = Hash[Budget::PHASES.map do |p|
|
||||
Budget::Phase::PHASE_KINDS.each_with_index do |phase, i|
|
||||
descriptions = Hash[Budget::Phase::PHASE_KINDS.map do |p|
|
||||
["description_#{p}",
|
||||
"<p>#{Faker::Lorem.paragraphs(2).join('</p><p>')}</p>"]
|
||||
end]
|
||||
|
||||
14
db/migrate/20180112123641_create_budget_phases.rb
Normal file
14
db/migrate/20180112123641_create_budget_phases.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
class CreateBudgetPhases < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :budget_phases do |t|
|
||||
t.references :budget
|
||||
t.references :next_phase, index: true
|
||||
t.string :kind, null: false, index: true
|
||||
t.text :summary
|
||||
t.text :description
|
||||
t.datetime :starts_at, index: true
|
||||
t.datetime :ends_at, index: true
|
||||
t.boolean :enabled, default: true
|
||||
end
|
||||
end
|
||||
end
|
||||
18
db/schema.rb
18
db/schema.rb
@@ -11,7 +11,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20180109175851) do
|
||||
ActiveRecord::Schema.define(version: 20180112123641) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
@@ -170,6 +170,22 @@ ActiveRecord::Schema.define(version: 20180109175851) do
|
||||
add_index "budget_investments", ["heading_id"], name: "index_budget_investments_on_heading_id", using: :btree
|
||||
add_index "budget_investments", ["tsv"], name: "index_budget_investments_on_tsv", using: :gin
|
||||
|
||||
create_table "budget_phases", force: :cascade do |t|
|
||||
t.integer "budget_id"
|
||||
t.integer "next_phase_id"
|
||||
t.string "kind", null: false
|
||||
t.text "summary"
|
||||
t.text "description"
|
||||
t.datetime "starts_at"
|
||||
t.datetime "ends_at"
|
||||
t.boolean "enabled", default: true
|
||||
end
|
||||
|
||||
add_index "budget_phases", ["ends_at"], name: "index_budget_phases_on_ends_at", using: :btree
|
||||
add_index "budget_phases", ["kind"], name: "index_budget_phases_on_kind", using: :btree
|
||||
add_index "budget_phases", ["next_phase_id"], name: "index_budget_phases_on_next_phase_id", using: :btree
|
||||
add_index "budget_phases", ["starts_at"], name: "index_budget_phases_on_starts_at", using: :btree
|
||||
|
||||
create_table "budget_reclassified_votes", force: :cascade do |t|
|
||||
t.integer "user_id"
|
||||
t.integer "investment_id"
|
||||
|
||||
@@ -13,4 +13,22 @@ namespace :budgets do
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
namespace :phases do
|
||||
desc "Generates Phases for existing Budgets without them & migrates description_* attributes"
|
||||
task generate_missing: :environment do
|
||||
Budget.where.not(id: Budget::Phase.all.pluck(:budget_id).uniq.compact).each do |budget|
|
||||
Budget::Phase::PHASE_KINDS.each do |phase|
|
||||
Budget::Phase.create(
|
||||
budget: budget,
|
||||
kind: phase,
|
||||
description: budget.send("description_#{phase}"),
|
||||
prev_phase: phases&.last,
|
||||
starts_at: phases&.last&.ends_at || Date.current,
|
||||
ends_at: (phases&.last&.ends_at || Date.current) + 1.month
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -341,7 +341,16 @@ FactoryBot.define do
|
||||
feasibility "feasible"
|
||||
valuation_finished true
|
||||
end
|
||||
end
|
||||
|
||||
factory :budget_phase, class: 'Budget::Phase' do
|
||||
budget
|
||||
kind :balloting
|
||||
summary Faker::Lorem.sentence(3)
|
||||
description Faker::Lorem.sentence(10)
|
||||
starts_at Date.yesterday
|
||||
ends_at Date.tomorrow
|
||||
enabled true
|
||||
end
|
||||
|
||||
factory :image do
|
||||
|
||||
@@ -426,7 +426,7 @@ feature 'Budget Investments' do
|
||||
context "When investment with price is selected" do
|
||||
|
||||
scenario "Price & explanation is shown when Budget is on published prices phase" do
|
||||
Budget::PUBLISHED_PRICES_PHASES.each do |phase|
|
||||
Budget::Phase::PUBLISHED_PRICES_PHASES.each do |phase|
|
||||
budget.update(phase: phase)
|
||||
visit budget_investment_path(budget_id: budget.id, id: investment.id)
|
||||
|
||||
@@ -440,7 +440,7 @@ feature 'Budget Investments' do
|
||||
end
|
||||
|
||||
scenario "Price & explanation isn't shown when Budget is not on published prices phase" do
|
||||
(Budget::PHASES - Budget::PUBLISHED_PRICES_PHASES).each do |phase|
|
||||
(Budget::Phase::PHASE_KINDS - Budget::Phase::PUBLISHED_PRICES_PHASES).each do |phase|
|
||||
budget.update(phase: phase)
|
||||
visit budget_investment_path(budget_id: budget.id, id: investment.id)
|
||||
|
||||
@@ -461,7 +461,7 @@ feature 'Budget Investments' do
|
||||
end
|
||||
|
||||
scenario "Price & explanation isn't shown for any Budget's phase" do
|
||||
Budget::PHASES.each do |phase|
|
||||
Budget::Phase::PHASE_KINDS.each do |phase|
|
||||
budget.update(phase: phase)
|
||||
visit budget_investment_path(budget_id: budget.id, id: investment.id)
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ feature 'Results' do
|
||||
end
|
||||
|
||||
scenario "If budget is in a phase different from finished results can't be accessed" do
|
||||
budget.update(phase: (Budget::PHASES - ['drafting', 'finished']).sample)
|
||||
budget.update(phase: (Budget::Phase::PHASE_KINDS - ['drafting', 'finished']).sample)
|
||||
visit budget_path(budget)
|
||||
expect(page).not_to have_link "See results"
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ feature 'Tags' do
|
||||
let!(:investment3) { create(:budget_investment, heading: heading, tag_list: newer_tag) }
|
||||
|
||||
scenario 'Display user tags' do
|
||||
Budget::PHASES.each do |phase|
|
||||
Budget::Phase::PHASE_KINDS.each do |phase|
|
||||
budget.update(phase: phase)
|
||||
|
||||
login_as(admin) if budget.drafting?
|
||||
@@ -197,7 +197,7 @@ feature 'Tags' do
|
||||
end
|
||||
|
||||
scenario "Filter by user tags" do
|
||||
Budget::PHASES.each do |phase|
|
||||
Budget::Phase::PHASE_KINDS.each do |phase|
|
||||
budget.update(phase: phase)
|
||||
|
||||
if budget.balloting?
|
||||
@@ -230,7 +230,7 @@ feature 'Tags' do
|
||||
let!(:investment3) { create(:budget_investment, heading: heading, tag_list: tag_economia.name) }
|
||||
|
||||
scenario 'Display category tags' do
|
||||
Budget::PHASES.each do |phase|
|
||||
Budget::Phase::PHASE_KINDS.each do |phase|
|
||||
budget.update(phase: phase)
|
||||
|
||||
login_as(admin) if budget.drafting?
|
||||
@@ -244,7 +244,7 @@ feature 'Tags' do
|
||||
end
|
||||
|
||||
scenario "Filter by category tags" do
|
||||
Budget::PHASES.each do |phase|
|
||||
Budget::Phase::PHASE_KINDS.each do |phase|
|
||||
budget.update(phase: phase)
|
||||
|
||||
if budget.balloting?
|
||||
|
||||
@@ -141,7 +141,7 @@ describe Budget::Investment do
|
||||
end
|
||||
|
||||
it "returns false in any other phase" do
|
||||
Budget::PHASES.reject {|phase| phase == "selecting"}.each do |phase|
|
||||
Budget::Phase::PHASE_KINDS.reject {|phase| phase == "selecting"}.each do |phase|
|
||||
budget = create(:budget, phase: phase)
|
||||
investment = create(:budget_investment, budget: budget)
|
||||
|
||||
@@ -159,7 +159,7 @@ describe Budget::Investment do
|
||||
end
|
||||
|
||||
it "returns false in any other phase" do
|
||||
Budget::PHASES.reject {|phase| phase == "valuating"}.each do |phase|
|
||||
Budget::Phase::PHASE_KINDS.reject {|phase| phase == "valuating"}.each do |phase|
|
||||
budget = create(:budget, phase: phase)
|
||||
investment = create(:budget_investment, budget: budget)
|
||||
|
||||
@@ -184,7 +184,7 @@ describe Budget::Investment do
|
||||
end
|
||||
|
||||
it "returns false in any other phase" do
|
||||
Budget::PHASES.reject {|phase| phase == "balloting"}.each do |phase|
|
||||
Budget::Phase::PHASE_KINDS.reject {|phase| phase == "balloting"}.each do |phase|
|
||||
budget = create(:budget, phase: phase)
|
||||
investment = create(:budget_investment, :selected, budget: budget)
|
||||
|
||||
@@ -200,7 +200,7 @@ describe Budget::Investment do
|
||||
end
|
||||
|
||||
it "returns true for selected investments which budget's phase is publishing_prices or later" do
|
||||
Budget::PUBLISHED_PRICES_PHASES.each do |phase|
|
||||
Budget::Phase::PUBLISHED_PRICES_PHASES.each do |phase|
|
||||
budget.update(phase: phase)
|
||||
|
||||
expect(investment.should_show_price?).to eq(true)
|
||||
@@ -208,7 +208,7 @@ describe Budget::Investment do
|
||||
end
|
||||
|
||||
it "returns false in any other phase" do
|
||||
(Budget::PHASES - Budget::PUBLISHED_PRICES_PHASES).each do |phase|
|
||||
(Budget::Phase::PHASE_KINDS - Budget::Phase::PUBLISHED_PRICES_PHASES).each do |phase|
|
||||
budget.update(phase: phase)
|
||||
|
||||
expect(investment.should_show_price?).to eq(false)
|
||||
@@ -235,7 +235,7 @@ describe Budget::Investment do
|
||||
end
|
||||
|
||||
it "returns true for selected with price_explanation & budget in publishing_prices or later" do
|
||||
Budget::PUBLISHED_PRICES_PHASES.each do |phase|
|
||||
Budget::Phase::PUBLISHED_PRICES_PHASES.each do |phase|
|
||||
budget.update(phase: phase)
|
||||
|
||||
expect(investment.should_show_price_explanation?).to eq(true)
|
||||
@@ -243,7 +243,7 @@ describe Budget::Investment do
|
||||
end
|
||||
|
||||
it "returns false in any other phase" do
|
||||
(Budget::PHASES - Budget::PUBLISHED_PRICES_PHASES).each do |phase|
|
||||
(Budget::Phase::PHASE_KINDS - Budget::Phase::PUBLISHED_PRICES_PHASES).each do |phase|
|
||||
budget.update(phase: phase)
|
||||
|
||||
expect(investment.should_show_price_explanation?).to eq(false)
|
||||
@@ -785,7 +785,7 @@ describe Budget::Investment do
|
||||
end
|
||||
|
||||
it "returns false if budget is not balloting phase" do
|
||||
Budget::PHASES.reject {|phase| phase == "balloting"}.each do |phase|
|
||||
Budget::Phase::PHASE_KINDS.reject {|phase| phase == "balloting"}.each do |phase|
|
||||
budget.update(phase: phase)
|
||||
investment = create(:budget_investment, budget: budget)
|
||||
|
||||
|
||||
230
spec/models/budget/phase_spec.rb
Normal file
230
spec/models/budget/phase_spec.rb
Normal file
@@ -0,0 +1,230 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Budget::Phase do
|
||||
|
||||
let(:budget) { create(:budget) }
|
||||
let(:first_phase) { budget.phases.drafting }
|
||||
let(:second_phase) { budget.phases.accepting }
|
||||
let(:third_phase) { budget.phases.reviewing }
|
||||
let(:fourth_phase) { budget.phases.selecting }
|
||||
let(:final_phase) { budget.phases.finished}
|
||||
|
||||
before do
|
||||
first_phase.update_attributes(starts_at: Date.current - 3.days, ends_at: Date.current - 1.day)
|
||||
second_phase.update_attributes(starts_at: Date.current - 1.days, ends_at: Date.current + 1.day)
|
||||
third_phase.update_attributes(starts_at: Date.current + 1.days, ends_at: Date.current + 3.day)
|
||||
fourth_phase.update_attributes(starts_at: Date.current + 3.days, ends_at: Date.current + 5.day)
|
||||
end
|
||||
|
||||
describe "validates" do
|
||||
it "is not valid without a budget" do
|
||||
expect(build(:budget_phase, budget: nil)).not_to be_valid
|
||||
end
|
||||
|
||||
describe "kind validations" do
|
||||
it "is not valid without a kind" do
|
||||
expect(build(:budget_phase, kind: nil)).not_to be_valid
|
||||
end
|
||||
|
||||
it "is not valid with a kind not in valid budget phases" do
|
||||
expect(build(:budget_phase, kind: 'invalid_phase_kind')).not_to be_valid
|
||||
end
|
||||
|
||||
it "is not valid with the same kind as another budget's phase" do
|
||||
expect(build(:budget_phase, budget: budget)).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe "#dates_range_valid?" do
|
||||
it "is valid when start & end dates are different & consecutive" do
|
||||
first_phase.update_attributes(starts_at: Date.today, ends_at: Date.tomorrow)
|
||||
|
||||
expect(first_phase).to be_valid
|
||||
end
|
||||
|
||||
it "is not valid when dates are equal" do
|
||||
first_phase.update_attributes(starts_at: Date.today, ends_at: Date.today)
|
||||
|
||||
expect(first_phase).not_to be_valid
|
||||
end
|
||||
|
||||
it "is not valid when start date is later than end date" do
|
||||
first_phase.update_attributes(starts_at: Date.tomorrow, ends_at: Date.today)
|
||||
|
||||
expect(first_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))"
|
||||
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)
|
||||
|
||||
expect(second_phase).not_to be_valid
|
||||
expect(second_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)
|
||||
|
||||
expect(second_phase).not_to be_valid
|
||||
expect(second_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)
|
||||
|
||||
expect(second_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)
|
||||
|
||||
expect(second_phase).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe "#next_phase_dates_valid?" do
|
||||
let(:error) do
|
||||
"End date must be earlier than the end date of the next enabled phase (Reviewing projects)"
|
||||
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)
|
||||
|
||||
expect(second_phase).not_to be_valid
|
||||
expect(second_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)
|
||||
|
||||
expect(second_phase).not_to be_valid
|
||||
expect(second_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)
|
||||
|
||||
expect(second_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)
|
||||
|
||||
expect(second_phase).to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#adjust_date_ranges" do
|
||||
let(:prev_enabled_phase) { second_phase.prev_enabled_phase }
|
||||
let(:next_enabled_phase) { second_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)
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when being enabled" do
|
||||
before do
|
||||
second_phase.update_attributes(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_attributes(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{
|
||||
second_phase.update_attributes(enabled: true)
|
||||
}.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_attributes(enabled: false)
|
||||
end
|
||||
|
||||
it "doesn't change previous enabled phase end date" do
|
||||
expect {
|
||||
second_phase.update_attributes(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_attributes(starts_at: Date.current,
|
||||
ends_at: Date.current + 2.days)
|
||||
}.not_to (change{ next_enabled_phase.starts_at })
|
||||
end
|
||||
end
|
||||
|
||||
describe "when being disabled" do
|
||||
it "doesn't adjust previous enabled phase end date to its own start date" do
|
||||
expect {
|
||||
second_phase.update_attributes(enabled: false,
|
||||
starts_at: Date.current,
|
||||
ends_at: Date.current + 2.days)
|
||||
}.not_to (change{ prev_enabled_phase.ends_at })
|
||||
end
|
||||
|
||||
it "adjusts next enabled phase start date to its own start date" do
|
||||
expect {
|
||||
second_phase.update_attributes(enabled: false,
|
||||
starts_at: Date.current,
|
||||
ends_at: Date.current + 2.days)
|
||||
}.to change{ next_enabled_phase.starts_at.to_date }.to(Date.current)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "next & prev enabled phases" do
|
||||
before do
|
||||
second_phase.update_attributes(enabled: false)
|
||||
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)
|
||||
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)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#sanitize_description" do
|
||||
it "removes html entities from the description" do
|
||||
expect{
|
||||
first_phase.update_attributes(description: "<a>a</p> <javascript>javascript</javascript>")
|
||||
}.to change{ first_phase.description }.to('a javascript')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,6 +2,8 @@ require 'rails_helper'
|
||||
|
||||
describe Budget do
|
||||
|
||||
let(:budget) { create(:budget) }
|
||||
|
||||
it_behaves_like "sluggable"
|
||||
|
||||
describe "name" do
|
||||
@@ -15,22 +17,40 @@ describe Budget do
|
||||
end
|
||||
|
||||
describe "description" do
|
||||
it "changes depending on the phase" do
|
||||
budget = create(:budget)
|
||||
describe "Without Budget::Phase associated" do
|
||||
before do
|
||||
budget.phases.destroy_all
|
||||
end
|
||||
|
||||
Budget::PHASES.each do |phase|
|
||||
budget.phase = phase
|
||||
expect(budget.description).to eq(budget.send("description_#{phase}"))
|
||||
expect(budget.description).to be_html_safe
|
||||
it "changes depending on the phase, falling back to budget description attributes" do
|
||||
Budget::Phase::PHASE_KINDS.each do |phase_kind|
|
||||
budget.phase = phase_kind
|
||||
expect(budget.description).to eq(budget.send("description_#{phase_kind}"))
|
||||
expect(budget.description).to be_html_safe
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "With associated Budget::Phases" do
|
||||
before do
|
||||
budget.phases.each do |phase|
|
||||
phase.description = phase.kind.humanize
|
||||
phase.save
|
||||
end
|
||||
end
|
||||
|
||||
it "changes depending on the phase" do
|
||||
Budget::Phase::PHASE_KINDS.each do |phase_kind|
|
||||
budget.phase = phase_kind
|
||||
expect(budget.description).to eq(phase_kind.humanize)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "phase" do
|
||||
let(:budget) { create(:budget) }
|
||||
|
||||
it "is validated" do
|
||||
Budget::PHASES.each do |phase|
|
||||
Budget::Phase::PHASE_KINDS.each do |phase|
|
||||
budget.phase = phase
|
||||
expect(budget).to be_valid
|
||||
end
|
||||
@@ -103,13 +123,13 @@ describe Budget do
|
||||
it "returns nil if there is only one budget and it is still in drafting phase" do
|
||||
budget = create(:budget, phase: "drafting")
|
||||
|
||||
expect(Budget.current).to eq(nil)
|
||||
expect(described_class.current).to eq(nil)
|
||||
end
|
||||
|
||||
it "returns the budget if there is only one and not in drafting phase" do
|
||||
budget = create(:budget, phase: "accepting")
|
||||
|
||||
expect(Budget.current).to eq(budget)
|
||||
expect(described_class.current).to eq(budget)
|
||||
end
|
||||
|
||||
it "returns the last budget created that is not in drafting phase" do
|
||||
@@ -118,7 +138,7 @@ describe Budget do
|
||||
current_budget = create(:budget, phase: "accepting", created_at: 1.month.ago)
|
||||
next_budget = create(:budget, phase: "drafting", created_at: 1.week.ago)
|
||||
|
||||
expect(Budget.current).to eq(current_budget)
|
||||
expect(described_class.current).to eq(current_budget)
|
||||
end
|
||||
|
||||
end
|
||||
@@ -126,17 +146,15 @@ describe Budget do
|
||||
describe "#open" do
|
||||
|
||||
it "returns all budgets that are not in the finished phase" do
|
||||
phases = Budget::PHASES - ["finished"]
|
||||
phases.each do |phase|
|
||||
(Budget::Phase::PHASE_KINDS - ["finished"]).each do |phase|
|
||||
budget = create(:budget, phase: phase)
|
||||
expect(Budget.open).to include(budget)
|
||||
expect(described_class.open).to include(budget)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "heading_price" do
|
||||
let(:budget) { create(:budget) }
|
||||
let(:group) { create(:budget_group, budget: budget) }
|
||||
|
||||
it "returns the heading price if the heading provided is part of the budget" do
|
||||
@@ -150,8 +168,6 @@ describe Budget do
|
||||
end
|
||||
|
||||
describe "investments_orders" do
|
||||
let(:budget) { create(:budget) }
|
||||
|
||||
it "is random when accepting and reviewing" do
|
||||
budget.phase = 'accepting'
|
||||
expect(budget.investments_orders).to eq(['random'])
|
||||
@@ -173,5 +189,40 @@ describe Budget do
|
||||
expect(budget.investments_orders).to eq(['random', 'confidence_score'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#generate_phases" do
|
||||
let(:drafting_phase) { budget.phases.drafting }
|
||||
let(:accepting_phase) { budget.phases.accepting }
|
||||
let(:reviewing_phase) { budget.phases.reviewing }
|
||||
let(:selecting_phase) { budget.phases.selecting }
|
||||
let(:valuating_phase) { budget.phases.valuating }
|
||||
let(:publishing_prices_phase) { budget.phases.publishing_prices }
|
||||
let(:balloting_phase) { budget.phases.balloting }
|
||||
let(:reviewing_ballots_phase) { budget.phases.reviewing_ballots }
|
||||
let(:finished_phase) { budget.phases.finished }
|
||||
|
||||
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(accepting_phase)
|
||||
expect(accepting_phase.next_phase).to eq(reviewing_phase)
|
||||
expect(reviewing_phase.next_phase).to eq(selecting_phase)
|
||||
expect(selecting_phase.next_phase).to eq(valuating_phase)
|
||||
expect(valuating_phase.next_phase).to eq(publishing_prices_phase)
|
||||
expect(publishing_prices_phase.next_phase).to eq(balloting_phase)
|
||||
expect(balloting_phase.next_phase).to eq(reviewing_ballots_phase)
|
||||
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(accepting_phase.prev_phase).to eq(drafting_phase)
|
||||
expect(reviewing_phase.prev_phase).to eq(accepting_phase)
|
||||
expect(selecting_phase.prev_phase).to eq(reviewing_phase)
|
||||
expect(valuating_phase.prev_phase).to eq(selecting_phase)
|
||||
expect(publishing_prices_phase.prev_phase).to eq(valuating_phase)
|
||||
expect(balloting_phase.prev_phase).to eq(publishing_prices_phase)
|
||||
expect(reviewing_ballots_phase.prev_phase).to eq(balloting_phase)
|
||||
expect(finished_phase.prev_phase).to eq(reviewing_ballots_phase)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user