decidim-budgets 0.19.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/images/decidim/budgets/icon.svg +1 -11
  3. data/app/assets/javascripts/decidim/budgets/projects.js.es6 +26 -4
  4. data/app/assets/stylesheets/decidim/budgets/budget/_budget-list.scss +147 -24
  5. data/app/cells/decidim/budgets/project_list_item/project_data.erb +5 -0
  6. data/app/cells/decidim/budgets/project_list_item/project_data_number.erb +3 -0
  7. data/app/cells/decidim/budgets/project_list_item/project_data_vote_button.erb +15 -0
  8. data/app/cells/decidim/budgets/project_list_item/project_data_votes.erb +7 -0
  9. data/app/cells/decidim/budgets/project_list_item/project_image.erb +5 -0
  10. data/app/cells/decidim/budgets/project_list_item/project_text.erb +17 -0
  11. data/app/cells/decidim/budgets/project_list_item/show.erb +5 -0
  12. data/app/cells/decidim/budgets/project_list_item_cell.rb +86 -0
  13. data/app/cells/decidim/budgets/project_m/data.erb +2 -2
  14. data/app/cells/decidim/budgets/project_m/footer.erb +1 -1
  15. data/app/commands/decidim/budgets/admin/create_project.rb +19 -9
  16. data/app/commands/decidim/budgets/admin/update_project.rb +12 -1
  17. data/app/commands/decidim/budgets/checkout.rb +2 -1
  18. data/app/controllers/concerns/decidim/budgets/needs_current_order.rb +5 -1
  19. data/app/controllers/concerns/decidim/budgets/orderable.rb +61 -0
  20. data/app/controllers/decidim/budgets/admin/projects_controller.rb +8 -5
  21. data/app/controllers/decidim/budgets/line_items_controller.rb +1 -1
  22. data/app/controllers/decidim/budgets/projects_controller.rb +23 -3
  23. data/app/forms/decidim/budgets/admin/component_form.rb +44 -0
  24. data/app/forms/decidim/budgets/admin/project_form.rb +14 -2
  25. data/app/helpers/decidim/budgets/application_helper.rb +1 -0
  26. data/app/helpers/decidim/budgets/projects_helper.rb +17 -0
  27. data/app/jobs/decidim/budgets/send_order_summary_job.rb +15 -0
  28. data/app/mailers/decidim/budgets/order_summary_mailer.rb +34 -0
  29. data/app/models/decidim/budgets/order.rb +28 -1
  30. data/app/models/decidim/budgets/project.rb +13 -0
  31. data/app/services/decidim/budgets/project_search.rb +8 -0
  32. data/app/types/decidim/budgets/budgets_type.rb +32 -0
  33. data/app/types/decidim/budgets/project_type.rb +25 -0
  34. data/app/views/decidim/budgets/admin/projects/_form.html.erb +3 -6
  35. data/app/views/decidim/budgets/admin/projects/proposals_picker.html.erb +1 -0
  36. data/app/views/decidim/budgets/order_summary_mailer/order_summary.html.erb +21 -0
  37. data/app/views/decidim/budgets/projects/_budget_confirm.html.erb +1 -1
  38. data/app/views/decidim/budgets/projects/_budget_summary.html.erb +8 -2
  39. data/app/views/decidim/budgets/projects/_filters.html.erb +10 -6
  40. data/app/views/decidim/budgets/projects/_linked_projects.html.erb +1 -1
  41. data/app/views/decidim/budgets/projects/_order_progress.html.erb +8 -16
  42. data/app/views/decidim/budgets/projects/_project.html.erb +1 -57
  43. data/app/views/decidim/budgets/projects/_project_budget_button.html.erb +3 -3
  44. data/app/views/decidim/budgets/projects/_projects.html.erb +8 -1
  45. data/app/views/decidim/budgets/projects/index.html.erb +3 -3
  46. data/app/views/decidim/budgets/projects/index.js.erb +8 -0
  47. data/app/views/decidim/budgets/projects/show.html.erb +1 -1
  48. data/config/locales/ar.yml +1 -6
  49. data/config/locales/bg-BG.yml +7 -0
  50. data/config/locales/ca.yml +27 -5
  51. data/config/locales/cs.yml +44 -22
  52. data/config/locales/da-DK.yml +1 -0
  53. data/config/locales/de.yml +27 -5
  54. data/config/locales/el-GR.yml +1 -0
  55. data/config/locales/el.yml +175 -0
  56. data/config/locales/en.yml +27 -5
  57. data/config/locales/es-MX.yml +27 -5
  58. data/config/locales/es-PY.yml +27 -5
  59. data/config/locales/es.yml +27 -5
  60. data/config/locales/et-EE.yml +1 -0
  61. data/config/locales/eu.yml +1 -6
  62. data/config/locales/fi-plain.yml +27 -5
  63. data/config/locales/fi.yml +40 -18
  64. data/config/locales/fr-CA.yml +175 -0
  65. data/config/locales/fr.yml +27 -5
  66. data/config/locales/ga-IE.yml +1 -0
  67. data/config/locales/gl.yml +1 -6
  68. data/config/locales/hr-HR.yml +1 -0
  69. data/config/locales/hu.yml +17 -6
  70. data/config/locales/id-ID.yml +1 -6
  71. data/config/locales/is-IS.yml +114 -0
  72. data/config/locales/it.yml +28 -6
  73. data/config/locales/ja-JP.yml +171 -0
  74. data/config/locales/lt-LT.yml +1 -0
  75. data/config/locales/lv-LV.yml +172 -0
  76. data/config/locales/mt-MT.yml +1 -0
  77. data/config/locales/nl.yml +32 -10
  78. data/config/locales/no.yml +160 -0
  79. data/config/locales/pl.yml +74 -52
  80. data/config/locales/pt-BR.yml +2 -7
  81. data/config/locales/pt.yml +64 -42
  82. data/config/locales/ro-RO.yml +179 -0
  83. data/config/locales/ru.yml +1 -6
  84. data/config/locales/sk-SK.yml +180 -0
  85. data/config/locales/sk.yml +175 -0
  86. data/config/locales/sl.yml +5 -0
  87. data/config/locales/sr-CS.yml +29 -0
  88. data/config/locales/sv.yml +27 -5
  89. data/config/locales/tr-TR.yml +1 -6
  90. data/config/locales/uk.yml +1 -6
  91. data/lib/decidim/budgets/admin_engine.rb +2 -0
  92. data/lib/decidim/budgets/component.rb +12 -0
  93. data/lib/decidim/budgets/seeds/city.jpeg +0 -0
  94. data/lib/decidim/budgets/test/factories.rb +33 -0
  95. data/lib/decidim/budgets/version.rb +1 -1
  96. metadata +55 -15
