decidim-budgets 0.26.2 → 0.27.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/budgets/order_activity_cell.rb +29 -0
  3. data/app/commands/decidim/budgets/add_line_item.rb +4 -2
  4. data/app/commands/decidim/budgets/admin/create_budget.rb +1 -1
  5. data/app/commands/decidim/budgets/admin/create_order_reminders.rb +66 -0
  6. data/app/commands/decidim/budgets/admin/create_project.rb +5 -2
  7. data/app/commands/decidim/budgets/admin/destroy_budget.rb +1 -1
  8. data/app/commands/decidim/budgets/admin/destroy_project.rb +1 -1
  9. data/app/commands/decidim/budgets/admin/import_proposals_to_budgets.rb +5 -2
  10. data/app/commands/decidim/budgets/admin/update_budget.rb +1 -1
  11. data/app/commands/decidim/budgets/admin/update_project.rb +5 -2
  12. data/app/commands/decidim/budgets/admin/update_project_category.rb +48 -0
  13. data/app/commands/decidim/budgets/admin/update_project_scope.rb +54 -0
  14. data/app/commands/decidim/budgets/admin/update_project_selection.rb +56 -0
  15. data/app/commands/decidim/budgets/cancel_order.rb +1 -1
  16. data/app/commands/decidim/budgets/checkout.rb +10 -3
  17. data/app/commands/decidim/budgets/remove_line_item.rb +1 -1
  18. data/app/controllers/decidim/budgets/admin/projects_controller.rb +149 -1
  19. data/app/controllers/decidim/budgets/projects_controller.rb +12 -12
  20. data/app/forms/decidim/budgets/admin/budget_form.rb +2 -2
  21. data/app/forms/decidim/budgets/admin/order_reminder_form.rb +75 -0
  22. data/app/forms/decidim/budgets/admin/project_form.rb +19 -3
  23. data/app/helpers/decidim/budgets/admin/project_bulk_actions_helper.rb +20 -0
  24. data/app/helpers/decidim/budgets/projects_helper.rb +29 -0
  25. data/app/jobs/decidim/budgets/send_vote_reminder_job.rb +16 -0
  26. data/app/mailers/decidim/budgets/vote_reminder_mailer.rb +39 -0
  27. data/app/models/decidim/budgets/order.rb +2 -3
  28. data/app/models/decidim/budgets/project.rb +12 -4
  29. data/app/packs/entrypoints/decidim_budgets_admin.js +1 -0
  30. data/app/packs/src/decidim/budgets/admin/projects.js +143 -0
  31. data/app/permissions/decidim/budgets/admin/permissions.rb +8 -3
  32. data/app/queries/decidim/budgets/filtered_projects.rb +1 -1
  33. data/app/serializers/decidim/budgets/{data_portability_budgets_order_serializer.rb → download_your_data_budgets_order_serializer.rb} +2 -2
  34. data/app/services/decidim/budgets/order_reminder_generator.rb +85 -0
  35. data/app/views/decidim/budgets/admin/budgets/index.html.erb +3 -0
  36. data/app/views/decidim/budgets/admin/projects/_bulk-actions.html.erb +13 -0
  37. data/app/views/decidim/budgets/admin/projects/_form.html.erb +6 -0
  38. data/app/views/decidim/budgets/admin/projects/_project-tr.html.erb +50 -0
  39. data/app/views/decidim/budgets/admin/projects/bulk_actions/_change-selected.html.erb +15 -0
  40. data/app/views/decidim/budgets/admin/projects/bulk_actions/_dropdown.html.erb +36 -0
  41. data/app/views/decidim/budgets/admin/projects/bulk_actions/_recategorize.html.erb +15 -0
  42. data/app/views/decidim/budgets/admin/projects/bulk_actions/_scope-change.html.erb +25 -0
  43. data/app/views/decidim/budgets/admin/projects/index.html.erb +6 -54
  44. data/app/views/decidim/budgets/admin/projects/update_attribute.js.erb +26 -0
  45. data/app/views/decidim/budgets/projects/_filters.html.erb +4 -4
  46. data/app/views/decidim/budgets/projects/index.html.erb +28 -1
  47. data/app/views/decidim/budgets/projects/show.html.erb +6 -1
  48. data/app/views/decidim/budgets/vote_reminder_mailer/vote_reminder.html.erb +21 -0
  49. data/config/assets.rb +2 -1
  50. data/config/locales/ar.yml +4 -0
  51. data/config/locales/bg.yml +1 -0
  52. data/config/locales/ca.yml +49 -3
  53. data/config/locales/cs.yml +50 -0
  54. data/config/locales/de.yml +48 -0
  55. data/config/locales/el.yml +1 -0
  56. data/config/locales/en.yml +45 -0
  57. data/config/locales/es-MX.yml +43 -0
  58. data/config/locales/es-PY.yml +46 -0
  59. data/config/locales/es.yml +46 -0
  60. data/config/locales/eu.yml +1 -0
  61. data/config/locales/fi-plain.yml +46 -0
  62. data/config/locales/fi.yml +46 -0
  63. data/config/locales/fr-CA.yml +46 -0
  64. data/config/locales/fr.yml +47 -1
  65. data/config/locales/ga-IE.yml +1 -0
  66. data/config/locales/gl.yml +10 -0
  67. data/config/locales/hu.yml +19 -0
  68. data/config/locales/id-ID.yml +1 -0
  69. data/config/locales/is-IS.yml +2 -1
  70. data/config/locales/it.yml +1 -0
  71. data/config/locales/ja.yml +44 -0
  72. data/config/locales/lb.yml +1 -0
  73. data/config/locales/lt.yml +388 -0
  74. data/config/locales/lv.yml +1 -0
  75. data/config/locales/nl.yml +74 -12
  76. data/config/locales/no.yml +16 -0
  77. data/config/locales/pl.yml +1 -0
  78. data/config/locales/pt-BR.yml +1 -0
  79. data/config/locales/pt.yml +1 -0
  80. data/config/locales/ro-RO.yml +1 -0
  81. data/config/locales/ru.yml +1 -0
  82. data/config/locales/sk.yml +1 -0
  83. data/config/locales/sr-CS.yml +1 -0
  84. data/config/locales/sv.yml +25 -0
  85. data/config/locales/tr-TR.yml +1 -0
  86. data/config/locales/uk.yml +1 -0
  87. data/config/locales/zh-CN.yml +1 -0
  88. data/db/migrate/20200804175222_votes_enabled_to_votes_choices.rb +2 -2
  89. data/db/migrate/20220428072638_add_geolocalization_fields_to_projects.rb +9 -0
  90. data/lib/decidim/budgets/admin_engine.rb +3 -0
  91. data/lib/decidim/budgets/component.rb +7 -6
  92. data/lib/decidim/budgets/engine.rb +17 -0
  93. data/lib/decidim/budgets/test/factories.rb +11 -0
  94. data/lib/decidim/budgets/version.rb +1 -1
  95. metadata +38 -18
  96. data/app/services/decidim/budgets/project_search.rb +0 -45
