Merge pull request #2323 from consul/feature/budget_phases

Create Budget::Phases backend
This commit is contained in:
BertoCQ
2018-01-16 23:11:15 +01:00
committed by GitHub
20 changed files with 517 additions and 61 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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 %>

View File

@@ -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) %>

View File

@@ -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})"

View File

@@ -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}) "

View File

@@ -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]

View 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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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"

View File

@@ -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?

View File

@@ -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)

View 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

View File

@@ -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