@@ -6,6 +6,9 @@ module Decidim
6
6
  # This command is executed when the user changes a Project from the admin
7
7
  # panel.
8
8
  class UpdateProject < Rectify::Command
9
+ include ::Decidim::AttachmentMethods
10
+ include ::Decidim::GalleryMethods
11
+
9
12
  # Initializes an UpdateProject Command.
10
13
  #
11
14
  # form - The form from which to get the data.
@@ -13,6 +16,7 @@ module Decidim
13
16
  def initialize(form, project)
14
17
  @form = form
15
18
  @project = project
19
+ @attached_to = project
16
20
  end
17
21
 
18
22
  # Updates the project if valid.
@@ -21,9 +25,16 @@ module Decidim
21
25
  def call
22
26
  return broadcast(:invalid) if form.invalid?
23
27
 
28
+ if process_gallery?
29
+ build_gallery
30
+ return broadcast(:invalid) if gallery_invalid?
31
+ end
32
+
24
33
  transaction do
25
34
  update_project
26
35
  link_proposals
36
+ create_gallery if process_gallery?
37
+ photo_cleanup!
27
38
  end
28
39
 
29
40
  broadcast(:ok)
@@ -31,7 +42,7 @@ module Decidim
31
42
 
32
43
  private
33
44
 
34
- attr_reader :project, :form
45
+ attr_reader :project, :form, :gallery
35
46
 
36
47
  def update_project