@@ -24,14 +24,14 @@ module Decidim
24
24
  #
25
25
  # Returns a Decidim::Scope
26
26
  def scope
27
- @scope ||= @decidim_scope_id ? current_component.scopes.find_by(id: @decidim_scope_id) : current_component.scope
27
+ @scope ||= @attributes["decidim_scope_id"].value ? current_component.scopes.find_by(id: @attributes["decidim_scope_id"].value) : current_component.scope
28
28
  end
29
29
 
30
30
  # Scope identifier
31
31
  #
32
32
  # Returns the scope identifier related to the meeting
33
33
  def decidim_scope_id
34
- @decidim_scope_id || scope&.id
34
+ super || scope&.id
35
35
  end
36
36
  end
37
37
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Budgets
5
+ module Admin
6
+ class OrderReminderForm < Decidim::Form
7
+ def reminder_amount
8
+ @reminder_amount ||= if !voting_enabled? || voting_ends_soon?
9
+ 0
10
+ else
11
+ user_ids = []
12
+ unfinished_orders.each do |order|
13
+ reminder = Decidim::Reminder.find_by(component: current_component, user: order.user)
14
+ if !reminder || (reminder.deliveries.present? && reminder.deliveries.last.created_at < minimum_interval_between_reminders.ago)
15
+ user_ids << order.user.id
16
+ end
17
+ end
18
+ user_ids.uniq.count
19
+ end
20
+ end
21
+
22
+ def voting_enabled?
23
+ current_component.current_settings.votes == "enabled"
24
+ end
25
+
26
+ def voting_ends_soon?
27
+ return false unless participatory_space.respond_to? :active_step
28
+ return false if participatory_space.active_step.blank?
29
+
30
+ time_zone = current_organization.time_zone
31
+ return false if time_zone.blank?
32
+
33
+ end_time = current_component.participatory_space.active_step[:end_date].in_time_zone(time_zone).end_of_day
34
+
35
+ 6.hours.from_now >= end_time
36
+ end
37
+
38
+ def minimum_interval_between_reminders
39
+ 24.hours
40
+ end
41
+
42
+ private
43
+
44
+ def minimum_time_before_first_reminder
45
+ @minimum_time_before_first_reminder ||= begin
46
+ reminder_manifest = Decidim.reminders_registry.for(:orders)
47
+ if reminder_manifest.blank?
48
+ minimum_interval_between_reminders
49
+ else
50
+ Array(reminder_manifest.settings.attributes[:reminder_times].default).first
51
+ end
52
+ end
53
+ end
54
+
55
+ def participatory_space
56
+ @participatory_space ||= current_component.participatory_space
57
+ end
58
+
59
+ def unfinished_orders
60
+ @unfinished_orders ||= Decidim::Budgets::Order.where(
61
+ budget: budgets,
62
+ checked_out_at: nil,
63
+ created_at: Time.zone.at(0)..minimum_time_before_first_reminder.ago
64
+ ).select do |order|
65
+ order.user.email.present?
66
+ end
67
+ end
68
+
69
+ def budgets
70
+ @budgets ||= Decidim::Budgets::Budget.where(component: current_component)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -13,6 +13,9 @@ module Decidim
13
13
  translatable_attribute :title, String
