decidim-proposals 0.20.1 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/decidim/proposals/admin/proposals.es6 +24 -11
  3. data/app/cells/decidim/proposals/cost_report/show.erb +35 -0
  4. data/app/cells/decidim/proposals/cost_report_cell.rb +42 -0
  5. data/app/cells/decidim/proposals/proposal_m_cell.rb +9 -1
  6. data/app/cells/decidim/proposals/proposal_tags/show.erb +12 -10
  7. data/app/cells/decidim/proposals/proposal_tags_cell.rb +5 -0
  8. data/app/commands/decidim/proposals/admin/answer_proposal.rb +24 -46
  9. data/app/commands/decidim/proposals/admin/assign_proposals_to_valuator.rb +61 -0
  10. data/app/commands/decidim/proposals/admin/create_proposal.rb +5 -0
  11. data/app/commands/decidim/proposals/admin/notify_proposal_answer.rb +85 -0
  12. data/app/commands/decidim/proposals/admin/publish_answers.rb +67 -0
  13. data/app/commands/decidim/proposals/admin/unassign_proposals_from_valuator.rb +62 -0
  14. data/app/commands/decidim/proposals/admin/update_proposal_scope.rb +75 -0
  15. data/app/controllers/concerns/decidim/proposals/admin/filterable.rb +82 -0
  16. data/app/controllers/decidim/proposals/admin/proposal_answers_controller.rb +16 -6
  17. data/app/controllers/decidim/proposals/admin/proposal_notes_controller.rb +8 -9
  18. data/app/controllers/decidim/proposals/admin/proposals_controller.rb +105 -29
  19. data/app/controllers/decidim/proposals/admin/valuation_assignments_controller.rb +58 -0
  20. data/app/controllers/decidim/proposals/collaborative_drafts_controller.rb +19 -3
  21. data/app/controllers/decidim/proposals/proposals_controller.rb +42 -7
  22. data/app/controllers/decidim/proposals/versions_controller.rb +4 -1
  23. data/app/events/decidim/proposals/admin/update_proposal_scope_event.rb +11 -0
  24. data/app/forms/decidim/proposals/admin/proposal_answer_form.rb +27 -2
  25. data/app/forms/decidim/proposals/admin/valuation_assignment_form.rb +37 -0
  26. data/app/forms/decidim/proposals/proposal_wizard_create_step_form.rb +8 -0
  27. data/app/helpers/decidim/proposals/admin/filterable_helper.rb +17 -0
  28. data/app/helpers/decidim/proposals/admin/proposal_bulk_actions_helper.rb +35 -0
  29. data/app/helpers/decidim/proposals/admin/proposal_rankings_helper.rb +63 -0
  30. data/app/helpers/decidim/proposals/admin/proposals_helper.rb +122 -0
  31. data/app/helpers/decidim/proposals/application_helper.rb +36 -25
  32. data/app/helpers/decidim/proposals/collaborative_draft_helper.rb +9 -9
  33. data/app/helpers/decidim/proposals/proposal_cells_helper.rb +1 -1
  34. data/app/helpers/decidim/proposals/proposals_helper.rb +18 -0
  35. data/app/models/decidim/proposals/proposal.rb +163 -16
  36. data/app/models/decidim/proposals/valuation_assignment.rb +24 -0
  37. data/app/permissions/decidim/proposals/admin/permissions.rb +77 -11
  38. data/app/presenters/decidim/proposals/admin_log/proposal_presenter.rb +1 -1
  39. data/app/presenters/decidim/proposals/admin_log/valuation_assignment_presenter.rb +51 -0
  40. data/app/presenters/decidim/proposals/admin_log/value_types/valuator_role_user_presenter.rb +19 -0
  41. data/app/presenters/decidim/proposals/collaborative_draft_presenter.rb +2 -28
  42. data/app/presenters/decidim/proposals/log/valuation_assignment_presenter.rb +22 -0
  43. data/app/presenters/decidim/proposals/proposal_presenter.rb +26 -1
  44. data/app/services/decidim/proposals/collaborative_draft_search.rb +18 -10
  45. data/app/services/decidim/proposals/proposal_search.rb +33 -40
  46. data/app/types/decidim/proposals/proposal_input_filter.rb +29 -0
  47. data/app/types/decidim/proposals/proposal_input_sort.rb +28 -0
  48. data/app/types/decidim/proposals/proposal_type.rb +35 -4
  49. data/app/types/decidim/proposals/proposals_type.rb +14 -17
  50. data/app/views/decidim/proposals/admin/proposal_answers/_form.html.erb +35 -0
  51. data/app/views/decidim/proposals/admin/proposal_notes/_form.html.erb +1 -1
  52. data/app/views/decidim/proposals/admin/proposal_notes/_proposal_notes.html.erb +1 -1
  53. data/app/views/decidim/proposals/admin/proposals/_bulk-actions.html.erb +8 -2
  54. data/app/views/decidim/proposals/admin/proposals/_form.html.erb +1 -1
  55. data/app/views/decidim/proposals/admin/proposals/_proposal-tr.html.erb +25 -17
  56. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_assign_to_valuator.html.erb +15 -0
  57. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_dropdown.html.erb +21 -1
  58. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_publish_answers.html.erb +14 -0
  59. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_scope-change.html.erb +25 -0
  60. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_unassign_from_valuator.html.erb +15 -0
  61. data/app/views/decidim/proposals/admin/proposals/index.html.erb +16 -7
  62. data/app/views/decidim/proposals/admin/proposals/publish_answers.js.erb +12 -0
  63. data/app/views/decidim/proposals/admin/proposals/show.html.erb +186 -0
  64. data/app/views/decidim/proposals/admin/proposals/update_category.js.erb +3 -2
  65. data/app/views/decidim/proposals/admin/proposals/update_scope.js.erb +27 -0
  66. data/app/views/decidim/proposals/collaborative_drafts/_filters.html.erb +3 -3
  67. data/app/views/decidim/proposals/proposals/_edit_form_fields.html.erb +1 -1
  68. data/app/views/decidim/proposals/proposals/_filters.html.erb +12 -12
  69. data/app/views/decidim/proposals/proposals/_proposal_badge.html.erb +1 -4
  70. data/app/views/decidim/proposals/proposals/_proposal_preview.html.erb +1 -1
  71. data/app/views/decidim/proposals/proposals/_vote_button.html.erb +1 -1
  72. data/app/views/decidim/proposals/proposals/index.html.erb +1 -1
  73. data/app/views/decidim/proposals/proposals/new.html.erb +1 -1
  74. data/app/views/decidim/proposals/proposals/show.html.erb +17 -23
  75. data/config/locales/ar.yml +69 -17
  76. data/config/locales/ca.yml +113 -18
  77. data/config/locales/cs.yml +123 -31
  78. data/config/locales/de.yml +38 -18
  79. data/config/locales/el.yml +1 -0
  80. data/config/locales/en.yml +112 -17
  81. data/config/locales/es-MX.yml +112 -17
  82. data/config/locales/es-PY.yml +112 -17
  83. data/config/locales/es.yml +113 -18
  84. data/config/locales/eu.yml +38 -18
  85. data/config/locales/fi-plain.yml +113 -18
  86. data/config/locales/fi.yml +113 -18
  87. data/config/locales/fr.yml +38 -18
  88. data/config/locales/gl.yml +38 -18
  89. data/config/locales/hu.yml +112 -17
  90. data/config/locales/id-ID.yml +38 -18
  91. data/config/locales/is-IS.yml +25 -15
  92. data/config/locales/it.yml +38 -18
  93. data/config/locales/nl.yml +43 -18
  94. data/config/locales/no.yml +66 -18
  95. data/config/locales/pl.yml +38 -18
  96. data/config/locales/pt-BR.yml +39 -19
  97. data/config/locales/pt.yml +39 -19
  98. data/config/locales/ru.yml +25 -17
  99. data/config/locales/sv.yml +39 -18
  100. data/config/locales/tr-TR.yml +38 -18
  101. data/config/locales/uk.yml +25 -17
  102. data/db/migrate/20200203111239_add_proposal_valuation_assignments.rb +12 -0
  103. data/db/migrate/20200210135152_add_costs_to_proposals.rb +9 -0
  104. data/db/migrate/20200212120110_sync_proposals_state_with_amendments_state.rb +28 -0
  105. data/db/migrate/20200227175922_add_state_published_at_to_proposals.rb +7 -0
  106. data/db/migrate/20200306123652_publish_existing_proposals_state.rb +15 -0
  107. data/lib/decidim/proposals.rb +1 -0
  108. data/lib/decidim/proposals/admin_engine.rb +7 -3
  109. data/lib/decidim/proposals/component.rb +39 -19
  110. data/lib/decidim/proposals/engine.rb +1 -1
  111. data/lib/decidim/proposals/test/factories.rb +55 -0
  112. data/lib/decidim/proposals/valuatable.rb +21 -0
  113. data/lib/decidim/proposals/version.rb +1 -1
  114. metadata +53 -36
  115. data/app/views/decidim/proposals/admin/proposal_answers/edit.html.erb +0 -22
  116. data/app/views/decidim/proposals/admin/proposal_notes/index.html.erb +0 -3
  117. data/app/views/decidim/proposals/admin/shared/_info_proposal.html.erb +0 -20
  118. data/app/views/decidim/proposals/proposal_widgets/show.html.erb +0 -4
