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/assets/javascripts/application.js b/app/assets/javascripts/application.js index 611ecc546..561825ccc 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -74,6 +74,7 @@ //= require polls //= require sortable //= require table_sortable +//= require investment_report_alert var initialize_modules = function() { App.Comments.initialize(); @@ -115,6 +116,7 @@ var initialize_modules = function() { App.Polls.initialize(); App.Sortable.initialize(); App.TableSortable.initialize(); + App.InvestmentReportAlert.initialize(); }; $(function(){ diff --git a/app/assets/javascripts/investment_report_alert.js.coffee b/app/assets/javascripts/investment_report_alert.js.coffee new file mode 100644 index 000000000..98b239a55 --- /dev/null +++ b/app/assets/javascripts/investment_report_alert.js.coffee @@ -0,0 +1,7 @@ +App.InvestmentReportAlert = + initialize: -> + $('#js-investment-report-alert').on 'click', -> + if this.checked && $('#budget_investment_feasibility_unfeasible').is(':checked') + confirm(this.dataset.alert + "\n" + this.dataset.notFeasibleAlert); + else if this.checked + confirm(this.dataset.alert); diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index 854d6b1e6..09902f649 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -368,7 +368,7 @@ a { vertical-align: top; } -.aling-middle { +.align-middle { vertical-align: middle; } diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index 606de72f3..a8a593dab 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -5,7 +5,7 @@ // 03. Show participation // 04. List participation // 05. Featured -// 06. Budget +// 06. Budgets // 07. Proposals successful // 08. Polls // 09. Polls results and stats @@ -1096,31 +1096,39 @@ } } -// 06. Budget -// ---------- +// 06. Budgets +// ----------- -.expanded.budget { - background: $budget; +.expanded { - h1, - h2, - p, - .back, - .icon-angle-left { - color: #fff; - } + &.budget { + background: $budget; - .button { - background: #fff; - color: $budget; - } + h1, + h2, + p, + a, + .back, + .icon-angle-left { + color: #fff; + } - .info { - background: #6a2a72; + a { + text-decoration: underline; + } - p { - margin-bottom: 0; - text-transform: uppercase; + .button { + background: #fff; + color: $budget; + } + + .info { + background: #6a2a72; + + p { + margin-bottom: 0; + text-transform: uppercase; + } } } } @@ -1182,7 +1190,7 @@ a { text-decoration: underline; - } + } .button { background: #fff; @@ -1212,6 +1220,36 @@ } } +.groups-and-headings { + + .heading { + border: 1px solid $border; + border-radius: rem-calc(3); + display: inline-block; + margin-bottom: $line-height / 2; + + &:hover { + background: $highlight; + text-decoration: none; + } + + a { + display: block; + padding: $line-height / 2; + + &:hover { + text-decoration: none; + } + } + + span { + color: $text; + display: block; + font-size: $small-font-size; + } + } +} + .progress-votes { position: relative; 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/controllers/budgets_controller.rb b/app/controllers/budgets_controller.rb index 586c90e94..f542a8c3f 100644 --- a/app/controllers/budgets_controller.rb +++ b/app/controllers/budgets_controller.rb @@ -15,7 +15,8 @@ class BudgetsController < ApplicationController def index @budgets = @budgets.order(:created_at) - budgets_map_locations = Budget.where.not(phase: 'drafting').map{ |budget| budget.investments.map{|investment| investment.map_location}}.flatten.compact + @budget = current_budget + budgets_map_locations = current_budget.investments.map{ |investment| investment.map_location }.compact @budgets_coordinates = budgets_map_locations.map{ |ml| {lat: ml.latitude, long: ml.longitude, investment_title: ml.investment.title , investment_id: ml.investment.id, budget_id: ml.investment.budget.id}} end diff --git a/app/controllers/management/base_controller.rb b/app/controllers/management/base_controller.rb index 66aee5a01..c7610eba9 100644 --- a/app/controllers/management/base_controller.rb +++ b/app/controllers/management/base_controller.rb @@ -5,6 +5,7 @@ class Management::BaseController < ActionController::Base before_action :set_locale helper_method :managed_user + helper_method :current_user private @@ -40,4 +41,7 @@ class Management::BaseController < ActionController::Base I18n.locale = session[:locale] end + def current_budget + Budget.current + end end diff --git a/app/controllers/management/budgets_controller.rb b/app/controllers/management/budgets_controller.rb index 470d1f9fd..b83d843e2 100644 --- a/app/controllers/management/budgets_controller.rb +++ b/app/controllers/management/budgets_controller.rb @@ -19,7 +19,7 @@ class Management::BudgetsController < Management::BaseController end def print_investments - @budget = Budget.current + @budget = current_budget end private diff --git a/app/controllers/valuation/budgets_controller.rb b/app/controllers/valuation/budgets_controller.rb index cf99f95e3..9789ab929 100644 --- a/app/controllers/valuation/budgets_controller.rb +++ b/app/controllers/valuation/budgets_controller.rb @@ -5,7 +5,7 @@ class Valuation::BudgetsController < Valuation::BaseController load_and_authorize_resource def index - @budget = Budget.current + @budget = current_budget if @budget.present? @investments_with_valuation_open = {} @investments_with_valuation_open = @budget.investments 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..2e4af0000 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 + where.not(phase: "drafting").order(:created_at).last + end + + def current_phase + phases.send(phase) end def description - send("description_#{phase}").try(:html_safe) + description_for_phase(phase) end - def self.description_max_length - 2000 + 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..5475d97be --- /dev/null +++ b/app/models/budget/phase.rb @@ -0,0 +1,86 @@ +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 :published, -> { enabled.where.not(kind: 'drafting') } + 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| %>
| <%= Budget.human_attribute_name(:name) %> | -<%= Budget.human_attribute_name(:phase) %> | -
|---|---|
| - <%= link_to budget.name, budget %> - | -- <%= budget.translated_phase %> - | -
- <%= t("budgets.index.section_footer.title") %> -
-<%= t("budgets.index.section_footer.description") %>
-<%= t("budgets.index.section_footer.help_text_1") %>
-<%= t("budgets.index.section_footer.help_text_2") %>
-<%= t("budgets.index.section_footer.help_text_3", - org: link_to(setting['org_name'], new_user_registration_path)).html_safe %>
-<%= t("budgets.index.section_footer.help_text_4") %>
+ + <% if @budget.finished? || (@budget.balloting? && can?(:read_results, @budget)) %> + <%= link_to t("budgets.show.see_results"), + budget_results_path(@budget, heading_id: @budget.headings.first), + class: "button margin-top expanded" %> + <% end %> +
+ <%= link_to budget_investments_path(@budget.id) do %>
+ <%= t("budgets.index.investment_proyects") %>
+ <% end %>
+ <%= link_to budget_investments_path(budget_id: @budget.id, filter: 'unfeasible') do %>
+ <%= t("budgets.index.unfeasible_investment_proyects") %>
+ <% end %>
+ <%= link_to budget_investments_path(budget_id: @budget.id, filter: 'unselected') do %>
+ <%= t("budgets.index.not_selected_investment_proyects") %>
+ <% end %>
+
+ <%= t("budgets.index.section_footer.title") %> +
+<%= t("budgets.index.section_footer.description") %>
+<%= t("budgets.index.section_footer.help_text_1") %>
+<%= t("budgets.index.section_footer.help_text_2") %>
+<%= t("budgets.index.section_footer.help_text_3", + org: link_to(setting['org_name'], new_user_registration_path)).html_safe %>
+<%= t("budgets.index.section_footer.help_text_4") %>
+#{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..34805c4d1 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: budget.phases&.last, + starts_at: budget.phases&.last&.ends_at || Date.current, + ends_at: (budget.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..a357a0bb1 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 @@ -378,7 +387,7 @@ FactoryBot.define do association :investment, factory: :budget_investment sequence(:title) { |n| "Budget investment milestone #{n} title" } description 'Milestone description' - publication_date Time.zone.today + publication_date Date.current end factory :vote do diff --git a/spec/features/admin/budget_investment_milestones_spec.rb b/spec/features/admin/budget_investment_milestones_spec.rb index 4221d5647..5967d71f5 100644 --- a/spec/features/admin/budget_investment_milestones_spec.rb +++ b/spec/features/admin/budget_investment_milestones_spec.rb @@ -40,12 +40,12 @@ feature 'Admin budget investment milestones' do click_link 'Create new milestone' fill_in 'budget_investment_milestone_description', with: 'New description milestone' - fill_in 'budget_investment_milestone_publication_date', with: Time.zone.today + fill_in 'budget_investment_milestone_publication_date', with: Date.current click_button 'Create milestone' expect(page).to have_content 'New description milestone' - expect(page).to have_content Time.zone.today + expect(page).to have_content Date.current end scenario "Show validation errors on milestone form" do @@ -78,13 +78,13 @@ feature 'Admin budget investment milestones' do expect(page).to have_css("img[alt='#{milestone.image.title}']") fill_in 'budget_investment_milestone_description', with: 'Changed description' - fill_in 'budget_investment_milestone_publication_date', with: Time.zone.today.to_date + fill_in 'budget_investment_milestone_publication_date', with: Date.current fill_in 'budget_investment_milestone_documents_attributes_0_title', with: 'New document title' click_button 'Update milestone' expect(page).to have_content 'Changed description' - expect(page).to have_content Time.zone.today.to_date + expect(page).to have_content Date.current expect(page).to have_link 'Show image' expect(page).to have_link 'New document title' end diff --git a/spec/features/admin/budget_investments_spec.rb b/spec/features/admin/budget_investments_spec.rb index e20ec2b32..627824045 100644 --- a/spec/features/admin/budget_investments_spec.rb +++ b/spec/features/admin/budget_investments_spec.rb @@ -508,6 +508,50 @@ feature 'Admin budget investments' do expect(page).not_to have_content "Refugees, Solidarity" end + scenario "Shows alert when 'Valuation finished' is checked", :js do + budget_investment = create(:budget_investment) + + visit admin_budget_budget_investment_path(budget_investment.budget, budget_investment) + click_link 'Edit dossier' + + expect(page).to have_content 'Valuation finished' + + find_field('budget_investment[valuation_finished]').click + + page.accept_confirm("Are you sure you want to mark this report as completed? If you do it, it can no longer be modified.") + + expect(page).to have_field('budget_investment[valuation_finished]', checked: true) + end + + scenario "Shows alert with unfeasible status when 'Valuation finished' is checked", :js do + budget_investment = create(:budget_investment) + + visit admin_budget_budget_investment_path(budget_investment.budget, budget_investment) + click_link 'Edit dossier' + + expect(page).to have_content 'Valuation finished' + + find_field('budget_investment_feasibility_unfeasible').click + find_field('budget_investment[valuation_finished]').click + + page.accept_confirm("Are you sure you want to mark this report as completed? If you do it, it can no longer be modified.\nAn email will be sent immediately to the author of the project with the report of unfeasibility.") + + expect(page).to have_field('budget_investment[valuation_finished]', checked: true) + end + + scenario "Undoes check in 'Valuation finished' if user clicks 'cancel' on alert", :js do + budget_investment = create(:budget_investment) + + visit admin_budget_budget_investment_path(budget_investment.budget, budget_investment) + click_link 'Edit dossier' + + dismiss_confirm do + find_field('budget_investment[valuation_finished]').click + end + + expect(page).to have_field('budget_investment[valuation_finished]', checked: false) + end + scenario "Errors on update" do budget_investment = create(:budget_investment) diff --git a/spec/features/admin/budgets_spec.rb b/spec/features/admin/budgets_spec.rb index 239865317..7b41715fe 100644 --- a/spec/features/admin/budgets_spec.rb +++ b/spec/features/admin/budgets_spec.rb @@ -279,7 +279,7 @@ feature 'Admin budgets' do click_link 'Delete' end - expect(page).to_not have_content 'District 1' + expect(page).not_to have_content 'District 1' end end diff --git a/spec/features/admin/poll/shifts_spec.rb b/spec/features/admin/poll/shifts_spec.rb index bddd1b6b1..8ee8bfe5c 100644 --- a/spec/features/admin/poll/shifts_spec.rb +++ b/spec/features/admin/poll/shifts_spec.rb @@ -14,13 +14,13 @@ feature 'Admin shifts' do booth1 = create(:poll_booth) booth2 = create(:poll_booth) - shift1 = create(:poll_shift, officer: officer, booth: booth1, date: Time.zone.today) + shift1 = create(:poll_shift, officer: officer, booth: booth1, date: Date.current) shift2 = create(:poll_shift, officer: officer, booth: booth2, date: Time.zone.tomorrow) visit new_admin_booth_shift_path(booth1) expect(page).to have_css(".shift", count: 1) - expect(page).to have_content I18n.l(Time.zone.today, format: :long) + expect(page).to have_content I18n.l(Date.current, format: :long) expect(page).to have_content officer.name visit new_admin_booth_shift_path(booth2) @@ -98,11 +98,11 @@ feature 'Admin shifts' do assignment = create(:poll_booth_assignment, poll: poll, booth: booth) officer = create(:poll_officer) - shift1 = create(:poll_shift, :vote_collection_task, officer: officer, booth: booth, date: Time.zone.today) + shift1 = create(:poll_shift, :vote_collection_task, officer: officer, booth: booth, date: Date.current) shift2 = create(:poll_shift, :recount_scrutiny_task, officer: officer, booth: booth, date: Time.zone.tomorrow) vote_collection_dates = (Date.current..poll.ends_at.to_date).to_a - .reject { |date| date == Time.zone.today } + .reject { |date| date == Date.current } .map { |date| I18n.l(date, format: :long) } recount_scrutiny_dates = (poll.ends_at.to_date..poll.ends_at.to_date + 1.week).to_a .reject { |date| date == Time.zone.tomorrow } diff --git a/spec/features/budgets/budgets_spec.rb b/spec/features/budgets/budgets_spec.rb index ee537a854..bb7ab2e91 100644 --- a/spec/features/budgets/budgets_spec.rb +++ b/spec/features/budgets/budgets_spec.rb @@ -7,8 +7,42 @@ feature 'Budgets' do scenario 'Index' do budgets = create_list(:budget, 3) + last_budget = budgets.last + group1 = create(:budget_group, budget: last_budget) + group2 = create(:budget_group, budget: last_budget) + + heading1 = create(:budget_heading, group: group1) + heading2 = create(:budget_heading, group: group2) + visit budgets_path - budgets.each {|budget| expect(page).to have_link(budget.name)} + + within("#budget_heading") do + expect(page).to have_content(last_budget.name) + expect(page).to have_content(last_budget.description) + expect(page).to have_content("Actual phase (1/8)") + expect(page).to have_content("Accepting projects") + expect(page).to have_link 'Help about participatory budgets' + expect(page).to have_link 'See all phases' + end + + last_budget.update_attributes(phase: 'publishing_prices') + visit budgets_path + + within("#budget_heading") do + expect(page).to have_content("Actual phase (5/8)") + end + + within('#budget_info') do + expect(page).to have_content group1.name + expect(page).to have_content group2.name + expect(page).to have_content heading1.name + expect(page).to have_content last_budget.formatted_heading_price(heading1) + expect(page).to have_content heading2.name + expect(page).to have_content last_budget.formatted_heading_price(heading2) + + expect(page).to have_content budgets.first.name + expect(page).to have_content budgets[2].name + end end context 'Show' do @@ -70,6 +104,7 @@ feature 'Budgets' do background do logout budget.update(phase: 'drafting') + create(:budget) end context "Listed" do diff --git a/spec/features/budgets/investments_spec.rb b/spec/features/budgets/investments_spec.rb index d1fdcc474..00f10e111 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) @@ -570,7 +570,7 @@ feature 'Budget Investments' do within("#tab-milestones") do expect(page).to have_content(milestone.description) - expect(page).to have_content(Time.zone.today.to_date) + expect(page).to have_content(Date.current) expect(page.find("#image_#{milestone.id}")['alt']).to have_content image.title expect(page).to have_link document.title end @@ -778,8 +778,7 @@ feature 'Budget Investments' do visit root_path first(:link, "Participatory budgeting").click - click_link budget.name - click_link "Health" + click_link "More hospitals" within("#budget_investment_#{sp1.id}") do expect(page).to have_content sp1.title 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/emails_spec.rb b/spec/features/emails_spec.rb index f4bd7e08e..16be1cbf4 100644 --- a/spec/features/emails_spec.rb +++ b/spec/features/emails_spec.rb @@ -396,7 +396,7 @@ feature 'Emails' do choose 'budget_investment_feasibility_unfeasible' fill_in 'budget_investment_unfeasibility_explanation', with: 'This is not legal as stated in Article 34.9' - check 'budget_investment_valuation_finished' + find_field('budget_investment[valuation_finished]').click click_button 'Save changes' expect(page).to have_content "Dossier updated" 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/features/valuation/budget_investments_spec.rb b/spec/features/valuation/budget_investments_spec.rb index 7bd955e0e..33ab47b8e 100644 --- a/spec/features/valuation/budget_investments_spec.rb +++ b/spec/features/valuation/budget_investments_spec.rb @@ -345,7 +345,7 @@ feature 'Valuation budget investments' do visit valuation_budget_budget_investment_path(@budget, @investment) click_link 'Edit dossier' - check 'budget_investment_valuation_finished' + find_field('budget_investment[valuation_finished]').click click_button 'Save changes' visit valuation_budget_budget_investments_path(@budget) diff --git a/spec/features/valuation/budgets_spec.rb b/spec/features/valuation/budgets_spec.rb index 4fd821c7e..9057f81d3 100644 --- a/spec/features/valuation/budgets_spec.rb +++ b/spec/features/valuation/budgets_spec.rb @@ -30,8 +30,8 @@ feature 'Valuation budgets' do visit valuation_budgets_path - expect(page).to_not have_content(budget1.name) - expect(page).to_not have_content(budget2.name) + expect(page).not_to have_content(budget1.name) + expect(page).not_to have_content(budget2.name) expect(page).to have_content(budget3.name) end 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