14
14
  translatable_attribute :description, String
15
15
 
16
+ attribute :address, String
17
+ attribute :latitude, Float
18
+ attribute :longitude, Float
16
19
  attribute :budget_amount, Integer
17
20
  attribute :decidim_scope_id, Integer
18
21
  attribute :decidim_category_id, Integer
@@ -25,7 +28,7 @@ module Decidim
25
28
  validates :title, translatable_presence: true
26
29
  validates :description, translatable_presence: true
27
30
  validates :budget_amount, presence: true, numericality: { greater_than: 0 }
28
-
31
+ validates :address, geocoding: true, if: ->(form) { form.has_address? && !form.geocoded? }
29
32
  validates :category, presence: true, if: ->(form) { form.decidim_category_id.present? }
30
33
  validates :scope, presence: true, if: ->(form) { form.decidim_scope_id.present? }
31
34
  validates :decidim_scope_id, scope_belongs_to_component: true, if: ->(form) { form.decidim_scope_id.present? }
@@ -33,6 +36,7 @@ module Decidim
33
36
  validate :notify_missing_attachment_if_errored
34
37
 
35
38
  delegate :categories, to: :current_component
39
+ alias component current_component
36
40
 
37
41
  def map_model(model)
38
42
  self.proposal_ids = model.linked_resources(:proposals, "included_proposals").pluck(:id)
@@ -49,6 +53,18 @@ module Decidim
49
53
  &.order(title: :asc)
50
54
  end
51
55
 
56
+ def geocoding_enabled?
57
+ Decidim::Map.available?(:geocoding) && current_component.settings.geocoding_enabled?
58
+ end
59
+
60
+ def has_address?
61
+ geocoding_enabled? && address.present?
62
+ end
63
+
64
+ def geocoded?
65
+ latitude.present? && longitude.present?
66
+ end
67
+
52
68
  # Finds the Budget from the decidim_budgets_budget_id.