@@ -13,6 +13,8 @@ module Decidim
13
13
  include Decidim::Proposals::MapHelper
14
14
  include CollaborativeDraftHelper
15
15
  include ControlVersionHelper
16
+ include Decidim::RichTextEditorHelper
17
+ include Decidim::CheckBoxesTreeHelper
16
18
 
17
19
  delegate :minimum_votes_per_user, to: :component_settings
18
20
 
@@ -78,6 +80,40 @@ module Decidim
78
80
  minimum_votes_per_user.positive?
79
81
  end
80
82
 
83
+ def not_from_collaborative_draft(proposal)
84
+ proposal.linked_resources(:proposals, "created_from_collaborative_draft").empty?
85
+ end
86
+
87
+ def not_from_participatory_text(proposal)
88
+ proposal.participatory_text_level.nil?
89
+ end
90
+
91
+ # If the proposal is official or the rich text editor is enabled on the
92
+ # frontend, the proposal body is considered as safe content; that's unless
93
+ # the proposal comes from a collaborative_draft or a participatory_text.
94
+ def safe_content?
95
+ rich_text_editor_in_public_views? && not_from_collaborative_draft(@proposal) ||
96
+ (@proposal.official? || @proposal.official_meeting?) && not_from_participatory_text(@proposal)
97
+ end
98
+
99
+ # If the content is safe, HTML tags are sanitized, otherwise, they are stripped.
100
+ def render_proposal_body(proposal)
101
+ body = present(proposal).body(links: true, strip_tags: !safe_content?)
102
+
103
+ safe_content? ? decidim_sanitize(body) : simple_format(body, {}, sanitize: false)
104
+ end
105
+
106
+ # Returns :text_area or :editor based on the organization' settings.
107
+ def text_editor_for_proposal_body(form)
108
+ options = {
109
+ class: "js-hashtags",
110
+ hashtaggable: true,
111
+ value: form_presenter.body(extras: false).strip
112
+ }
113
+
114
+ text_editor_for(form, :body, options)
115
+ end
116
+
81
117
  def proposal_limit
