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| %>
- <%= 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 } %>
<% end %> diff --git a/app/views/budgets/index.html.erb b/app/views/budgets/index.html.erb index e034dd482..dd86ecb71 100644 --- a/app/views/budgets/index.html.erb +++ b/app/views/budgets/index.html.erb @@ -3,43 +3,158 @@ <%= render "shared/canonical", href: budgets_url %> <% end %> -<%= render "shared/section_header", i18n_namespace: "budgets.index.section_header", image: "budgets" %> -<%= render_map(nil, "budgets", false, nil, @budgets_coordinates) %> -
-
- - - - - - - - - <% @budgets.each do |budget| %> - <% if budget_published?(budget) %> - - - - +
+
+
+ +

<%= @budget.name %>

+ + <%= safe_html_with_links @budget.description %> + <%= link_to t("budgets.index.section_header.help"), "#section_help" %> +
+
+

+ <% published_phases = @budget.phases.published %> + <% current_phase_number = published_phases.index(@budget.current_phase) + 1 || 0 %> + <% phases_progress_numbers = "(#{current_phase_number}/#{published_phases.count})" %> + <%= t('budgets.show.phase') %> <%= phases_progress_numbers %> +

+

<%= t("budgets.phase.#{@budget.phase}") %>

+ + <%= link_to t("budgets.index.section_header.all_phases"), "#all_phases" %> + + <% if @budget.accepting? %> + <% if current_user %> + <% if current_user.level_two_or_three_verified? %> + <%= link_to t("budgets.investments.index.sidebar.create"), + new_budget_investment_path(@budget), + class: "button margin-top expanded" %> + <% else %> +
+ <%= t("budgets.investments.index.sidebar.verified_only", + verify: link_to(t("budgets.investments.index.sidebar.verify_account"), + verification_path)).html_safe %> +
+ <% end %> + <% else %> +
+ <%= t("budgets.investments.index.sidebar.not_logged_in", + sign_in: link_to(t("budgets.investments.index.sidebar.sign_in"), + new_user_session_path), + sign_up: link_to(t("budgets.investments.index.sidebar.sign_up"), + new_user_registration_path)).html_safe %> +
<% end %> <% end %> -
-
<%= 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 %> +
+
+
+ +
+
+
+ +
+ <% @budget.groups.each do |group| %> +

<%= group.name %>

+
    + <% group.headings.each do |heading| %> +
  • + <%= link_to budget_investments_path(@budget.id, heading_id: heading.id) do %> + <%= heading.name %> + <%= @budget.formatted_heading_price(heading) %> + <% end %> +
  • + <% end %> +
+ <% end %> +
+ +
+

<%= t("budgets.index.map") %>

+ <%= render_map(nil, "budgets", false, nil, @budgets_coordinates) %> +
+ +

+ <%= 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.all_phases") %>

+ +
+
+
+ +
+
+ + +
+ <% @budgets.each do |budget| %> + <% if budget_published?(budget) %> +
+
+
+
+

<%= budget.name %>

+ <%= budget.description %> +
+
+
+ <%= link_to t("budgets.index.see_results"), + budget_results_path(budget.id), + class: "button expanded" %> +
+
+
+
+
+ <% end %> + <% 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") %>