53
69
  #
54
70
  # Returns a Decidim::Budgets:Budget
@@ -67,14 +83,14 @@ module Decidim
67
83
  #
68
84
  # Returns a Decidim::Scope
69
85
  def scope
70
- @scope ||= @decidim_scope_id ? current_component.scopes.find_by(id: @decidim_scope_id) : current_component.scope
86
+ @scope ||= @attributes["decidim_scope_id"].value ? current_component.scopes.find_by(id: @attributes["decidim_scope_id"].value) : current_component.scope
71
87
  end
72
88
 
73
89
  # Scope identifier
74
90
  #
75
91
  # Returns the scope identifier related to the project
76
92
  def decidim_scope_id
77
- @decidim_scope_id || scope&.id
93
+ super || scope&.id
78
94
  end
79
95
 
80
96
  private
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Budgets
5
+ module Admin
6
+ module ProjectBulkActionsHelper
7
+ def bulk_selections
8
+ select(
9
+ :selected,
10
+ :value,
11
+ [
12
+ [t("projects.index.select_for_implementation", scope: "decidim.budgets.admin"), true],
13
+ [t("projects.index.deselect_implementation", scope: "decidim.budgets.admin"), false]
14
+ ]
15
+ )
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -4,6 +4,9 @@ module Decidim
4
4
  module Budgets
5
5
  # A helper to render order and budgets actions
6
6
  module ProjectsHelper
7
+ include Decidim::ApplicationHelper
8
+ include Decidim::MapHelper
9
+
7
10
  # Render a budget as a currency
8
11
  #
9
12
  # budget - A integer to represent a budget
@@ -80,6 +83,32 @@ module Decidim
80
83
  t(".vote_threshold_percent_rule.description", minimum_budget: budget_to_currency(current_order.minimum_budget))
81
84
  end
82
85
  end
86
+
87
+ # Serialize a collection of geocoded projects to be used by the dynamic map component
88
+ #
89
+ # geocoded_projects - A collection of geocoded projects
90
+ def projects_data_for_map(geocoded_projects)
91
+ geocoded_projects.map do |project|
92
+ project_data_for_map(project)
93
+ end
94
+ end
95
+
96
+ def project_data_for_map(project)
97
+ project
98
+ .slice(:latitude, :longitude, :address)
99
+ .merge(
100
+ title: decidim_html_escape(translated_attribute(project.title)),
101
+ description: html_truncate(decidim_sanitize_editor(translated_attribute(project.description)), length: 100),
102
+ icon: icon("project", width: 40, height: 70, remove_icon_class: true),
103
+ link: ::Decidim::ResourceLocatorPresenter.new([project.budget, project]).path
104
+ )
105
+ end
106
+
107
+ def has_position?(project)
108
+ return if project.address.blank?
109
+
110
+ project.latitude.present? && project.longitude.present?
111
+ end
83
112
  end
84
113
  end
85
114
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Budgets
5
+ class SendVoteReminderJob < ApplicationJob
6
+ queue_as :vote_reminder
7
+
8
+ def perform(reminder)
9
+ return if reminder.records.active.blank?
10
+
11
+ ::Decidim::ReminderDelivery.create(reminder: reminder)
12
+ ::Decidim::Budgets::VoteReminderMailer.vote_reminder(reminder).deliver_now
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Budgets
5
+ class VoteReminderMailer < Decidim::ApplicationMailer
6
+ include Decidim::TranslationsHelper
7
+ include Decidim::SanitizeHelper
8
+
9
+ helper Decidim::TranslationsHelper
10
+
11
+ helper_method :routes
12
+
13
+ # Send the user an email reminder to finish voting
14
+ #
15
+ # reminder - the reminder to send.
16
+ def vote_reminder(reminder)
17
+ @reminder = reminder
18
+ @user = reminder.user
19
+ with_user(@user) do
20
+ @orders = reminder.records.active.map(&:remindable)
21
+ @organization = @user.organization
22
+
23
+ subject = I18n.t(
24
+ "decidim.budgets.vote_reminder_mailer.vote_reminder.email_subject",
25
+ count: @orders.count
26
+ )
27
+
28
+ mail(to: @user.email, subject: subject)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def routes
35
+ @routes ||= Decidim::EngineRouter.main_proxy(@reminder.component)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -5,7 +5,7 @@ module Decidim
5
5
  # The data store for a Order in the Decidim::Budgets component. It is unique for each
