decidim-budgets 0.23.6 → 0.24.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/decidim/budgets/projects.js.es6 +30 -13
  3. data/app/cells/decidim/budgets/limit_announcement_cell.rb +1 -1
  4. data/app/cells/decidim/budgets/project_list_item/project_data_vote_button.erb +1 -0
  5. data/app/cells/decidim/budgets/project_list_item_cell.rb +4 -0
  6. data/app/commands/decidim/budgets/admin/import_proposals_to_budgets.rb +8 -1
  7. data/app/controllers/decidim/budgets/admin/budgets_controller.rb +22 -1
  8. data/app/controllers/decidim/budgets/orders_controller.rb +1 -1
  9. data/app/forms/decidim/budgets/admin/component_form.rb +57 -10
  10. data/app/forms/decidim/budgets/admin/project_form.rb +3 -2
  11. data/app/forms/decidim/budgets/admin/project_import_proposals_form.rb +11 -0
  12. data/app/helpers/decidim/budgets/projects_helper.rb +45 -1
  13. data/app/models/decidim/budgets/order.rb +94 -18
  14. data/app/permissions/decidim/budgets/admin/permissions.rb +1 -1
  15. data/app/views/decidim/budgets/admin/budgets/index.html.erb +22 -5
  16. data/app/views/decidim/budgets/admin/projects/index.html.erb +13 -6
  17. data/app/views/decidim/budgets/admin/proposals_imports/new.html.erb +6 -1
  18. data/app/views/decidim/budgets/projects/_budget_excess.html.erb +2 -2
  19. data/app/views/decidim/budgets/projects/_budget_summary.html.erb +32 -12
  20. data/app/views/decidim/budgets/projects/_order_progress.html.erb +1 -1
  21. data/app/views/decidim/budgets/projects/_order_total_budget.html.erb +5 -1
  22. data/config/locales/ar.yml +2 -5
  23. data/config/locales/ca.yml +7 -9
  24. data/config/locales/cs.yml +34 -6
  25. data/config/locales/de.yml +34 -6
  26. data/config/locales/el.yml +2 -6
  27. data/config/locales/en.yml +35 -7
  28. data/config/locales/es-MX.yml +26 -8
  29. data/config/locales/es-PY.yml +26 -8
  30. data/config/locales/es.yml +26 -8
  31. data/config/locales/eu.yml +2 -5
  32. data/config/locales/fi-plain.yml +34 -6
  33. data/config/locales/fi.yml +36 -8
  34. data/config/locales/fr-CA.yml +35 -7
  35. data/config/locales/fr.yml +35 -7
  36. data/config/locales/gl.yml +7 -22
  37. data/config/locales/hu.yml +2 -5
  38. data/config/locales/id-ID.yml +2 -5
  39. data/config/locales/is-IS.yml +2 -4
  40. data/config/locales/it.yml +2 -6
  41. data/config/locales/ja.yml +3 -7
  42. data/config/locales/lv.yml +2 -6
  43. data/config/locales/nl.yml +35 -7
  44. data/config/locales/no.yml +2 -7
  45. data/config/locales/pl.yml +38 -10
  46. data/config/locales/pt-BR.yml +2 -5
  47. data/config/locales/pt.yml +2 -6
  48. data/config/locales/ro-RO.yml +2 -67
  49. data/config/locales/ru.yml +2 -5
  50. data/config/locales/sk.yml +2 -6
  51. data/config/locales/sr-CS.yml +0 -2
  52. data/config/locales/sv.yml +26 -7
  53. data/config/locales/tr-TR.yml +31 -7
  54. data/config/locales/uk.yml +2 -5
  55. data/config/locales/zh-CN.yml +2 -7
  56. data/db/migrate/20200804175222_votes_enabled_to_votes_choices.rb +4 -4
  57. data/lib/decidim/api/budget_type.rb +21 -0
  58. data/lib/decidim/api/budgets_type.rb +26 -0
  59. data/lib/decidim/api/project_type.rb +23 -0
  60. data/lib/decidim/budgets/api.rb +9 -0
  61. data/lib/decidim/budgets/component.rb +31 -14
  62. data/lib/decidim/budgets/project_serializer.rb +81 -0
  63. data/lib/decidim/budgets/test/factories.rb +35 -1
  64. data/lib/decidim/budgets/version.rb +1 -1
  65. data/lib/decidim/budgets/workflows/all.rb +1 -1
  66. data/lib/decidim/budgets/workflows/base.rb +3 -3
  67. data/lib/decidim/budgets/workflows/one.rb +1 -1
  68. data/lib/decidim/budgets.rb +2 -0
  69. metadata +21 -19
  70. data/app/types/decidim/budgets/budget_type.rb +0 -24
  71. data/app/types/decidim/budgets/budgets_type.rb +0 -32
  72. data/app/types/decidim/budgets/project_type.rb +0 -26
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad2c33a58a6364cf18c6abdf638b116b76e35545dfbf6f4f44ddf7d4d5b0c104
4
- data.tar.gz: 3e11021d08a6ee0647044b48bd41c6a7246dae68122573333bceec1d60d01983
3
+ metadata.gz: 41710b06043e856a900ffaa5f04509ba3ac513e9e9bd0e3c95dfd6fa5cd6c9e3
4
+ data.tar.gz: d232449475a2e39087ce38fd9b265e7a689ef1e3b4c50860a304857df109b720
5
5
  SHA512:
6
- metadata.gz: f331c9330a446e8e38577c125775941d5fdda7817433a2910a4a27eaad623e514453e7b3b9d972c38c478ffa6ba93eee0b3b467459c72596ca1981465c12852b
7
- data.tar.gz: 1b4fc9b6e53ba26e252a0d02ba43de4689c1d520eb1a4da5ad9a5a065fa252bff0d1b8821d3bb36947c526bc0732fcc536eb631e43f0293cc790543db2b576f4
6
+ metadata.gz: 38699120554e836883325bb833b07bea0471d8793271342f88a5c09bd8335ced004bb6c8f1428cfafc8b14ff078d3659e1639f5fd1cb6193488c9f27c475eed9
7
+ data.tar.gz: 96404bf36a1032fcc72cdebfddc5f90520a495bf8023ad595c44c456a15f9f4f89e556b31ed891952c251afcc9fccca6f1623e83ad8e3c5821dfd811a0b1278c
@@ -6,22 +6,31 @@ $(() => {
6
6
  const $budgetSummaryTotal = $(".budget-summary__total");
7
7
  const $budgetExceedModal = $("#budget-excess");
8
8
  const $budgetSummary = $(".budget-summary__progressbox");
9
- const totalBudget = parseInt($budgetSummaryTotal.attr("data-total-budget"), 10);
9
+ const totalAllocation = parseInt($budgetSummaryTotal.attr("data-total-allocation"), 10);
10
10
 
11
11
  const cancelEvent = (event) => {
12
12
  event.stopPropagation();
13
13
  event.preventDefault();
14
14
  };
15
15
 
16
+ const allowExitFrom = ($el) => {
17
+ if ($el.parents("#loginModal").length > 0) {
18
+ return true;
19
+ } else if ($el.parents("#authorizationModal").length > 0) {
20
+ return true;
21
+ }
22
+
23
+ return false;
24
+ }
25
+
16
26
  $projects.on("click", ".budget-list__action", (event) => {
17
- const currentBudget = parseInt($budgetSummary.attr("data-current-budget"), 10);
27
+ const currentAllocation = parseInt($budgetSummary.attr("data-current-allocation"), 10);
18
28
  const $currentTarget = $(event.currentTarget);
19
- const projectBudget = parseInt($currentTarget.attr("data-budget"), 10);
29
+ const projectAllocation = parseInt($currentTarget.attr("data-allocation"), 10);
20
30
 
21
31
  if ($currentTarget.attr("disabled")) {
22
32
  cancelEvent(event);
23
-
24
- } else if (($currentTarget.attr("data-add") === "true") && ((currentBudget + projectBudget) > totalBudget)) {
33
+ } else if (($currentTarget.attr("data-add") === "true") && ((currentAllocation + projectAllocation) > totalAllocation)) {
25
34
  $budgetExceedModal.foundation("toggle");
26
35
  cancelEvent(event);
27
36
  }
@@ -30,22 +39,30 @@ $(() => {
30
39
  if ($("#order-progress [data-toggle=budget-confirm]").length > 0) {
31
40
  const safeUrl = $(".budget-summary").attr("data-safe-url").split("?")[0];
32
41
  $(document).on("click", "a", (event) => {
33
- window.exitUrl = event.currentTarget.href;
42
+ if (allowExitFrom($(event.currentTarget))) {
43
+ window.exitUrl = null;
44
+ } else {
45
+ window.exitUrl = event.currentTarget.href;
46
+ }
34
47
  });
35
48
  $(document).on("submit", "form", (event) => {
36
- window.exitUrl = event.currentTarget.action;
49
+ if (allowExitFrom($(event.currentTarget))) {
50
+ window.exitUrl = null;
51
+ } else {
52
+ window.exitUrl = event.currentTarget.action;
53
+ }
37
54
  });
38
55
 
39
- window.onbeforeunload = () => {
40
- const currentBudget = parseInt($budgetSummary.attr("data-current-budget"), 10);
56
+ window.addEventListener("beforeunload", (event) => {
57
+ const currentAllocation = parseInt($budgetSummary.attr("data-current-allocation"), 10);
41
58
  const exitUrl = window.exitUrl;
42
59
  window.exitUrl = null;
43
60
 
44
- if (currentBudget === 0 || (exitUrl && exitUrl.startsWith(safeUrl))) {
45
- return null;
61
+ if (currentAllocation === 0 || (exitUrl && exitUrl.startsWith(safeUrl))) {
62
+ return;
46
63
  }
47
64
 
48
- return "";
49
- }
65
+ event.returnValue = true;
66
+ });
50
67
  }
51
68
  });
@@ -20,7 +20,7 @@ module Decidim
20
20
  return if vote_allowed?(budget)
21
21
  return if voted?(budget)
22
22
 
23
- discardable.any? || !vote_allowed?(budget, false)
23
+ discardable.any? || !vote_allowed?(budget, consider_progress: false)
24
24
  end
25
25
 
26
26
  def announcement_args
@@ -7,6 +7,7 @@
7
7
  add: !resource_added?,
8
8
  disable: true,
9
9
  budget: model.budget_amount,
10
+ allocation: resource_allocation,
10
11
  "redirect-url": resource_path
11
12
  },
12
13
  disabled: vote_button_disabled?,
@@ -28,6 +28,10 @@ module Decidim
28
28
  current_order && current_order.projects.include?(model)
29
29
  end
30
30
 
31
+ def resource_allocation
32
+ current_order.allocation_for(model)
33
+ end
34
+
31
35
  def data_class
32
36
  [].tap do |list|
33
37
  list << "budget-list__data--added" if can_have_order? && resource_added?
@@ -60,7 +60,14 @@ module Decidim
60
60
  end
61
61
 
62
62
  def proposals
63
- Decidim::Proposals::Proposal.where(component: origin_component).where(state: "accepted")
63
+ return all_proposals if form.scope_id.blank?
64
+
65
+ all_proposals.where(decidim_scope_id: form.scope_id)
66
+ end
67
+
68
+ def all_proposals
69
+ Decidim::Proposals::Proposal.where(component: origin_component)
70
+ .where(state: :accepted)
64
71
  end
65
72
 
66
73
  def origin_component
@@ -5,7 +5,8 @@ module Decidim
5
5
  module Admin
6
6
  # This controller allows the create or update a budget.
7
7
  class BudgetsController < Admin::ApplicationController
8
- helper_method :budgets, :budget
8
+ helper_method :budgets, :budget, :finished_orders, :pending_orders,
9
+ :users_with_pending_orders, :users_with_finished_orders
9
10
 
10
11
  def new
11
12
  enforce_permission_to :create, :budget
@@ -76,6 +77,26 @@ module Decidim
76
77
  def budget
77
78
  @budget ||= budgets.find_by(id: params[:id])
78
79
  end
80
+
81
+ def orders
82
+ @orders ||= Order.where(decidim_budgets_budget_id: budgets)
83
+ end
84
+
85
+ def pending_orders
86
+ orders.pending
87
+ end
88
+
89
+ def finished_orders
90
+ orders.finished
91
+ end
92
+
93
+ def users_with_pending_orders
94
+ orders.pending.pluck(:decidim_user_id).uniq
95
+ end
96
+
97
+ def users_with_finished_orders
98
+ orders.finished.pluck(:decidim_user_id).uniq
99
+ end
79
100
  end
80
101
  end
81
102
  end
@@ -43,7 +43,7 @@ module Decidim
43
43
  end
44
44
 
45
45
  def redirect_path
46
- if params.dig(:return_to) == "budget"
46
+ if params[:return_to] == "budget"
47
47
  budget_path(budget)
48
48
  else
49
49
  budgets_path
@@ -7,7 +7,10 @@ module Decidim
7
7
  # to a participatory process from the admin panel.
8
8
  #
9
9
  class ComponentForm < Decidim::Admin::ComponentForm
10
- validate :budget_voting_rule_enabled_setting, :budget_voting_rule_value_setting
10
+ validate :budget_voting_rule_enabled_setting,
11
+ :budget_voting_rule_threshold_value_setting,
12
+ :budget_voting_rule_minimum_value_setting,
13
+ :budget_voting_rule_projects_value_setting
11
14
 
12
15
  private
13
16
 
@@ -16,27 +19,71 @@ module Decidim
16
19
  def budget_voting_rule_enabled_setting
17
20
  return unless manifest&.name == :budgets
18
21
 
22
+ rule_settings = [
23
+ :vote_rule_threshold_percent_enabled,
24
+ :vote_rule_minimum_budget_projects_enabled,
25
+ :vote_rule_selected_projects_enabled
26
+ ]
27
+ active_rules = rule_settings.select { |key| settings.public_send(key) == true }
19
28
  i18n_error_scope = "decidim.components.budgets.settings.global.form.errors"
20
- if settings.vote_rule_threshold_percent_enabled.blank? && settings.vote_rule_minimum_budget_projects_enabled.blank?
21
- settings.errors.add(:vote_rule_threshold_percent_enabled, I18n.t(:budget_voting_rule_required, scope: i18n_error_scope))
22
- settings.errors.add(:vote_rule_minimum_budget_projects_enabled, I18n.t(:budget_voting_rule_required, scope: i18n_error_scope))
29
+ if active_rules.blank?
30
+ rule_settings.each do |key|
31
+ settings.errors.add(key, I18n.t(:budget_voting_rule_required, scope: i18n_error_scope))
32
+ end
23
33
  end
24
34
 
25
- if settings.vote_rule_threshold_percent_enabled && settings.vote_rule_minimum_budget_projects_enabled
26
- settings.errors.add(:vote_rule_threshold_percent_enabled, I18n.t(:budget_voting_rule_only_one, scope: i18n_error_scope))
27
- settings.errors.add(:vote_rule_minimum_budget_projects_enabled, I18n.t(:budget_voting_rule_only_one, scope: i18n_error_scope))
35
+ if active_rules.length > 1
36
+ rule_settings.each do |key|
37
+ settings.errors.add(key, I18n.t(:budget_voting_rule_only_one, scope: i18n_error_scope))
38
+ end
28
39
  end
29
40
  end
30
41
 
31
42
  # - the value must be a valid number
32
- def budget_voting_rule_value_setting
43
+ def budget_voting_rule_threshold_value_setting
33
44
  return unless manifest&.name == :budgets
45
+ return unless settings.vote_rule_threshold_percent_enabled
34
46
 
35
47
  invalid_percent_number = settings.vote_threshold_percent.blank? || settings.vote_threshold_percent.to_i.negative?
36
- settings.errors.add(:vote_threshold_percent) if settings.vote_rule_threshold_percent_enabled && invalid_percent_number
48
+ settings.errors.add(:vote_threshold_percent) if invalid_percent_number
49
+ end
50
+
51
+ def budget_voting_rule_minimum_value_setting
52
+ return unless manifest&.name == :budgets
53
+ return unless settings.vote_rule_minimum_budget_projects_enabled
37
54
 
38
55
  invalid_minimum_number = settings.vote_minimum_budget_projects_number.blank? || (settings.vote_minimum_budget_projects_number.to_i < 1)
39
- settings.errors.add(:vote_minimum_budget_projects_number) if settings.vote_rule_minimum_budget_projects_enabled && invalid_minimum_number
56
+ settings.errors.add(:vote_minimum_budget_projects_number) if invalid_minimum_number
57
+ end
58
+
59
+ def budget_voting_rule_projects_value_setting
60
+ return unless manifest&.name == :budgets
61
+ return unless settings.vote_rule_selected_projects_enabled
62
+
63
+ budget_voting_projects_value_setting_min
64
+ budget_voting_projects_value_setting_max
65
+ budget_voting_projects_value_setting_both
66
+ end
67
+
68
+ def budget_voting_projects_value_setting_min
69
+ return if settings.vote_selected_projects_minimum.present? && settings.vote_selected_projects_minimum.to_i >= 0
70
+
71
+ settings.errors.add(:vote_selected_projects_minimum)
72
+ end
73
+
74
+ def budget_voting_projects_value_setting_max
75
+ return if settings.vote_selected_projects_maximum.present? && settings.vote_selected_projects_maximum.to_i.positive?
76
+
77
+ settings.errors.add(:vote_selected_projects_maximum)
78
+ end
79
+
80
+ def budget_voting_projects_value_setting_both
81
+ return if settings.errors[:vote_selected_projects_minimum].present?
82
+ return if settings.errors[:vote_selected_projects_maximum].present?
83
+ return if settings.vote_selected_projects_maximum >= settings.vote_selected_projects_minimum
84
+
85
+ settings.errors.add(:vote_selected_projects_minimum)
86
+ settings.errors.add(:vote_selected_projects_maximum)
40
87
  end
41
88
  end
42
89
  end
@@ -6,6 +6,7 @@ module Decidim
6
6
  # This class holds a Form to create/update projects from Decidim's admin panel.
7
7
  class ProjectForm < Decidim::Form
8
8
  include TranslatableAttributes
9
+ include AttachmentAttributes
9
10
  include TranslationsHelper
10
11
  include Decidim::ApplicationHelper
11
12
 
@@ -17,10 +18,10 @@ module Decidim
17
18
  attribute :decidim_category_id, Integer
18
19
  attribute :proposal_ids, Array[Integer]
19
20
  attribute :attachment, AttachmentForm
20
- attribute :photos, Array[String]
21
- attribute :add_photos, Array
22
21
  attribute :selected, Boolean
23
22
 
23
+ attachments_attribute :photos
24
+
24
25
  validates :title, translatable_presence: true
25
26
  validates :description, translatable_presence: true
26
27
  validates :budget_amount, presence: true, numericality: { greater_than: 0 }
@@ -9,12 +9,15 @@ module Decidim
9
9
  mimic :proposals_import
10
10
 
11
11
  attribute :origin_component_id, Integer
12
+ attribute :scope_id, Integer
12
13
  attribute :default_budget, Integer
13
14
  attribute :import_all_accepted_proposals, Boolean
14
15
 
15
16
  validates :origin_component_id, :origin_component, :current_component, presence: true
16
17
  validates :import_all_accepted_proposals, allow_nil: false, acceptance: true
17
18
  validates :default_budget, presence: true, numericality: { greater_than: 0 }
19
+ validates :scope, presence: true, if: ->(form) { form.scope_id.present? }
20
+ validates :scope_id, scope_belongs_to_component: true, if: ->(form) { form.scope_id.present? }
18
21
 
19
22
  def origin_component
20
23
  @origin_component ||= origin_components.find_by(id: origin_component_id)
@@ -30,6 +33,14 @@ module Decidim
30
33
  end
31
34
  end
32
35
 
36
+ def scope
37
+ @scope ||= @scope_id ? current_component.scopes.find_by(id: @scope_id) : current_component.scope
38
+ end
39
+
40
+ def scope_id
41
+ @scope_id || scope&.id
42
+ end
43
+
33
44
  def budget
34
45
  @budget ||= context[:budget]
35
46
  end
@@ -20,7 +20,11 @@ module Decidim
20
20
  def current_order_budget_percent_minimum
21
21
  return 0 if current_order.minimum_projects_rule?
22
22
 
23
- component_settings.vote_threshold_percent
23
+ if current_order.projects_rule?
24
+ (current_order.minimum_projects.to_f / current_order.maximum_projects)
25
+ else
26
+ component_settings.vote_threshold_percent
27
+ end
24
28
  end
25
29
 
26
30
  def budget_confirm_disabled_attr
@@ -36,6 +40,46 @@ module Decidim
36
40
  def current_order_can_be_checked_out?
37
41
  current_order&.can_checkout?
38
42
  end
43
+
44
+ def current_rule_explanation
45
+ return unless current_order
46
+
47
+ if current_order.projects_rule?
48
+ if current_order.minimum_projects.positive? && current_order.minimum_projects < current_order.maximum_projects
49
+ t(
50
+ ".projects_rule.instruction",
51
+ minimum_number: current_order.minimum_projects,
52
+ maximum_number: current_order.maximum_projects
53
+ )
54
+ else
55
+ t(".projects_rule_maximum_only.instruction", maximum_number: current_order.maximum_projects)
56
+ end
57
+ elsif current_order.minimum_projects_rule?
58
+ t(".minimum_projects_rule.instruction", minimum_number: current_order.minimum_projects)
59
+ else
60
+ t(".vote_threshold_percent_rule.instruction", minimum_budget: budget_to_currency(current_order.minimum_budget))
61
+ end
62
+ end
63
+
64
+ def current_rule_description
65
+ return unless current_order
66
+
67
+ if current_order.projects_rule?
68
+ if current_order.minimum_projects.positive? && current_order.minimum_projects < current_order.maximum_projects
69
+ t(
70
+ ".projects_rule.description",
71
+ minimum_number: current_order.minimum_projects,
72
+ maximum_number: current_order.maximum_projects
73
+ )
74
+ else
75
+ t(".projects_rule_maximum_only.description", maximum_number: current_order.maximum_projects)
76
+ end
77
+ elsif current_order.minimum_projects_rule?
78
+ t(".minimum_projects_rule.description", minimum_number: current_order.minimum_projects)
79
+ else
80
+ t(".vote_threshold_percent_rule.description", minimum_budget: budget_to_currency(current_order.minimum_budget))
81
+ end
82
+ end
39
83
  end
40
84
  end
41
85
  end
@@ -18,24 +18,76 @@ module Decidim
18
18
  validates :budget, presence: true
19
19
  validate :user_belongs_to_organization
20
20
 
21
- validates :total_budget, numericality: {
22
- greater_than_or_equal_to: :minimum_budget
23
- }, if: :checked_out?
21
+ # Rules active for the budget threshold and minimum budgets rules.
22
+ with_options if: -> { !projects_rule? && checked_out? } do
23
+ validates :total_budget, numericality: {
24
+ greater_than_or_equal_to: :minimum_budget
25
+ }
26
+ end
27
+ with_options unless: :projects_rule? do
28
+ validates :total_budget, numericality: {
29
+ less_than_or_equal_to: :maximum_budget
30
+ }
31
+ end
24
32
 
25
- validates :total_budget, numericality: {
26
- less_than_or_equal_to: :maximum_budget
27
- }
33
+ # Rules active for the minimum projects rule.
34
+ with_options if: -> { minimum_projects_rule? && checked_out? } do
35
+ validates :total_projects, numericality: {
36
+ greater_than_or_equal_to: :minimum_projects
37
+ }
38
+ end
28
39
 
29
- validate :reach_minimum_projects, if: :checked_out?
40
+ # Rules active for the projects rule.
41
+ with_options if: -> { projects_rule? && checked_out? } do
42
+ validates :total_projects, numericality: {
43
+ greater_than_or_equal_to: :minimum_projects
44
+ }
45
+
46
+ validates :total_projects, numericality: {
47
+ less_than_or_equal_to: :maximum_projects
48
+ }
49
+ end
30
50
 
31
51
  scope :finished, -> { where.not(checked_out_at: nil) }
32
52
  scope :pending, -> { where(checked_out_at: nil) }
33
53
 
54
+ # Public: Returns the available budget allocation the user is able to
55
+ # allocate to this order or the maximum amount of projects to be selected
56
+ # in case the project selection voting is enabled.
57
+ def available_allocation
58
+ return maximum_projects if projects_rule?
59
+
60
+ maximum_budget
61
+ end
62
+
63
+ # Public: Returns the numeric amount the given project should allocate
64
+ # from the total available allocation when it is added to the order. The
65
+ # allocation is normally the project's budget but for project selection
66
+ # voting, the allocation is one.
67
+ def allocation_for(project)
68
+ return 1 if projects_rule?
69
+
70
+ project.budget_amount
71
+ end
72
+
34
73
  # Public: Returns the sum of project budgets
35
74
  def total_budget
36
75
  projects.to_a.sum(&:budget_amount)
37
76
  end
38
77
 
78
+ # Public: Returns the count of projects
79
+ def total_projects
80
+ projects.count
81
+ end
82
+
83
+ # Public: For budget voting returns the total budget and for project
84
+ # selection voting, returns the amount of selected projects.
85
+ def total
86
+ return total_projects if projects_rule?
87
+
88
+ total_budget
89
+ end
90
+
39
91
  # Public: Returns true if the order has been checked out
40
92
  def checked_out?
41
93
  checked_out_at.present?
@@ -43,22 +95,28 @@ module Decidim
43
95
 
44
96
  # Public: Check if the order total budget is enough to checkout
45
97
  def can_checkout?
46
- if minimum_projects_rule?
47
- projects.count >= minimum_projects
98
+ if projects_rule?
99
+ total_projects >= minimum_projects && total_projects <= maximum_projects
100
+ elsif minimum_projects_rule?
101
+ total_projects >= minimum_projects
48
102
  else
49
103
  total_budget.to_f >= minimum_budget
50
104
  end
51
105
  end
52
106
 
53
107
  # Public: Returns the order budget percent from the settings total budget
108
+ # or the progress for selected projects if the selected project rule is
109
+ # enabled
54
110
  def budget_percent
55
- (total_budget.to_f / budget.total_budget.to_f) * 100
111
+ return (total_projects.to_f / maximum_projects) * 100 if projects_rule?
112
+
113
+ (total_budget.to_f / budget.total_budget) * 100
56
114
  end
57
115
 
58
116
  # Public: Returns the required minimum budget to checkout
59
117
  def minimum_budget
60
118
  return 0 unless budget
61
- return 0 if minimum_projects_rule?
119
+ return 0 if minimum_projects_rule? || projects_rule?
62
120
 
63
121
  budget.total_budget.to_f * (budget.settings.vote_threshold_percent.to_f / 100)
64
122
  end
@@ -77,11 +135,35 @@ module Decidim
77
135
  budget.settings.vote_rule_minimum_budget_projects_enabled
78
136
  end
79
137
 
138
+ # Public: Returns true if the project voting rule is enabled
139
+ def projects_rule?
140
+ return unless budget
141
+
142
+ budget.settings.vote_rule_selected_projects_enabled
143
+ end
144
+
80
145
  # Public: Returns the required minimum projects to checkout
81
146
  def minimum_projects
82
147
  return 0 unless budget
83
148
 
84
- budget.settings.vote_minimum_budget_projects_number
149
+ if minimum_projects_rule?
150
+ budget.settings.vote_minimum_budget_projects_number
151
+ elsif projects_rule?
152
+ budget.settings.vote_selected_projects_minimum
153
+ else
154
+ 0
155
+ end
156
+ end
157
+
158
+ # Public: Returns the required maximum projects to checkout
159
+ def maximum_projects
160
+ return nil unless budget
161
+
162
+ if projects_rule?
163
+ budget.settings.vote_selected_projects_maximum
164
+ else
165
+ 0
166
+ end
85
167
  end
86
168
 
87
169
  def self.user_collection(user)
@@ -112,12 +194,6 @@ module Decidim
112
194
 
113
195
  errors.add(:user, :invalid) unless user.organization == organization
114
196
  end
115
-
116
- def reach_minimum_projects
117
- return unless minimum_projects_rule?
118
-
119
- errors.add(:projects, :invalid) if minimum_projects > projects.count
120
- end
121
197
  end
122
198
  end
123
199
  end
@@ -10,7 +10,7 @@ module Decidim
10
10
  case permission_action.subject
11
11
  when :budget
12
12
  case permission_action.action
13
- when :create, :read
13
+ when :create, :read, :export
14
14
  allow!
15
15
  when :update
16
16
  toggle_allow(budget)
@@ -1,9 +1,17 @@
1
- <div class="card">
1
+ <div class="card with-overflow">
2
2
  <div class="card-divider">
3
- <h2 class="card-title">
4
- <%= t(".title") %>
5
-
6
- <%= link_to t("actions.new", scope: "decidim.budgets", name: t("models.budget.name", scope: "decidim.budgets.admin")), new_budget_path, class: "button tiny button--title" if allowed_to? :create, :budget %>
3
+ <h2 class="card-title flex--sbc">
4
+ <div>
5
+ <%= t(".title") %>
6
+ </div>
7
+ <div class="flex--cc flex-gap--1">
8
+ <% if allowed_to? :export, :budget %>
9
+ <%= export_dropdown %>
10
+ <% end %>
11
+ <div id="js-other-actions-wrapper">
12
+ <%= link_to t("actions.new", scope: "decidim.budgets", name: t("models.budget.name", scope: "decidim.budgets.admin")), new_budget_path, class: "button tiny button--title" if allowed_to? :create, :budget %>
13
+ </div>
14
+ </div>
7
15
  </h2>
8
16
  </div>
9
17
 
@@ -55,4 +63,13 @@
55
63
  </table>
56
64
  </div>
57
65
  </div>
66
+ <div class="card-divider">
67
+ <strong><%= t ".finished_orders" %>:&nbsp;</strong><span><%= finished_orders.count %></span>
68
+ <span>&nbsp;|&nbsp;</span>
69
+ <strong><%= t ".pending_orders" %>:&nbsp;</strong><span><%= pending_orders.count %></span>
70
+ <span>&nbsp;|&nbsp;</span>
71
+ <strong><%= t ".users_with_finished_orders" %>:&nbsp;</strong><span><%= users_with_finished_orders.count %></span>
72
+ <span>&nbsp;|&nbsp;</span>
73
+ <strong><%= t ".users_with_pending_orders" %>:&nbsp;</strong><span><%= users_with_pending_orders.count %></span>
74
+ </div>
58
75
  </div>
@@ -1,10 +1,17 @@
1
- <div class="card">
1
+ <div class="card with-overflow">
2
2
  <div class="card-divider">
3
- <h2 class="card-title">
4
- <%= link_to translated_attribute(budget.title), budgets_path %> &gt;
5
- <%= t(".title") %>
6
- <%= link_to t("actions.new", scope: "decidim.budgets", name: t("models.project.name", scope: "decidim.budgets.admin")), new_budget_project_path, class: "button tiny button--title new" if allowed_to? :create, :project %>
7
- <%= link_to t("actions.import", scope: "decidim.budgets", name: t("models.project.name", scope: "decidim.budgets.admin")), new_budget_proposals_import_path(budget), class: "button tiny button--title" if allowed_to? :import_proposals, :project %>
3
+ <h2 class="card-title flex--sbc">
4
+ <div>
5
+ <%= link_to translated_attribute(budget.title), budgets_path %> &gt;
6
+ <%= t(".title") %>
7
+ </div>
8
+ <div class="flex--cc flex-gap--1">
9
+ <%= link_to t("actions.import", scope: "decidim.budgets", name: t("models.project.name", scope: "decidim.budgets.admin")), new_budget_proposals_import_path(budget), class: "button tiny button--title" if allowed_to? :import_proposals, :project %>
10
+ <% if allowed_to? :export, :budget %>
11
+ <%= export_dropdown(current_component, budget.id) %>
12
+ <% end %>
13
+ <%= link_to t("actions.new", scope: "decidim.budgets", name: t("models.project.name", scope: "decidim.budgets.admin")), new_budget_project_path, class: "button tiny button--title new" if allowed_to? :create, :project %>
14
+ </div>
8
15
  </h2>
9
16
  </div>
10
17
 
@@ -2,13 +2,18 @@
2
2
  <% if @form.origin_components.any? %>
3
3
  <div class="card">
4
4
  <div class="card-divider">
5
- <h2 class="card-title"><%= title %></h2>
5
+ <h2 class="card-title"><%= t(".title") %></h2>
6
6
  </div>
7
7
 
8
8
  <div class="card-section">
9
9
  <div class="row column">
10
10
  <%= f.select :origin_component_id, @form.origin_components_collection, prompt: t(".select_component") %>
11
11
  </div>
12
+ <% if current_component.scopes_enabled? %>
13
+ <div class="row column">
14
+ <%= scopes_picker_field f, :scope_id, root: budget.scope %>
15
+ </div>
16
+ <% end %>
12
17
  <div class="row column">
13
18
  <%= f.number_field :default_budget %>
14
19
  </div>
@@ -1,11 +1,11 @@
1
1
  <div class="reveal" data-reveal id="budget-excess">
2
2
  <div class="reveal__header">
3
- <h3 class="reveal__title"><%= t(".title") %></h3>
3
+ <h3 class="reveal__title"><%= current_order.projects_rule? ? t(".projects_excess.title") : t(".budget_excess.title") %></h3>
4
4
  <button class="close-button" data-close aria-label="<%= t(".close") %>" type="button">
5
5
  <span aria-hidden="true">&times;</span>
6
6
  </button>
7
7
  </div>
8
- <p><%= t(".description") %></p>
8
+ <p><%= current_order.projects_rule? ? t(".projects_excess.description") : t(".budget_excess.description") %></p>
9
9
  <div class="row">
10
10
  <div class="columns medium-8 medium-offset-2">
11
11
  <button data-close class="button expanded"><%= t(".ok") %></button>