diff --git a/CHANGELOG.md b/CHANGELOG.md index 24ffaa9b5..397c51f1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/app/controllers/admin/budgets_controller.rb b/app/controllers/admin/budgets_controller.rb index 7dbd66d7b..04b8ea0b2 100644 --- a/app/controllers/admin/budgets_controller.rb +++ b/app/controllers/admin/budgets_controller.rb @@ -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 diff --git a/app/helpers/budgets_helper.rb b/app/helpers/budgets_helper.rb index d2d6e1f5d..5e1949775 100644 --- a/app/helpers/budgets_helper.rb +++ b/app/helpers/budgets_helper.rb @@ -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 diff --git a/app/models/budget.rb b/app/models/budget.rb index 608599217..ceaffb19a 100644 --- a/app/models/budget.rb +++ b/app/models/budget.rb @@ -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 + diff --git a/app/models/budget/phase.rb b/app/models/budget/phase.rb new file mode 100644 index 000000000..903c698aa --- /dev/null +++ b/app/models/budget/phase.rb @@ -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 diff --git a/app/views/admin/budgets/_form.html.erb b/app/views/admin/budgets/_form.html.erb index 58a56d564..48a52079e 100644 --- a/app/views/admin/budgets/_form.html.erb +++ b/app/views/admin/budgets/_form.html.erb @@ -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| %>
#{Faker::Lorem.paragraphs(2).join('
')}
"] end] diff --git a/db/migrate/20180112123641_create_budget_phases.rb b/db/migrate/20180112123641_create_budget_phases.rb new file mode 100644 index 000000000..fbac158f0 --- /dev/null +++ b/db/migrate/20180112123641_create_budget_phases.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index d2c5d3450..5cdff80f1 100644 --- a/db/schema.rb +++ b/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" diff --git a/lib/tasks/budgets.rake b/lib/tasks/budgets.rake index f9810ac1c..738649538 100644 --- a/lib/tasks/budgets.rake +++ b/lib/tasks/budgets.rake @@ -13,4 +13,22 @@ namespace :budgets do end -end \ No newline at end of file + 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 diff --git a/spec/factories.rb b/spec/factories.rb index 7372b5ba9..77bfda68e 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -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 diff --git a/spec/features/budgets/investments_spec.rb b/spec/features/budgets/investments_spec.rb index d1fdcc474..7569d96bc 100644 --- a/spec/features/budgets/investments_spec.rb +++ b/spec/features/budgets/investments_spec.rb @@ -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) diff --git a/spec/features/budgets/results_spec.rb b/spec/features/budgets/results_spec.rb index 5e194a393..3eb349df0 100644 --- a/spec/features/budgets/results_spec.rb +++ b/spec/features/budgets/results_spec.rb @@ -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" diff --git a/spec/features/tags/budget_investments_spec.rb b/spec/features/tags/budget_investments_spec.rb index f244583a6..04fb05ddb 100644 --- a/spec/features/tags/budget_investments_spec.rb +++ b/spec/features/tags/budget_investments_spec.rb @@ -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? diff --git a/spec/models/budget/investment_spec.rb b/spec/models/budget/investment_spec.rb index 37cfd84ad..7fbb46039 100644 --- a/spec/models/budget/investment_spec.rb +++ b/spec/models/budget/investment_spec.rb @@ -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) diff --git a/spec/models/budget/phase_spec.rb b/spec/models/budget/phase_spec.rb new file mode 100644 index 000000000..5f9ea34c2 --- /dev/null +++ b/spec/models/budget/phase_spec.rb @@ -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