6
6
  # user and component and contains a collection of projects
7
7
  class Order < Budgets::ApplicationRecord
8
- include Decidim::DataPortability
8
+ include Decidim::DownloadYourData
9
9
  include Decidim::NewsletterParticipant
10
10
 
11
11
  belongs_to :user, class_name: "Decidim::User", foreign_key: "decidim_user_id"
@@ -15,7 +15,6 @@ module Decidim
15
15
  has_many :projects, through: :line_items, class_name: "Decidim::Budgets::Project", foreign_key: "decidim_project_id"
16
16
 
17
17
  validates :user, uniqueness: { scope: :budget }
18
- validates :budget, presence: true
19
18
  validate :user_belongs_to_organization
20
19
 
21
20
  # Rules active for the budget threshold and minimum budgets rules.
@@ -171,7 +170,7 @@ module Decidim
171
170
  end
172
171
 
173
172
  def self.export_serializer
174
- Decidim::Budgets::DataPortabilityBudgetsOrderSerializer
173
+ Decidim::Budgets::DownloadYourDataBudgetsOrderSerializer
175
174
  end
176
175
 
177
176
  def self.newsletter_participant_ids(component)
@@ -18,6 +18,7 @@ module Decidim
18
18
  include Decidim::Randomable
19
19
  include Decidim::Searchable
20
20
  include Decidim::TranslatableResource
21
+ include Decidim::FilterableResource
21
22
 
22
23
  translatable_fields :title, :description
23
24
 
@@ -33,6 +34,10 @@ module Decidim
33
34
  scope :selected, -> { where.not(selected_at: nil) }
34
35
  scope :not_selected, -> { where(selected_at: nil) }
35
36
 