82
118
  return if component_settings.proposal_limit.zero?
83
119
 
@@ -125,31 +161,6 @@ module Decidim
125
161
  return true if minimum_votes_per_user_enabled?
126
162
  end
127
163
 
128
- def filter_origin_values
129
- base = if component_settings.official_proposals_enabled
130
- [
131
- ["all", t("decidim.proposals.application_helper.filter_origin_values.all")],
132
- ["official", t("decidim.proposals.application_helper.filter_origin_values.official")]
133
- ]
134
- else
135
- [["all", t("decidim.proposals.application_helper.filter_origin_values.all")]]
136
- end
137
-
138
- base += [["citizens", t("decidim.proposals.application_helper.filter_origin_values.citizens")]]
139
- base += [["user_group", t("decidim.proposals.application_helper.filter_origin_values.user_groups")]] if current_organization.user_groups_enabled?
140
- base + [["meeting", t("decidim.proposals.application_helper.filter_origin_values.meetings")]]
141
- end
142
-
143
- def filter_state_values
144
- [
145
- ["except_rejected", t("decidim.proposals.application_helper.filter_state_values.except_rejected")],
146
- ["accepted", t("decidim.proposals.application_helper.filter_state_values.accepted")],
147
- ["evaluating", t("decidim.proposals.application_helper.filter_state_values.evaluating")],
148
- ["rejected", t("decidim.proposals.application_helper.filter_state_values.rejected")],
149
- ["all", t("decidim.proposals.application_helper.filter_state_values.all")]
150
- ]
151
- end
152
-
153
164
  def filter_type_values
