decidim-budgets 0.21.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) 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 +5 -1
  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/projects_helper.rb +17 -0
  26. data/app/jobs/decidim/budgets/send_order_summary_job.rb +15 -0
  27. data/app/mailers/decidim/budgets/order_summary_mailer.rb +34 -0
  28. data/app/models/decidim/budgets/order.rb +28 -1
  29. data/app/models/decidim/budgets/project.rb +4 -0
  30. data/app/views/decidim/budgets/admin/projects/_form.html.erb +3 -6
  31. data/app/views/decidim/budgets/admin/projects/proposals_picker.html.erb +1 -0
  32. data/app/views/decidim/budgets/order_summary_mailer/order_summary.html.erb +21 -0
  33. data/app/views/decidim/budgets/projects/_budget_confirm.html.erb +1 -1
  34. data/app/views/decidim/budgets/projects/_budget_summary.html.erb +8 -2
  35. data/app/views/decidim/budgets/projects/_filters.html.erb +10 -6
  36. data/app/views/decidim/budgets/projects/_linked_projects.html.erb +1 -1
  37. data/app/views/decidim/budgets/projects/_order_progress.html.erb +8 -16
  38. data/app/views/decidim/budgets/projects/_project.html.erb +1 -57
  39. data/app/views/decidim/budgets/projects/_project_budget_button.html.erb +3 -3
  40. data/app/views/decidim/budgets/projects/_projects.html.erb +8 -1
  41. data/app/views/decidim/budgets/projects/index.html.erb +3 -3
  42. data/app/views/decidim/budgets/projects/index.js.erb +8 -0
  43. data/app/views/decidim/budgets/projects/show.html.erb +1 -1
  44. data/config/locales/ar.yml +0 -5
  45. data/config/locales/bg-BG.yml +7 -0
  46. data/config/locales/ca.yml +26 -4
  47. data/config/locales/cs.yml +43 -21
  48. data/config/locales/da-DK.yml +1 -0
  49. data/config/locales/de.yml +26 -4
  50. data/config/locales/el.yml +174 -0
  51. data/config/locales/en.yml +26 -4
  52. data/config/locales/es-MX.yml +26 -4
  53. data/config/locales/es-PY.yml +26 -4
  54. data/config/locales/es.yml +26 -4
  55. data/config/locales/et-EE.yml +1 -0
  56. data/config/locales/eu.yml +0 -5
  57. data/config/locales/fi-plain.yml +26 -4
  58. data/config/locales/fi.yml +37 -15
  59. data/config/locales/fr-CA.yml +175 -0
  60. data/config/locales/fr.yml +26 -4
  61. data/config/locales/ga-IE.yml +1 -0
  62. data/config/locales/gl.yml +0 -5
  63. data/config/locales/hr-HR.yml +1 -0
  64. data/config/locales/hu.yml +16 -5
  65. data/config/locales/id-ID.yml +0 -5
  66. data/config/locales/is-IS.yml +0 -5
  67. data/config/locales/it.yml +27 -5
  68. data/config/locales/ja-JP.yml +171 -0
  69. data/config/locales/lt-LT.yml +1 -0
  70. data/config/locales/lv-LV.yml +172 -0
  71. data/config/locales/mt-MT.yml +1 -0
  72. data/config/locales/nl.yml +26 -4
  73. data/config/locales/no.yml +20 -7
  74. data/config/locales/pl.yml +73 -51
  75. data/config/locales/pt-BR.yml +1 -6
  76. data/config/locales/pt.yml +63 -41
  77. data/config/locales/ro-RO.yml +179 -0
  78. data/config/locales/ru.yml +0 -5
  79. data/config/locales/sk-SK.yml +180 -0
  80. data/config/locales/sk.yml +175 -0
  81. data/config/locales/sl.yml +5 -0
  82. data/config/locales/sr-CS.yml +29 -0
  83. data/config/locales/sv.yml +26 -4
  84. data/config/locales/tr-TR.yml +0 -5
  85. data/config/locales/uk.yml +0 -5
  86. data/lib/decidim/budgets/admin_engine.rb +2 -0
  87. data/lib/decidim/budgets/component.rb +4 -0
  88. data/lib/decidim/budgets/seeds/city.jpeg +0 -0
  89. data/lib/decidim/budgets/test/factories.rb +33 -0
  90. data/lib/decidim/budgets/version.rb +1 -1
  91. metadata +49 -14
@@ -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
@@ -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
@@ -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
@@ -31,6 +31,10 @@ module Decidim
31
31
  datetime: :created_at
32
32
  )
33
33
 
34
+ def self.ordered_ids(ids)
35
+ order(Arel.sql("position(id::text in '#{ids.join(",")}')"))
36
+ end
37
+
34
38
  def self.log_presenter_class_for(_log)
35
39
  Decidim::Budgets::AdminLog::ProjectPresenter
36
40
  end