37
+ geocoded_by :address
38
+
39
+ scope_search_multi :with_any_status, [:selected, :not_selected]
40
+
36
41
  searchable_fields(
37
42
  scope_id: :decidim_scope_id,
38
43
  participatory_space: { component: :participatory_space },
@@ -98,10 +103,9 @@ module Decidim
98
103
  Arel.sql(%{cast("decidim_budgets_projects"."id" as text)})
99
104
  end
100
105
 
101
- # Allow ransacker to search for a key in a hstore column (`title`.`en`)
102
- ransacker :title do |parent|
103
- Arel::Nodes::InfixOperation.new("->>", parent.table[:title], Arel::Nodes.build_quoted(I18n.locale.to_s))
104
- end
106
+ # Create i18n ransackers for :title and :description.
107
+ # Create the :search_text ransacker alias for searching from both of these.
108
+ ransacker_i18n_multi :search_text, [:title, :description]
105
109
 
106
110
  ransacker :selected do
107
111
  Arel.sql(%{("decidim_budgets_projects"."selected_at")::text})
@@ -119,6 +123,10 @@ module Decidim
119
123
  SQL
120
124
  Arel.sql(query)
121
125
  end
126
+
127
+ def self.ransackable_scopes(_auth_object = nil)
128
+ [:with_any_status, :with_any_scope, :with_any_category]
129
+ end
122
130
  end
123
131
  end
124
132
  end
@@ -0,0 +1 @@
1
+ import "src/decidim/budgets/admin/projects"
@@ -0,0 +1,143 @@
1
+ /* eslint-disable no-invalid-this */
2
+ $(() => {
3
+ const selectedResourcesCount = () => {
4
+ return $(".table-list .js-check-all-resources:checked").length
5
+ }
6
+
7
+ const selectedResourcesNotPublishedAnswerCount = () => {
8
+ return $(".table-list [data-published-state=false] .js-check-all-resources:checked").length
9
+ }
10
+
11
+ const selectedResourcesCountUpdate = () => {
12
+ const selectedResources = selectedResourcesCount();
13
+ const selectedResourcesNotPublishedAnswer = selectedResourcesNotPublishedAnswerCount();
14
+
15
+ if (selectedResources === 0) {
16
+ $("#js-selected-resources-count").text("")
17
+ } else {
18
+ $("#js-selected-resources-count").text(selectedResources);
19
+ }
20
+
21
+ if (selectedResources >= 2) {
22
+ $('button[data-action="merge-resources"]').parent().show();
23
+ } else {
24
+ $('button[data-action="merge-resources"]').parent().hide();
25
+ }
26
+
27
+ if (selectedResourcesNotPublishedAnswer > 0) {
28
+ $('button[data-action="publish-answers"]').parent().show();
29
+ $("#js-form-publish-answers-number").text(selectedResourcesNotPublishedAnswer);
30
+ } else {
31
+ $('button[data-action="publish-answers"]').parent().hide();
32
+ }
33
+ }
34
+
35
+ const showBulkActionsButton = () => {
36
+ if (selectedResourcesCount() > 0) {
37
+ $("#js-bulk-actions-button").removeClass("hide");
38
+ }
39
+ }
40
+
41
+ const hideBulkActionsButton = (force = false) => {
42
+ if (selectedResourcesCount() === 0 || force === true) {
43
+ $("#js-bulk-actions-button").addClass("hide");
44
+ $("#js-bulk-actions-dropdown").removeClass("is-open");
45
+ }
46
+ }
47
+
48
+ const showOtherActionsButtons = () => {
49
+ $("#js-other-actions-wrapper").removeClass("hide");
50
+ }
51
+
52
+ const hideOtherActionsButtons = () => {
53
+ $("#js-other-actions-wrapper").addClass("hide");
54
+ }
55
+
56
+ const hideBulkActionForms = () => {
57
+ $(".js-bulk-action-form").addClass("hide");
58
+ }
59
+
60
+ if ($("#js-bulk-actions-wrapper").length === 0) {
61
+ return;
62
+ }
63
+
64
+ // Expose functions to make them available in .js.erb templates
65
+ window.hideBulkActionForms = hideBulkActionForms;
66
+ window.hideBulkActionsButton = hideBulkActionsButton;
67
+ window.showOtherActionsButtons = showOtherActionsButtons;
68
+ window.selectedResourcesCountUpdate = selectedResourcesCountUpdate;
69
+
70
+
71
+ if ($(".js-bulk-action-form").length) {
72
+ hideBulkActionForms();
73
+ $("#js-bulk-actions-button").addClass("hide");
74
+
75
+ $("#js-bulk-actions-dropdown ul li button").on("click", (event) => {
76
+ event.preventDefault();
77
+ let action = $(event.target).data("action");
78
+
79
+ if (action) {
80
+ $(`#js-form-${action}`).on("submit", () => {
81
+ $(".layout-content > .callout-wrapper").html("");
82
+ })
83
+
84
+ $(`#js-${action}-actions`).removeClass("hide");
85
+ hideBulkActionsButton(true);
86
+ hideOtherActionsButtons();
87
+ }
88
+ })
89
+
90
+ // select all checkboxes
91
+ $(".js-check-all").on("change", function() {
92
+ $(".js-check-all-resources").prop("checked", $(this).prop("checked"));
93
+
94
+ if ($(this).prop("checked")) {
95
+ $(".js-check-all-resources").closest("tr").addClass("selected");
96
+ showBulkActionsButton();
97
+ } else {
98
+ $(".js-check-all-resources").closest("tr").removeClass("selected");
99
+ hideBulkActionsButton();
100
+ }
101
+
102
+ selectedResourcesCountUpdate();
103
+ });
104
+
105
+ // resource checkbox change
106
+ $(".table-list").on("change", ".js-check-all-resources", function() {
107
+ let resourceId = $(this).val()
108
+ let checked = $(this).prop("checked")
109
+
110
+ // uncheck "select all", if one of the listed checkbox item is unchecked
111
+ if ($(this).prop("checked") === false) {
112
+ $(".js-check-all").prop("checked", false);
113
+ }
114
+ // check "select all" if all checkbox resources are checked
115
+ if ($(".js-check-all-resources:checked").length === $(".js-check-all-resources").length) {
116
+ $(".js-check-all").prop("checked", true);
117
+ showBulkActionsButton();
118
+ }
119
+
120
+ if ($(this).prop("checked")) {
121
+ showBulkActionsButton();
122
+ $(this).closest("tr").addClass("selected");
123
+ } else {
124
+ hideBulkActionsButton();
125
+ $(this).closest("tr").removeClass("selected");
126
+ }
127
+
128
+ if ($(".js-check-all-resources:checked").length === 0) {
129
+ hideBulkActionsButton();
130
+ }
131
+
132
+ $(".js-bulk-action-form").find(`.js-resource-id-${resourceId}`).prop("checked", checked);
133
+ selectedResourcesCountUpdate();
134
+ });
135
+
136
+ $(".js-cancel-bulk-action").on("click", () => {
137
+ hideBulkActionForms()
138
+ showBulkActionsButton();
139
+ showOtherActionsButtons();
140
+ });
141
+ }
142
+ });
143
+ /* eslint-enable no-invalid-this */
@@ -19,13 +19,18 @@ module Decidim
19
19
  end
20
20
  when :project, :projects
21
21
  case permission_action.action
22
- when :create
23
- permission_action.allow!
24
- when :import_proposals
22
+ when :create, :import_proposals, :project_category
25
23
  permission_action.allow!
26
24
  when :update, :destroy
27
25
  permission_action.allow! if project.present?
28
26
  end
27
+ when :order
28
+ case permission_action.action
29
+ when :remind
30
+ permission_action.allow!
31
+ end
32
+ when :project_category, :project_scope, :project_selected
33
+ permission_action.allow!
29
34
  end
30
35
 
31
36
  permission_action
@@ -3,7 +3,7 @@
3
3
  module Decidim
4
4
  module Budgets
5
5
  # A class used to find projects filtered by components and a date range
6
- class FilteredProjects < Rectify::Query
6
+ class FilteredProjects < Decidim::Query
7
7
  # Syntactic sugar to initialize the class and return the queried objects.
8
8
  #
9
9
  # components - An array of Decidim::Component
@@ -2,13 +2,13 @@
2
2
 
3
3
  module Decidim
4
4
  module Budgets
5
- class DataPortabilityBudgetsOrderSerializer < Decidim::Exporters::Serializer
5
+ class DownloadYourDataBudgetsOrderSerializer < Decidim::Exporters::Serializer
6
6
  # Public: Initializes the serializer with a conversation.
7
7
  def initialize(order)
8
8
  @order = order
9
9
  end
10
10
 
11
- # Serializes a Debate for data portability
11
+ # Serializes a Debate for download your data
12
12
  def serialize
13
13
  {
14
14
  id: order.id,
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Budgets
5
+ # This class is the generator class which creates and updates order related reminders,
6
+ # after reminder is generated it is send to user who have not checked out his/her/their vote.
7
+ class OrderReminderGenerator
8
+ attr_reader :reminder_jobs_queued
9
+
10
+ def initialize
11
+ @reminder_manifest = Decidim.reminders_registry.for(:orders)
12
+ @reminder_jobs_queued = 0
13
+ end
14
+
15
+ # Creates reminders and updates them if they already exists.
16
+ def generate
17
+ Decidim::Component.where(manifest_name: "budgets").each do |component|
18
+ next if component.current_settings.votes != "enabled"
19
+
20
+ send_reminders(component)
21
+ end
22
+ end
23
+
24
+ def generate_for(component, &block)
25
+ @alternative_refresh_state = block
26
+ send_reminders(component)
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :reminder_manifest
32
+
33
+ def send_reminders(component)
34
+ budgets = Decidim::Budgets::Budget.where(component: component)
35
+ pending_orders = Decidim::Budgets::Order.where(budget: budgets, checked_out_at: nil)
36
+ users = Decidim::User.where(id: pending_orders.pluck(:decidim_user_id).uniq)
37
+ users.each do |user|
38
+ reminder = Decidim::Reminder.find_or_create_by(user: user, component: component)
39
+ users_pending_orders = pending_orders.where(user: user)
40
+ update_reminder_records(reminder, users_pending_orders)
41
+ if reminder.records.active.any?
42
+ Decidim::Budgets::SendVoteReminderJob.perform_later(reminder)
43
+ @reminder_jobs_queued += 1
44
+ end
45
+ end
46
+ end
47
+
48
+ def update_reminder_records(reminder, users_pending_orders)
49
+ clean_checked_out_and_deleted_orders(reminder)
50
+ add_pending_orders(reminder, users_pending_orders)
51
+ end
52
+
53
+ def clean_checked_out_and_deleted_orders(reminder)
54
+ reminder.records.each do |record|
55
+ if record.remindable.nil?
56
+ record.update(state: "deleted")
57
+ elsif record.remindable.checked_out_at.present?
58
+ record.update(state: "completed")
59
+ end
60
+ end
61
+ end
62
+
63
+ def add_pending_orders(reminder, users_pending_orders)
64
+ reminder.records << users_pending_orders.map { |order| Decidim::ReminderRecord.find_or_create_by(reminder: reminder, remindable: order) }
65
+ return @alternative_refresh_state.call(reminder) if @alternative_refresh_state.present?
66
+
67
+ reminder.records.each do |record|
68
+ refresh_state(record, reminder.deliveries.length) if %w(active pending).include? record.state
69
+ end
70
+ end
71
+
72
+ def refresh_state(record, delivered_count)
73
+ intervals = Array(reminder_manifest.settings.attributes[:reminder_times].default)
74
+ return record.update(state: "pending") if delivered_count >= intervals.length
75
+
76
+ record.state = intervals[delivered_count].ago > record.remindable.created_at ? "active" : "pending"
77
+ record.save if record.changed?
78
+ end
79
+
80
+ def voting_enabled?(component)
81
+ component.current_settings.votes == "enabled"
82
+ end
83
+ end
84
+ end
85
+ end
@@ -5,6 +5,9 @@
5
5
  <%= t(".title") %>
6
6
  </div>
7
7
  <div class="flex--cc flex-gap--1">
8
+ <% if allowed_to? :remind, :order %>
9
+ <%= link_to "Send voting reminders", admin_reminders_path(current_component, name: "orders"), class: "button tiny button--title" %>
10
+ <% end %>
8
11
  <% if allowed_to? :export, :budget %>
9
12
  <%= export_dropdown %>
10
13
  <% end %>
@@ -0,0 +1,13 @@
1
+ <div class="flex--cc flex-gap--1">
2
+ <%= render partial: "decidim/budgets/admin/projects/bulk_actions/dropdown" %>
3
+
4
+ <%= render partial: "decidim/budgets/admin/projects/bulk_actions/recategorize" %>
5
+ <%= render partial: "decidim/budgets/admin/projects/bulk_actions/scope-change" %>
6
+ <%= render partial: "decidim/budgets/admin/projects/bulk_actions/change-selected" %>
7
+
8
+ <%= 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 %>
9
+ <% if allowed_to? :export, :budget %>
10
+ <%= export_dropdown(current_component, budget.id) %>
11
+ <% end %>
12
+ <%= 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 %>
13
+ </div>
@@ -18,6 +18,12 @@
18
18
  <%= form.number_field :budget_amount %>
19
19
  </div>
20
20
 
21
+ <% if @form.geocoding_enabled? %>
22
+ <div class="row column">
23
+ <%= form.geocoding_field :address %>
24
+ </div>
25
+ <% end %>
26
+
21
27
  <% if current_component.has_subscopes? %>
22
28
  <div class="row column">
23
29
  <%= scopes_picker_field form, :decidim_scope_id, root: budget.scope %>