37
48
  Decidim.traceability.update!(
@@ -28,9 +28,10 @@ module Decidim
28
28
  private
29
29
 
30
30
  def checkout!
31
- return unless @order
31
+ return unless @order && @order.valid?
32
32
 
33
33
  @order.with_lock do
34
+ SendOrderSummaryJob.perform_later(@order)
34
35
  @order.checked_out_at = Time.current
35
36
  @order.save
36
37
  end
@@ -15,12 +15,16 @@ module Decidim
15
15
  #
16
16
  # Returns an Order.
17
17
  def current_order
18
- @current_order ||= Order.includes(:projects).find_by(user: current_user, component: current_component)
18
+ @current_order ||= Order.includes(:projects).find_or_initialize_by(user: current_user, component: current_component)
19
19
  end
20
20
 
21
21
  def current_order=(order)
22
22
  @current_order = order
23
23
  end
24
+
25
+ def persisted_current_order
26
+ current_order if current_order&.persisted?
27
+ end
24
28
  end
25
29
  end
26
30
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Decidim
6
+ module Budgets
7
+ # Common logic to sorting resources
8
+ module Orderable
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ include Decidim::Orderable
13
+
14
+ private
15
+
16
+ # Available orders based on enabled settings
17
+ def available_orders
18
+ @available_orders ||= begin
19
+ available_orders = []
20
+ available_orders << "random" if voting_is_open? || !votes_are_visible?
21
+ available_orders << "most_voted" if votes_are_visible?
22
+ available_orders += %w(highest_cost lowest_cost)
23
+ available_orders
24
+ end
25
+ end
26
+
27
+ def default_order
28
+ available_orders.first
29
+ end
30
+
31
+ def voting_is_open?
32
+ current_settings.votes_enabled?
33
+ end
34
+
35
+ def votes_are_visible?
36
+ current_settings.show_votes?
37
+ end
38
+
39
+ def reorder(projects)
40
+ case order
41
+ when "highest_cost"
42
+ projects.order(budget: :desc)
43
+ when "lowest_cost"
44
+ projects.order(budget: :asc)
45
+ when "most_voted"
46
+ if votes_are_visible?
47
+ ids = projects.sort_by(&:confirmed_orders_count).map(&:id).reverse
48
+ projects.ordered_ids(ids)
49
+ else
50
+ projects
51
+ end
52
+ when "random"
53
+ projects.order_randomly(random_seed)
54
+ else
55
+ projects
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -5,12 +5,16 @@ module Decidim
5
5
  module Admin
6
6
  # This controller allows an admin to manage projects from a Participatory Process
7
7
  class ProjectsController < Admin::ApplicationController
8
- helper_method :projects, :finished_orders, :pending_orders
8
+ include Decidim::ApplicationHelper
9
+ include Decidim::Proposals::Admin::Picker
10
+
11
+ helper_method :projects, :finished_orders, :pending_orders, :present
9
12
 
10
13
  def new
11
14
  enforce_permission_to :create, :project
12
-
13
- @form = form(ProjectForm).instance
15
+ @form = form(ProjectForm).from_params(
16
+ attachment: form(AttachmentForm).instance
17
+ )
14
18
  end
15
19
 
16
20
  def create
@@ -33,13 +37,12 @@ module Decidim
33
37
 
34
38
  def edit
35
39
  enforce_permission_to :update, :project, project: project
36
-
37
40
  @form = form(ProjectForm).from_model(project)
41
+ @form.attachment = form(AttachmentForm).instance
38
42
  end
39
43
 
40
44
  def update
41
45
  enforce_permission_to :update, :project, project: project
42
-
43
46
  @form = form(ProjectForm).from_params(params)
44
47
 
45
48
  UpdateProject.call(@form, project) do
@@ -12,7 +12,7 @@ module Decidim
12
12
  enforce_permission_to :vote, :project, project: project
13
13
 
14
14
  respond_to do |format|
15
- AddLineItem.call(current_order, project, current_user) do
15
+ AddLineItem.call(persisted_current_order, project, current_user) do
16
16
  on(:ok) do |order|
17
17
  self.current_order = order
18
18
  format.html { redirect_to :back }
@@ -7,13 +7,17 @@ module Decidim
7
7
  include FilterResource
8
8
  include NeedsCurrentOrder
9
9
  include Orderable
10
+ include Decidim::Budgets::Orderable
10
11
 
11
12
  helper_method :projects, :project
12
13
 
13
14
  private
14
15
 
15
16
  def projects
16
- @projects ||= search.results.order_randomly(random_seed).page(params[:page]).per(current_component.settings.projects_per_page)
17
+ return @projects if @projects
18
+
19
+ @projects = search.results.page(params[:page]).per(current_component.settings.projects_per_page)
20
+ @projects = reorder(@projects)
17
21
  end
18
22
 
19
23
  def project
@@ -27,11 +31,27 @@ module Decidim
27
31
  def default_filter_params
28
32
  {
29
33
  search_text: "",
30
- scope_id: "",
31
- category_id: ""
34
+ scope_id: default_filter_scope_params,
35
+ category_id: default_filter_category_params
32
36
  }
33
37
  end
34
38
 
39
+ def default_filter_category_params
40
+ return "all" unless current_component.participatory_space.categories.any?
41
+
42
+ ["all"] + current_component.participatory_space.categories.map { |category| category.id.to_s }
43
+ end
44
+
45
+ def default_filter_scope_params
46
+ return "all" unless current_component.participatory_space.scopes.any?
47
+
48
+ if current_component.participatory_space.scope
49
+ ["all", current_component.participatory_space.scope.id] + current_component.participatory_space.scope.children.map { |scope| scope.id.to_s }
50
+ else
51
+ %w(all global) + current_component.participatory_space.scopes.map { |scope| scope.id.to_s }
52
+ end
53
+ end
54
+
35
55
  def context_params
36
56
  { component: current_component, organization: current_organization }
37
57
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Budgets
5
+ module Admin
6
+ # A form object for the budgets component. Used to attach the component
7
+ # to a participatory process from the admin panel.
8
+ #
9
+ class ComponentForm < Decidim::Admin::ComponentForm
10
+ validate :budget_voting_rule_enabled_setting, :budget_voting_rule_value_setting
11
+
12
+ private
13
+
14
+ # Validations on budget settings:
15
+ # - a voting rule must be enabled.
16
+ def budget_voting_rule_enabled_setting
17
+ return unless manifest&.name == :budgets
18
+
19
+ 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))
23
+ end
24
+
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))
28
+ end
29
+ end
30
+
31
+ # - the value must be a valid number
32
+ def budget_voting_rule_value_setting
33
+ return unless manifest&.name == :budgets
34
+
35
+ 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
37
+
38
+ 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
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -16,6 +16,9 @@ module Decidim
16
16
  attribute :decidim_scope_id, Integer