154
165
  [
155
166
  ["all", t("decidim.proposals.application_helper.filter_type_values.all")],
@@ -5,16 +5,16 @@ module Decidim
5
5
  # Custom helpers, scoped to the collaborative_draft resource.
6
6
  #
7
7
  module CollaborativeDraftHelper
8
- def collaborative_drafts_states_collection
8
+ def filter_collaborative_drafts_state_values
9
9
  scope = "decidim.proposals.collaborative_drafts.filters"
10
- @collaborative_drafts_states_collection ||= begin
11
- collection = []
12
- collection << ["all", t("all", scope: scope)]
13
- collection << ["open", t("open", scope: scope)]
14
- collection << ["withdrawn", t("withdrawn", scope: scope)]
15
- collection << ["published", t("published", scope: scope)]
16
- collection
17
- end
10
+ Decidim::CheckBoxesTreeHelper::TreeNode.new(
11
+ Decidim::CheckBoxesTreeHelper::TreePoint.new("", t("all", scope: scope)),
12
+ [
13
+ Decidim::CheckBoxesTreeHelper::TreePoint.new("open", t("open", scope: scope)),
14
+ Decidim::CheckBoxesTreeHelper::TreePoint.new("withdrawn", t("withdrawn", scope: scope)),
15
+ Decidim::CheckBoxesTreeHelper::TreePoint.new("published", t("published", scope: scope))
16
+ ]
17
+ )
18
18
  end
19
19
 
20
20
  def accept_request_button_label
@@ -14,7 +14,7 @@ module Decidim
14
14
  include Decidim::TranslatableAttributes
15
15
  include Decidim::CardHelper
16
16
 
17
- delegate :title, :state, :answered?, :withdrawn?, :amendable?, :emendation?, to: :model
17
+ delegate :title, :state, :published_state?, :withdrawn?, :amendable?, :emendation?, to: :model
18
18
 
19
19
  def has_actions?
20
20
  return context[:has_actions] if context[:has_actions].present?
@@ -37,6 +37,24 @@ module Decidim
37
37
 
38
38
  t(i18n_key, scope: "decidim.proposals.proposals.show")
39
39
  end
40
+
41
+ def filter_proposals_state_values
42
+ Decidim::CheckBoxesTreeHelper::TreeNode.new(
43
+ Decidim::CheckBoxesTreeHelper::TreePoint.new("", t("decidim.proposals.application_helper.filter_state_values.all")),
44
+ [
45
+ Decidim::CheckBoxesTreeHelper::TreePoint.new("accepted", t("decidim.proposals.application_helper.filter_state_values.accepted")),
46
+ Decidim::CheckBoxesTreeHelper::TreePoint.new("evaluating", t("decidim.proposals.application_helper.filter_state_values.evaluating")),
47
+ Decidim::CheckBoxesTreeHelper::TreePoint.new("not_answered", t("decidim.proposals.application_helper.filter_state_values.not_answered")),
48
+ Decidim::CheckBoxesTreeHelper::TreePoint.new("rejected", t("decidim.proposals.application_helper.filter_state_values.rejected"))
49
+ ]
50
+ )
51
+ end
52
+
53
+ def proposal_has_costs?
54
+ @proposal.cost.present? &&
55
+ translated_attribute(@proposal.cost_report).present? &&
56
+ translated_attribute(@proposal.execution_period).present?
57
+ end
40
58
  end
41
59
  end
42
60
  end
@@ -24,6 +24,9 @@ module Decidim
24
24
  include Decidim::Amendable
25
25
  include Decidim::NewsletterParticipant
26
26
  include Decidim::Randomable
27
+ include Decidim::Proposals::Valuatable
28
+
29
+ POSSIBLE_STATES = %w(not_answered evaluating accepted rejected withdrawn).freeze
27
30
 
28
31
  fingerprint fields: [:title, :body]
29
32
 
@@ -49,15 +52,56 @@ module Decidim
49
52
 
50
53
  geocoded_by :address, http_headers: ->(proposal) { { "Referer" => proposal.component.organization.host } }
51
54
 
52
- scope :accepted, -> { where(state: "accepted") }
53
- scope :rejected, -> { where(state: "rejected") }
54
- scope :evaluating, -> { where(state: "evaluating") }
55
+ scope :answered, -> { where.not(answered_at: nil) }
56
+ scope :not_answered, -> { where(answered_at: nil) }
57
+
58
+ scope :state_not_published, -> { where(state_published_at: nil) }
59
+ scope :state_published, -> { where.not(state_published_at: nil).where.not(state: nil) }
60
+
61
+ scope :accepted, -> { state_published.where(state: "accepted") }
62
+ scope :rejected, -> { state_published.where(state: "rejected") }
63
+ scope :evaluating, -> { state_published.where(state: "evaluating") }
55
64
  scope :withdrawn, -> { where(state: "withdrawn") }
56
- scope :except_rejected, -> { where.not(state: "rejected").or(where(state: nil)) }
65
+ scope :except_rejected, -> { where.not(state: "rejected").or(state_not_published) }
57
66
  scope :except_withdrawn, -> { where.not(state: "withdrawn").or(where(state: nil)) }
58
67
  scope :drafts, -> { where(published_at: nil) }
59
68
  scope :except_drafts, -> { where.not(published_at: nil) }
60
69
  scope :published, -> { where.not(published_at: nil) }
70
+ scope :official_origin, lambda {
71
+ where.not(coauthorships_count: 0)
72
+ .joins(:coauthorships)
73
+ .where(decidim_coauthorships: { decidim_author_type: "Decidim::Organization" })
74
+ }
75
+ scope :citizens_origin, lambda {
76
+ where.not(coauthorships_count: 0)
77
+ .joins(:coauthorships)
78
+ .where.not(decidim_coauthorships: { decidim_author_type: "Decidim::Organization" })
79
+ }
80
+ scope :user_group_origin, lambda {
81
+ where.not(coauthorships_count: 0)
82
+ .joins(:coauthorships)
83
+ .where(decidim_coauthorships: { decidim_author_type: "Decidim::UserBaseEntity" })
84
+ .where.not(decidim_coauthorships: { decidim_user_group_id: nil })
85
+ }
86
+ scope :meeting_origin, lambda {
87
+ where.not(coauthorships_count: 0)
88
+ .joins(:coauthorships)
89
+ .where(decidim_coauthorships: { decidim_author_type: "Decidim::Meetings::Meeting" })
90
+ }
91
+ scope :sort_by_valuation_assignments_count_asc, lambda {
92
+ order(sort_by_valuation_assignments_count_nulls_last_query + "ASC NULLS FIRST")
93
+ }
94
+
95
+ scope :sort_by_valuation_assignments_count_desc, lambda {
96
+ order(sort_by_valuation_assignments_count_nulls_last_query + "DESC NULLS LAST")
97
+ }
98
+
99
+ def self.with_valuation_assigned_to(user, space)
100
+ valuator_roles = space.user_roles(:valuator).where(user: user)
101
+
102
+ includes(:valuation_assignments)
103
+ .where(decidim_proposals_valuation_assignments: { valuator_role_id: valuator_roles })
104
+ end
61
105
 
62
106
  acts_as_list scope: :decidim_component_id
63
107
 
@@ -140,39 +184,68 @@ module Decidim
140
184
  published_at.present?
141
185
  end
142
186
 
187
+ # Public: Returns the published state of the proposal.
188
+ #
189
+ # Returns Boolean.
190
+ def state
191
+ return amendment.state if emendation?
192
+ return nil unless published_state? || withdrawn?
193
+
194
+ super
195
+ end
196
+
197
+ # This is only used to define the setter, as the getter will be overriden below.
198
+ alias_attribute :internal_state, :state
199
+
200
+ # Public: Returns the internal state of the proposal.
201
+ #
202
+ # Returns Boolean.
203
+ def internal_state
204
+ return amendment.state if emendation?
205
+
206
+ self[:state]
207
+ end
208
+
209
+ # Public: Checks if the organization has published the state for the proposal.
210
+ #
211
+ # Returns Boolean.
212
+ def published_state?
213
+ emendation? || state_published_at.present?
214
+ end
215
+
143
216
  # Public: Checks if the organization has given an answer for the proposal.
144
217
  #
145
218
  # Returns Boolean.
146
219
  def answered?
147
- answered_at.present? && state.present?
220
+ answered_at.present?
221
+ end
222
+
223
+ # Public: Checks if the author has withdrawn the proposal.
224
+ #
225
+ # Returns Boolean.
226
+ def withdrawn?
227
+ internal_state == "withdrawn"
148
228
  end
149
229
 
150
230
  # Public: Checks if the organization has accepted a proposal.
151
231
  #
152
232
  # Returns Boolean.
153
233
  def accepted?
154
- answered? && state == "accepted"
234
+ state == "accepted"
155
235
  end
156
236
 
157
237
  # Public: Checks if the organization has rejected a proposal.
158
238
  #
159
239
  # Returns Boolean.
160
240
  def rejected?
161
- answered? && state == "rejected"
241
+ state == "rejected"
162
242
  end
163
243
 
164
244
  # Public: Checks if the organization has marked the proposal as evaluating it.
165
245
  #
166
246
  # Returns Boolean.
167
247
  def evaluating?
168
- answered? && state == "evaluating"
169
- end
170
-
171
- # Public: Checks if the author has withdrawn the proposal.
172
- #
173
- # Returns Boolean.
174
- def withdrawn?
175
- state == "withdrawn"
248
+ state == "evaluating"
176
249
  end
177
250
 
178
251
  # Public: Overrides the `reported_content_url` Reportable concern method.
@@ -222,7 +295,7 @@ module Decidim
222
295
  def editable_by?(user)
223
296
  return true if draft?
224
297
 
225
- !answered? && within_edit_time_limit? && !copied_from_other_component? && created_by?(user)
298
+ !published_state? && within_edit_time_limit? && !copied_from_other_component? && created_by?(user)
226
299
  end
227
300
 
228
301
  # Checks whether the user can withdraw the given proposal.
@@ -250,6 +323,69 @@ module Decidim
250
323
  Arel.sql(query)
251
324
  end
252
325
 
326
+ # Defines the base query so that ransack can actually sort by this value
327
+ def self.sort_by_valuation_assignments_count_nulls_last_query
328
+ <<-SQL
329
+ (
330
+ SELECT COUNT(decidim_proposals_valuation_assignments.id)
331
+ FROM decidim_proposals_valuation_assignments
332
+ WHERE decidim_proposals_valuation_assignments.decidim_proposal_id = decidim_proposals_proposals.id
333
+ GROUP BY decidim_proposals_valuation_assignments.decidim_proposal_id
334
+ )
335
+ SQL
336
+ end
337
+
338
+ # method to filter by assigned valuator role ID
339
+ def self.valuator_role_ids_has(value)
340
+ query = <<-SQL
341
+ :value = any(
342
+ (SELECT decidim_proposals_valuation_assignments.valuator_role_id
343
+ FROM decidim_proposals_valuation_assignments
344
+ WHERE decidim_proposals_valuation_assignments.decidim_proposal_id = decidim_proposals_proposals.id
345
+ )
346
+ )
347
+ SQL
348
+ where(query, value: value)
349
+ end
350
+
351
+ def self.ransackable_scopes(_auth = nil)
352
+ [:valuator_role_ids_has]
353
+ end
354
+
355
+ ransacker :state_published do
356
+ Arel.sql("CASE
357
+ WHEN EXISTS (
358
+ SELECT 1 FROM decidim_amendments
359
+ WHERE decidim_amendments.decidim_emendation_type = 'Decidim::Proposals::Proposal'
360
+ AND decidim_amendments.decidim_emendation_id = decidim_proposals_proposals.id
361
+ ) THEN 0
362
+ WHEN state_published_at IS NULL AND answered_at IS NOT NULL THEN 2
363
+ WHEN state_published_at IS NOT NULL THEN 1
364
+ ELSE 0 END
365
+ ")
366
+ end
367
+
368
+ ransacker :state do
369
+ Arel.sql("CASE WHEN state = 'withdrawn' THEN 'withdrawn' WHEN state_published_at IS NULL THEN NULL ELSE state END")
370
+ end
371
+
372
+ ransacker :id_string do
373
+ Arel.sql(%{cast("decidim_proposals_proposals"."id" as text)})
374
+ end
375
+
376
+ ransacker :is_emendation do |_parent|
377
+ query = <<-SQL
378
+ (
379
+ SELECT EXISTS (
380
+ SELECT 1 FROM decidim_amendments
381
+ WHERE decidim_amendments.decidim_emendation_type = 'Decidim::Proposals::Proposal'
382
+ AND decidim_amendments.decidim_emendation_id = decidim_proposals_proposals.id
383
+ )
384
+ )
385
+ SQL
386
+ Arel.sql(query)
387
+ end
388
+
253
389
  def self.export_serializer
254
390
  Decidim::Proposals::ProposalSerializer
255
391
  end
@@ -271,6 +407,17 @@ module Decidim
271
407
  Time.current < limit
272
408
  end
273
409
 
410
+ def process_amendment_state_change!
411
+ return unless %w(accepted rejected evaluating withdrawn).member?(amendment.state)
412
+
413
+ PaperTrail.request(enabled: false) do
414
+ update!(
415
+ state: amendment.state,
416
+ state_published_at: Time.current
417
+ )
418
+ end
419
+ end
420
+
274
421
  private
275
422
 
276
423
  def copied_from_other_component?
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Proposals
5
+ # A valuation assignment links a proposal and a Valuator user role.
6
+ # Valuators will be users in charge of defining the monetary cost of a
7
+ # proposal.
8
+ class ValuationAssignment < ApplicationRecord
9
+ include Decidim::Traceable
10
+ include Decidim::Loggable
11
+
12
+ belongs_to :proposal, foreign_key: "decidim_proposal_id", class_name: "Decidim::Proposals::Proposal"
13
+ belongs_to :valuator_role, polymorphic: true
14
+
15
+ def self.log_presenter_class_for(_log)
16
+ Decidim::Proposals::AdminLog::ValuationAssignmentPresenter
17
+ end
18
+
19
+ def valuator
20
+ valuator_role.user
21
+ end
22
+ end
23
+ end
24
+ end
@@ -8,18 +8,22 @@ module Decidim
8
8
  # The public part needs to be implemented yet
9
9
  return permission_action if permission_action.scope != :admin
10
10
 
11
- if create_permission_action?
12
- # There's no special condition to create proposal notes, only
13
- # users with access to the admin section can do it.
14
- allow! if permission_action.subject == :proposal_note
15
-
16
- # Proposals can only be created from the admin when the
17
- # corresponding setting is enabled.
18
- toggle_allow(admin_creation_is_enabled?) if permission_action.subject == :proposal
11
+ # Valuators can only perform these actions
12
+ if user_is_valuator?
13
+ if valuator_assigned_to_proposal?
14
+ can_create_proposal_note?
15
+ can_create_proposal_answer?
16
+ end
17
+ can_export_proposals?
18
+ valuator_can_unassign_valuator_from_proposals?
19
+
20
+ return permission_action
21
+ end
19
22
 
20
- # Proposals can only be answered from the admin when the
21
- # corresponding setting is enabled.
22
- toggle_allow(admin_proposal_answering_is_enabled?) if permission_action.subject == :proposal_answer
23
+ if create_permission_action?
24
+ can_create_proposal_note?
25
+ can_create_proposal_from_admin?
26
+ can_create_proposal_answer?
23
27
  end
24
28
 
25
29
  # Admins can only edit official proposals if they are within the
@@ -29,15 +33,30 @@ module Decidim
29
33
  # Every user allowed by the space can update the category of the proposal
30
34
  allow! if permission_action.subject == :proposal_category && permission_action.action == :update
31
35
 
36
+ # Every user allowed by the space can update the scope of the proposal
37
+ allow! if permission_action.subject == :proposal_scope && permission_action.action == :update
38
+
32
39
  # Every user allowed by the space can import proposals from another_component
33
40
  allow! if permission_action.subject == :proposals && permission_action.action == :import
34
41
 
42
+ # Every user allowed by the space can export proposals
43
+ can_export_proposals?
44
+
35
45
  # Every user allowed by the space can merge proposals to another component
36
46
  allow! if permission_action.subject == :proposals && permission_action.action == :merge
37
47
 
38
48
  # Every user allowed by the space can split proposals to another component
39
49
  allow! if permission_action.subject == :proposals && permission_action.action == :split
40
50
 
51
+ # Every user allowed by the space can assign proposals to a valuator
52
+ allow! if permission_action.subject == :proposals && permission_action.action == :assign_to_valuator
53
+
54
+ # Every user allowed by the space can unassign a valuator from proposals
55
+ can_unassign_valuator_from_proposals?
56
+
57
+ # Only admin users can publish many answers at once
58
+ toggle_allow(user.admin?) if permission_action.subject == :proposals && permission_action.action == :publish_answers
59
+
41
60
  if permission_action.subject == :participatory_texts && participatory_texts_are_enabled?
42
61
  # Every user allowed by the space can manage (import, update and publish) participatory texts to proposals
43
62
  allow! if permission_action.action == :manage
@@ -52,6 +71,23 @@ module Decidim
52
71
  @proposal ||= context.fetch(:proposal, nil)
53
72
  end
54
73
 
74
+ def user_valuator_role
75
+ @user_valuator_role ||= space.user_roles(:valuator).find_by(user: user)
76
+ end
77
+
78
+ def user_is_valuator?
79
+ return if user.admin?
80
+
81
+ user_valuator_role.present?
82
+ end
83
+
84
+ def valuator_assigned_to_proposal?
85
+ @valuator_assigned_to_proposal ||=
86
+ Decidim::Proposals::ValuationAssignment
87
+ .where(proposal: proposal, valuator_role: user_valuator_role)
88
+ .any?
89
+ end
90
+
55
91
  def admin_creation_is_enabled?
56
92
  current_settings.try(:creation_enabled?) &&
57
93
  component_settings.try(:official_proposals_enabled)
@@ -75,6 +111,36 @@ module Decidim
75
111
  def participatory_texts_are_enabled?
76
112
  component_settings.participatory_texts_enabled?
77
113
  end
114
+
115
+ # There's no special condition to create proposal notes, only
116
+ # users with access to the admin section can do it.
117
+ def can_create_proposal_note?
118
+ allow! if permission_action.subject == :proposal_note
119
+ end
120
+
121
+ # Proposals can only be created from the admin when the
122
+ # corresponding setting is enabled.
123
+ def can_create_proposal_from_admin?
124
+ toggle_allow(admin_creation_is_enabled?) if permission_action.subject == :proposal
125
+ end
126
+
127
+ # Proposals can only be answered from the admin when the
128
+ # corresponding setting is enabled.
129
+ def can_create_proposal_answer?
130
+ toggle_allow(admin_proposal_answering_is_enabled?) if permission_action.subject == :proposal_answer
131
+ end
132
+
133
+ def can_unassign_valuator_from_proposals?
134
+ allow! if permission_action.subject == :proposals && permission_action.action == :unassign_from_valuator
135
+ end
136
+
137
+ def valuator_can_unassign_valuator_from_proposals?
138
+ can_unassign_valuator_from_proposals? if user == context.fetch(:valuator, nil)
139
+ end
140
+
141
+ def can_export_proposals?
142
+ allow! if permission_action.subject == :proposals && permission_action.action == :export
143
+ end
78
144
  end
79
145
  end
80
146
  end