+
diff --git a/app/views/budgets/results/show.html.erb b/app/views/budgets/results/show.html.erb index c276a4c6f..54b31e7ba 100644 --- a/app/views/budgets/results/show.html.erb +++ b/app/views/budgets/results/show.html.erb @@ -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) %> diff --git a/app/views/polls/_poll_group.html.erb b/app/views/polls/_poll_group.html.erb index 033aff23e..3e14b3bf9 100644 --- a/app/views/polls/_poll_group.html.erb +++ b/app/views/polls/_poll_group.html.erb @@ -57,7 +57,7 @@
-
+
<%= link_to poll, class: "button hollow expanded" do %> <% if poll.expired? %> <%= t("polls.index.participate_button_expired") %> diff --git a/app/views/valuation/budget_investments/edit.html.erb b/app/views/valuation/budget_investments/edit.html.erb index 4ecd8748e..7c1d51239 100644 --- a/app/views/valuation/budget_investments/edit.html.erb +++ b/app/views/valuation/budget_investments/edit.html.erb @@ -79,7 +79,11 @@
<%= f.label :valuation_finished do %> - <%= f.check_box :valuation_finished, title: t('valuation.budget_investments.edit.valuation_finished'), label: false %> + <%= f.check_box :valuation_finished, + title: t('valuation.budget_investments.edit.valuation_finished'), + label: false, id: 'js-investment-report-alert', + "data-alert": t("valuation.budget_investments.edit.valuation_finished_alert"), + "data-not-feasible-alert": t("valuation.budget_investments.edit.not_feasible_alert") %> <%= t("valuation.budget_investments.edit.valuation_finished") %> <% end %>
diff --git a/config/locales/en/budgets.yml b/config/locales/en/budgets.yml index fce3a3ff9..ea5730bfa 100644 --- a/config/locales/en/budgets.yml +++ b/config/locales/en/budgets.yml @@ -44,6 +44,14 @@ en: icon_alt: Participatory budgets icon title: Participatory budgets help: Help about participatory budgets + all_phases: See all phases + all_phases: Budget investment's phases + map: Budget investments' proposals located geographically + investment_proyects: List of all investment projects + unfeasible_investment_proyects: List of all unfeasible investment projects + not_selected_investment_proyects: List of all investment projects not selected for balloting + finished_budgets: Finished participatory budgets + see_results: See results section_footer: title: Help about participatory budgets description: With the participatory budgets the citizens decide to which projects presented by the neighbors is destined a part of the municipal budget. @@ -158,3 +166,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})" \ No newline at end of file diff --git a/config/locales/en/valuation.yml b/config/locales/en/valuation.yml index 761ac2154..d1c267305 100644 --- a/config/locales/en/valuation.yml +++ b/config/locales/en/valuation.yml @@ -68,6 +68,8 @@ en: undefined_feasible: Pending feasible_explanation_html: Feasibility explanation valuation_finished: Valuation finished + valuation_finished_alert: "Are you sure you want to mark this report as completed? If you do it, it can no longer be modified." + not_feasible_alert: "An email will be sent immediately to the author of the project with the report of unfeasibility." duration_html: Time scope internal_comments_html: Internal comments save: Save changes @@ -121,4 +123,4 @@ en: internal_comments_html: Internal comments save: Save changes notice: - valuate: "Dossier updated" \ No newline at end of file + valuate: "Dossier updated" diff --git a/config/locales/es/budgets.yml b/config/locales/es/budgets.yml index c00b5b714..326ddabbe 100644 --- a/config/locales/es/budgets.yml +++ b/config/locales/es/budgets.yml @@ -44,6 +44,14 @@ es: icon_alt: Icono de Presupuestos participativos title: Presupuestos participativos help: Ayuda sobre presupuestos participativos + all_phases: Ver todas las fases + all_phases: Fases de los presupuestos participativos + map: Propuestas de los presupuestos participativos localizables geográficamente + investment_proyects: Ver lista completa de proyectos de inversión + unfeasible_investment_proyects: Ver lista de proyectos de inversión inviables + not_selected_investment_proyects: Ver lista de proyectos de inversión no seleccionados para la votación final + finished_budgets: Presupuestos participativos terminados + see_results: Ver resultados section_footer: title: Ayuda sobre presupuestos participativos description: Con los presupuestos participativos la ciudadanía decide a qué proyectos presentados por los vecinos y vecinas va destinada una parte del presupuesto municipal. @@ -158,3 +166,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}) " \ No newline at end of file diff --git a/config/locales/es/valuation.yml b/config/locales/es/valuation.yml index 5b4f6efd1..e84b92ad1 100644 --- a/config/locales/es/valuation.yml +++ b/config/locales/es/valuation.yml @@ -68,6 +68,8 @@ es: undefined_feasible: Sin decidir feasible_explanation_html: Informe de inviabilidad (en caso de que lo sea, dato público) valuation_finished: Informe finalizado + valuation_finished_alert: "¿Estás seguro/a de querer marcar este informe como completado? Una vez hecho, no se puede deshacer la acción." + not_feasible_alert: "Un email será enviado inmediatamente al autor del proyecto con el informe de inviabilidad." duration_html: Plazo de ejecución (opcional, dato no público) internal_comments_html: Comentarios y observaciones (para responsables internos, dato no público) save: Guardar Cambios diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index 8ed80d5cb..cc36569c8 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -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}", "

#{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

javascript") + }.to change{ first_phase.description }.to('a javascript') + end + end +end diff --git a/spec/models/budget_spec.rb b/spec/models/budget_spec.rb index f8ec2d5a5..cbc5fd5de 100644 --- a/spec/models/budget_spec.rb +++ b/spec/models/budget_spec.rb @@ -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