17
17
  attribute :decidim_category_id, Integer
18
18
  attribute :proposal_ids, Array[Integer]
19
+ attribute :attachment, AttachmentForm
20
+ attribute :photos, Array[String]
21
+ attribute :add_photos, Array
19
22
 
20
23
  validates :title, translatable_presence: true
21
24
  validates :description, translatable_presence: true
@@ -26,6 +29,8 @@ module Decidim
26
29
 
27
30
  validate :scope_belongs_to_participatory_space_scope
28
31
 
32
+ validate :notify_missing_attachment_if_errored
33
+
29
34
  delegate :categories, to: :current_component
30
35
 
31
36
  def map_model(model)
@@ -38,9 +43,8 @@ module Decidim
38
43
 
39
44
  def proposals
40
45
  @proposals ||= Decidim.find_resource_manifest(:proposals).try(:resource_scope, current_component)
41
- &.published
46
+ &.where(id: proposal_ids)
42
47
  &.order(title: :asc)
43
- &.map { |proposal| [present(proposal).title, proposal.id] }
44
48
  end
45
49
 
46
50
  # Finds the Category from the decidim_category_id.
@@ -69,6 +73,14 @@ module Decidim
69
73
  def scope_belongs_to_participatory_space_scope
70
74
  errors.add(:decidim_scope_id, :invalid) if current_participatory_space.out_of_scope?(scope)
71
75
  end
76
+
77
+ # This method will add an error to the `attachment` field only if there's
78
+ # any error in any other field. This is needed because when the form has
79
+ # an error, the attachment is lost, so we need a way to inform the user of
80
+ # this problem.
81
+ def notify_missing_attachment_if_errored
82
+ errors.add(:add_photos, :needs_to_be_reattached) if errors.any? && add_photos.present?
83
+ end
72
84
  end
73
85
  end
74
86
  end
@@ -8,6 +8,7 @@ module Decidim
8
8
  include PaginateHelper
9
9
  include Decidim::Comments::CommentsHelper
10
10
  include ProjectsHelper
11
+ include Decidim::CheckBoxesTreeHelper
11
12
  end
12
13
  end
13
14
  end
@@ -16,6 +16,19 @@ module Decidim
16
16
  current_order&.budget_percent.to_f.floor
17
17
  end
18
18
 
19
+ # Return the minimum percentage of the current order budget from the total budget
20
+ def current_order_budget_percent_minimum
21
+ return 0 if current_order.minimum_projects_rule?
22
+
23
+ component_settings.vote_threshold_percent
24
+ end
25
+
26
+ def budget_confirm_disabled_attr
27
+ return if current_order_can_be_checked_out?
28
+
29
+ %( disabled="disabled" ).html_safe
30
+ end
31
+
19
32
  # Return true if the current order is checked out
20
33
  delegate :checked_out?, to: :current_order, prefix: true, allow_nil: true
21
34
 
@@ -23,6 +36,10 @@ module Decidim
23
36
  def current_order_can_be_checked_out?
24
37
  current_order&.can_checkout?
25
38
  end
39
+
40
+ def projects_base_url
41
+ URI.parse(root_url).tap { |uri| uri.query = uri.fragment = nil } .to_s
42
+ end
26
43
  end
27
44
  end
28
45
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Budgets
5
+ class SendOrderSummaryJob < ApplicationJob
6
+ queue_as :default
7
+
8
+ def perform(order)
9
+ return if order&.user&.email.blank?
10
+
11
+ OrderSummaryMailer.order_summary(order).deliver_now
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Budgets
5
+ class OrderSummaryMailer < Decidim::ApplicationMailer
6
+ include Decidim::TranslationsHelper
7
+ include Decidim::SanitizeHelper
8
+
9
+ helper Decidim::TranslationsHelper
10
+
11
+ # Send an email to an user with the summary of the order.
12
+ #
13
+ # order - the order that was just created
14
+ def order_summary(order)
15
+ user = order.user
16
+
17
+ with_user(user) do
18
+ @user = user
19
+ @order = order
20
+ @space = order.participatory_space
21
+ @component = order.component
22
+ @organization = order.participatory_space.organization
23
+
24
+ subject = I18n.t(
25
+ "order_summary.subject",
26
+ scope: "decidim.budgets.order_summary_mailer",
27
+ space_name: translated_attribute(@space.title)
28
+ )
29
+ mail(to: user.email, subject: subject)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -27,6 +27,8 @@ module Decidim
27
27
  less_than_or_equal_to: :maximum_budget
28
28
  }
29
29
 
30
+ validate :reach_minimum_projects, if: :checked_out?
31
+
30
32
  scope :finished, -> { where.not(checked_out_at: nil) }
31
33
  scope :pending, -> { where(checked_out_at: nil) }
32
34
 
@@ -42,7 +44,11 @@ module Decidim
42
44
 
43
45
  # Public: Check if the order total budget is enough to checkout
44
46
  def can_checkout?
45
- total_budget.to_f >= minimum_budget
47
+ if minimum_projects_rule?
48
+ projects.count >= minimum_projects
49
+ else
50
+ total_budget.to_f >= minimum_budget
51
+ end
46
52
  end
47
53
 
48
54
  # Public: Returns the order budget percent from the settings total budget
@@ -53,6 +59,7 @@ module Decidim
53
59
  # Public: Returns the required minimum budget to checkout
54
60
  def minimum_budget
55
61
  return 0 unless component
62
+ return 0 if minimum_projects_rule?
56
63
 
57
64
  component.settings.total_budget.to_f * (component.settings.vote_threshold_percent.to_f / 100)
58
65
  end
@@ -64,6 +71,20 @@ module Decidim
64
71
  component.settings.total_budget.to_f
65
72
  end
66
73
 
74
+ # Public: Returns if it is required a minimum projects limit to checkout
75
+ def minimum_projects_rule?
76
+ return unless component
77
+
78
+ component.settings.vote_rule_minimum_budget_projects_enabled
79
+ end
80
+
81
+ # Public: Returns the required minimum projects to checkout
82
+ def minimum_projects
83
+ return 0 unless component
84
+
85
+ component.settings.vote_minimum_budget_projects_number
86
+ end
87
+
67
88
  def self.user_collection(user)
68
89
  where(decidim_user_id: user.id)
69
90
  end
@@ -87,6 +108,12 @@ module Decidim
87
108
 
88
109
  errors.add(:user, :invalid) unless user.organization == organization
89
110
  end
111
+
112
+ def reach_minimum_projects
113
+ return unless minimum_projects_rule?
114
+
115
+ errors.add(:projects, :invalid) if minimum_projects > projects.count
116
+ end
90
117
  end
91
118
  end
92
119
  end