decidim-proposals 0.29.2 → 0.30.0.rc2

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 (196) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/proposals/highlighted_proposals_for_component/show.erb +1 -1
  3. data/app/cells/decidim/proposals/highlighted_proposals_for_component_cell.rb +1 -1
  4. data/app/cells/decidim/proposals/participatory_text_proposal_cell.rb +1 -1
  5. data/app/cells/decidim/proposals/proposal_g/show.erb +13 -0
  6. data/app/cells/decidim/proposals/proposal_g_cell.rb +13 -0
  7. data/app/cells/decidim/proposals/proposal_history_cell.rb +107 -0
  8. data/app/cells/decidim/proposals/proposal_l/show.erb +37 -0
  9. data/app/cells/decidim/proposals/proposal_l_cell.rb +9 -0
  10. data/app/cells/decidim/proposals/proposal_metadata_cell.rb +2 -2
  11. data/app/cells/decidim/proposals/proposal_vote/show.erb +75 -0
  12. data/app/cells/decidim/proposals/proposal_vote_cell.rb +43 -0
  13. data/app/commands/decidim/proposals/accept_coauthorship.rb +62 -0
  14. data/app/commands/decidim/proposals/admin/assign_proposals_to_valuator.rb +14 -0
  15. data/app/commands/decidim/proposals/admin/create_proposal.rb +6 -14
  16. data/app/commands/decidim/proposals/admin/create_proposal_note.rb +20 -11
  17. data/app/commands/decidim/proposals/admin/import_proposals.rb +0 -5
  18. data/app/commands/decidim/proposals/admin/merge_proposals.rb +2 -2
  19. data/app/commands/decidim/proposals/admin/proposal_notes_methods.rb +48 -0
  20. data/app/commands/decidim/proposals/admin/reply_proposal_note.rb +92 -0
  21. data/app/commands/decidim/proposals/admin/split_proposals.rb +2 -2
  22. data/app/commands/decidim/proposals/admin/update_proposal.rb +10 -16
  23. data/app/commands/decidim/proposals/admin/update_proposal_taxonomies.rb +34 -0
  24. data/app/commands/decidim/proposals/cancel_coauthorship.rb +32 -0
  25. data/app/commands/decidim/proposals/create_collaborative_draft.rb +1 -2
  26. data/app/commands/decidim/proposals/create_proposal.rb +1 -2
  27. data/app/commands/decidim/proposals/invite_coauthor.rb +45 -0
  28. data/app/commands/decidim/proposals/publish_collaborative_draft.rb +1 -2
  29. data/app/commands/decidim/proposals/reject_coauthorship.rb +54 -0
  30. data/app/commands/decidim/proposals/update_collaborative_draft.rb +1 -2
  31. data/app/commands/decidim/proposals/update_proposal.rb +1 -2
  32. data/app/controllers/concerns/decidim/proposals/admin/filterable.rb +5 -1
  33. data/app/controllers/concerns/decidim/proposals/admin/needs_interpolations.rb +40 -0
  34. data/app/controllers/decidim/proposals/admin/proposal_answers_controller.rb +55 -2
  35. data/app/controllers/decidim/proposals/admin/proposal_notes_controller.rb +18 -0
  36. data/app/controllers/decidim/proposals/admin/proposals_controller.rb +41 -85
  37. data/app/controllers/decidim/proposals/collaborative_drafts_controller.rb +2 -4
  38. data/app/controllers/decidim/proposals/invite_coauthors_controller.rb +87 -0
  39. data/app/controllers/decidim/proposals/proposals_controller.rb +7 -32
  40. data/app/events/decidim/proposals/accepted_coauthorship_event.rb +8 -0
  41. data/app/events/decidim/proposals/admin/proposal_assigned_to_valuator_event.rb +27 -0
  42. data/app/events/decidim/proposals/admin/proposal_note_created_event.rb +5 -0
  43. data/app/events/decidim/proposals/coauthor_accepted_invite_event.rb +49 -0
  44. data/app/events/decidim/proposals/coauthor_invited_event.rb +45 -0
  45. data/app/events/decidim/proposals/coauthor_rejected_invite_event.rb +8 -0
  46. data/app/events/decidim/proposals/rejected_coauthorship_event.rb +8 -0
  47. data/app/events/decidim/proposals/update_proposal_taxonomies_event.rb +9 -0
  48. data/app/forms/decidim/proposals/admin/proposal_answer_form.rb +1 -0
  49. data/app/forms/decidim/proposals/admin/proposal_base_form.rb +3 -31
  50. data/app/forms/decidim/proposals/admin/proposal_form.rb +11 -6
  51. data/app/forms/decidim/proposals/admin/proposals_import_form.rb +0 -5
  52. data/app/forms/decidim/proposals/collaborative_draft_form.rb +0 -8
  53. data/app/forms/decidim/proposals/proposal_form.rb +5 -32
  54. data/app/helpers/decidim/proposals/admin/proposal_bulk_actions_helper.rb +25 -0
  55. data/app/helpers/decidim/proposals/admin/proposals_helper.rb +0 -1
  56. data/app/helpers/decidim/proposals/application_helper.rb +24 -14
  57. data/app/helpers/decidim/proposals/collaborative_draft_helper.rb +7 -7
  58. data/app/helpers/decidim/proposals/map_helper.rb +0 -18
  59. data/app/helpers/decidim/proposals/proposal_votes_helper.rb +15 -2
  60. data/app/jobs/decidim/proposals/admin/proposal_answer_job.rb +20 -0
  61. data/app/models/decidim/proposals/collaborative_draft.rb +10 -1
  62. data/app/models/decidim/proposals/proposal.rb +66 -5
  63. data/app/models/decidim/proposals/proposal_note.rb +11 -0
  64. data/app/models/decidim/proposals/proposal_state.rb +1 -1
  65. data/app/packs/entrypoints/decidim_proposals.js +1 -0
  66. data/app/packs/entrypoints/decidim_proposals_geocoding.js +2 -0
  67. data/app/packs/src/decidim/proposals/admin/proposals.js +16 -1
  68. data/app/packs/src/decidim/proposals/exit_handler.js +73 -0
  69. data/app/packs/stylesheets/decidim/proposals/proposals.scss +248 -3
  70. data/app/permissions/decidim/proposals/admin/permissions.rb +2 -5
  71. data/app/permissions/decidim/proposals/permissions.rb +42 -0
  72. data/app/presenters/decidim/proposals/admin_log/proposal_presenter.rb +1 -1
  73. data/app/presenters/decidim/proposals/proposal_presenter.rb +1 -1
  74. data/app/queries/decidim/proposals/filtered_proposals.rb +2 -2
  75. data/app/queries/decidim/proposals/metrics/accepted_proposals_metric_manage.rb +2 -2
  76. data/app/queries/decidim/proposals/metrics/endorsements_metric_manage.rb +10 -10
  77. data/app/queries/decidim/proposals/metrics/proposal_followers_metric_measure.rb +4 -4
  78. data/app/queries/decidim/proposals/metrics/proposal_participants_metric_measure.rb +6 -6
  79. data/app/queries/decidim/proposals/metrics/proposals_metric_manage.rb +6 -6
  80. data/app/queries/decidim/proposals/metrics/votes_metric_manage.rb +6 -6
  81. data/app/services/decidim/proposals/proposal_builder.rb +1 -1
  82. data/app/views/decidim/proposals/admin/proposal_notes/_form.html.erb +3 -3
  83. data/app/views/decidim/proposals/admin/proposal_notes/_proposal_note.html.erb +28 -0
  84. data/app/views/decidim/proposals/admin/proposal_notes/_proposal_note_reply.html.erb +9 -0
  85. data/app/views/decidim/proposals/admin/proposal_notes/_proposal_notes.html.erb +4 -28
  86. data/app/views/decidim/proposals/admin/proposal_states/_form.html.erb +1 -1
  87. data/app/views/decidim/proposals/admin/proposals/_actions.html.erb +21 -0
  88. data/app/views/decidim/proposals/admin/proposals/_bulk-actions.html.erb +3 -2
  89. data/app/views/decidim/proposals/admin/proposals/_form.html.erb +17 -24
  90. data/app/views/decidim/proposals/admin/proposals/_proposal-tr.html.erb +12 -28
  91. data/app/views/decidim/proposals/admin/proposals/_proposals-thead.html.erb +45 -0
  92. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_apply_answer_template.html.erb +22 -0
  93. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_dropdown.html.erb +15 -11
  94. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_taxonomy_change.html.erb +23 -0
  95. data/app/views/decidim/proposals/admin/proposals/index.html.erb +17 -48
  96. data/app/views/decidim/proposals/admin/proposals/manage_trash.html.erb +18 -0
  97. data/app/views/decidim/proposals/admin/proposals/publish_answers.js.erb +1 -1
  98. data/app/views/decidim/proposals/admin/proposals/show.html.erb +14 -26
  99. data/app/views/decidim/proposals/admin/proposals/update_attribute.js.erb +1 -1
  100. data/app/views/decidim/proposals/admin/proposals_imports/new.html.erb +0 -3
  101. data/app/views/decidim/proposals/collaborative_drafts/_collaborative_actions.html.erb +9 -0
  102. data/app/views/decidim/proposals/collaborative_drafts/_collaborative_draft_aside.html.erb +0 -15
  103. data/app/views/decidim/proposals/collaborative_drafts/_edit_form_fields.html.erb +4 -6
  104. data/app/views/decidim/proposals/collaborative_drafts/index.html.erb +6 -2
  105. data/app/views/decidim/proposals/collaborative_drafts/show.html.erb +27 -11
  106. data/app/views/decidim/proposals/proposal_votes/update_buttons_and_counters.js.erb +29 -9
  107. data/app/views/decidim/proposals/proposals/_actions.html.erb +4 -7
  108. data/app/views/decidim/proposals/proposals/_edit_form_fields.html.erb +17 -22
  109. data/app/views/decidim/proposals/proposals/_exit_modal.html.erb +17 -0
  110. data/app/views/decidim/proposals/proposals/_notification_alert_box.html.erb +1 -0
  111. data/app/views/decidim/proposals/proposals/_proposal_actions.html.erb +19 -0
  112. data/app/views/decidim/proposals/proposals/_proposal_aside.html.erb +9 -32
  113. data/app/views/decidim/proposals/proposals/_proposal_voting_rules.html.erb +33 -0
  114. data/app/views/decidim/proposals/proposals/_remaining_votes_count.html.erb +2 -2
  115. data/app/views/decidim/proposals/proposals/_remaining_votes_notification.html.erb +12 -0
  116. data/app/views/decidim/proposals/proposals/_update_proposal_voting_rules.html.erb +6 -0
  117. data/app/views/decidim/proposals/proposals/_vote_button.html.erb +12 -8
  118. data/app/views/decidim/proposals/proposals/_votes_count.html.erb +2 -1
  119. data/app/views/decidim/proposals/proposals/_voting_rules.html.erb +1 -7
  120. data/app/views/decidim/proposals/proposals/index.html.erb +10 -18
  121. data/app/views/decidim/proposals/proposals/index.js.erb +1 -1
  122. data/app/views/decidim/proposals/proposals/participatory_texts/_proposal_vote_button.html.erb +3 -1
  123. data/app/views/decidim/proposals/proposals/show.html.erb +35 -15
  124. data/config/locales/ar.yml +18 -72
  125. data/config/locales/bg.yml +7 -89
  126. data/config/locales/bs-BA.yml +0 -13
  127. data/config/locales/ca.yml +212 -72
  128. data/config/locales/cs.yml +213 -73
  129. data/config/locales/de.yml +215 -75
  130. data/config/locales/el.yml +8 -82
  131. data/config/locales/en.yml +209 -69
  132. data/config/locales/es-MX.yml +213 -73
  133. data/config/locales/es-PY.yml +213 -73
  134. data/config/locales/es.yml +215 -75
  135. data/config/locales/eu.yml +217 -78
  136. data/config/locales/fi-plain.yml +216 -75
  137. data/config/locales/fi.yml +216 -75
  138. data/config/locales/fr-CA.yml +118 -87
  139. data/config/locales/fr.yml +118 -87
  140. data/config/locales/ga-IE.yml +0 -19
  141. data/config/locales/gl.yml +8 -43
  142. data/config/locales/hu.yml +6 -66
  143. data/config/locales/id-ID.yml +8 -40
  144. data/config/locales/is-IS.yml +0 -14
  145. data/config/locales/it.yml +8 -53
  146. data/config/locales/ja.yml +162 -87
  147. data/config/locales/lt.yml +8 -83
  148. data/config/locales/lv.yml +8 -50
  149. data/config/locales/nl.yml +6 -55
  150. data/config/locales/no.yml +8 -42
  151. data/config/locales/pl.yml +6 -88
  152. data/config/locales/pt-BR.yml +6 -74
  153. data/config/locales/pt.yml +8 -54
  154. data/config/locales/ro-RO.yml +10 -54
  155. data/config/locales/ru.yml +0 -18
  156. data/config/locales/sk.yml +8 -50
  157. data/config/locales/sr-CS.yml +0 -14
  158. data/config/locales/sv.yml +128 -85
  159. data/config/locales/tr-TR.yml +8 -51
  160. data/config/locales/uk.yml +0 -18
  161. data/config/locales/zh-CN.yml +8 -51
  162. data/config/locales/zh-TW.yml +8 -84
  163. data/db/migrate/20171220084719_add_published_at_to_proposals.rb +1 -1
  164. data/db/migrate/20181016132225_add_organization_as_author.rb +1 -1
  165. data/db/migrate/20200120215928_move_proposal_endorsements_to_core_endorsements.rb +1 -1
  166. data/db/migrate/20200827154156_add_commentable_counter_cache_to_proposals.rb +3 -3
  167. data/db/migrate/20210310102839_add_followable_counter_cache_to_proposals.rb +1 -1
  168. data/db/migrate/20240110203504_create_default_proposal_states.rb +1 -1
  169. data/db/migrate/20240404202756_add_valuation_assignments_count_to_decidim_proposals_proposals.rb +1 -1
  170. data/db/migrate/20240617091140_add_email_on_assigned_proposals_to_users.rb +7 -0
  171. data/db/migrate/20240617170052_add_parent_relation_to_decidim_proposal_notes.rb +7 -0
  172. data/db/migrate/20240828103755_add_deleted_at_to_decidim_proposals_proposals.rb +8 -0
  173. data/decidim-proposals.gemspec +1 -1
  174. data/lib/decidim/api/functions/proposal_finder_helper.rb +12 -0
  175. data/lib/decidim/api/functions/proposal_list_helper.rb +12 -0
  176. data/lib/decidim/api/proposal_type.rb +17 -25
  177. data/lib/decidim/api/proposals_type.rb +4 -19
  178. data/lib/decidim/proposals/admin_engine.rb +12 -3
  179. data/lib/decidim/proposals/admin_filter.rb +3 -6
  180. data/lib/decidim/proposals/component.rb +4 -5
  181. data/lib/decidim/proposals/download_your_data_proposal_serializer.rb +15 -0
  182. data/lib/decidim/proposals/engine.rb +5 -0
  183. data/lib/decidim/proposals/import/proposal_creator.rb +4 -4
  184. data/lib/decidim/proposals/proposal_serializer.rb +12 -29
  185. data/lib/decidim/proposals/seeds.rb +21 -17
  186. data/lib/decidim/proposals/test/factories.rb +2 -1
  187. data/lib/decidim/proposals/version.rb +1 -1
  188. data/lib/decidim/proposals.rb +4 -0
  189. metadata +65 -29
  190. data/app/commands/decidim/proposals/admin/update_proposal_category.rb +0 -70
  191. data/app/commands/decidim/proposals/admin/update_proposal_scope.rb +0 -75
  192. data/app/events/decidim/proposals/admin/update_proposal_category_event.rb +0 -11
  193. data/app/events/decidim/proposals/admin/update_proposal_scope_event.rb +0 -11
  194. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_recategorize.html.erb +0 -15
  195. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_scope-change.html.erb +0 -21
  196. data/app/views/decidim/proposals/collaborative_drafts/_actions.html.erb +0 -7
@@ -7,6 +7,7 @@ module Decidim
7
7
  include Decidim::TranslatableAttributes
8
8
  include Decidim::AttachmentAttributes
9
9
  include Decidim::HasUploadValidations
10
+ include Decidim::HasTaxonomyFormAttributes
10
11
 
11
12
  mimic :proposal
12
13
 
@@ -17,29 +18,24 @@ module Decidim
17
18
  attribute :address, String
18
19
  attribute :latitude, Float
19
20
  attribute :longitude, Float
20
- attribute :category_id, Integer
21
- attribute :scope_id, Integer
22
21
  attribute :attachment, AttachmentForm
23
22
  attribute :suggested_hashtags, Array[String]
24
23
 
25
24
  attachments_attribute :documents
26
25
 
27
- validates :title, :body, presence: true, etiquette: true
26
+ validates :title, :body, presence: true
27
+ validates :title, :body, etiquette: true
28
28
  validates :title, length: { in: 15..150 }
29
29
  validates :body, proposal_length: {
30
30
  minimum: 15,
31
31
  maximum: ->(record) { record.component.settings.proposal_length }
32
32
  }
33
33
  validates :address, geocoding: true, if: ->(form) { form.has_address? && !form.geocoded? }
34
- validates :category, presence: true, if: ->(form) { form.category_id.present? }
35
- validates :scope, presence: true, if: ->(form) { form.scope_id.present? }
36
- validates :scope_id, scope_belongs_to_component: true, if: ->(form) { form.scope_id.present? }
37
34
 
38
35
  validate :body_is_not_bare_template
39
36
  validate :notify_missing_attachment_if_errored
40
37
 
41
38
  alias component current_component
42
- delegate :categories, to: :current_component
43
39
 
44
40
  def map_model(model)
45
41
  self.title = translated_attribute(model.title)
@@ -50,34 +46,11 @@ module Decidim
50
46
  self.body = presenter.editor_body(all_locales: body.is_a?(Hash))
51
47
 
52
48
  self.user_group_id = model.user_groups.first&.id
53
- self.category_id = model.categorization.decidim_category_id if model.categorization
54
-
55
- # The scope attribute is with different key (decidim_scope_id), so it
56
- # has to be manually mapped.
57
- self.scope_id = model.scope.id if model.scope
58
-
59
49
  self.documents = model.attachments
60
50
  end
61
51
 
62
- # Finds the Category from the category_id.
63
- #
64
- # Returns a Decidim::Category
65
- def category
66
- @category ||= categories.find_by(id: category_id)
67
- end
68
-
69
- # Finds the Scope from the given scope_id, uses participatory space scope if missing.
70
- #
71
- # Returns a Decidim::Scope
72
- def scope
73
- @scope ||= @attributes["scope_id"].value ? current_component.scopes.find_by(id: @attributes["scope_id"].value) : current_component.scope
74
- end
75
-
76
- # Scope identifier
77
- #
78
- # Returns the scope identifier related to the proposal
79
- def scope_id
80
- super || scope&.id
52
+ def participatory_space_manifest
53
+ @participatory_space_manifest ||= current_component.participatory_space.manifest.name
81
54
  end
82
55
 
83
56
  def geocoding_enabled?
@@ -8,6 +8,31 @@ module Decidim
8
8
  Decidim::Proposals::Proposal.find(id)
9
9
  end
10
10
 
11
+ # Public: Generates a select field with the templates of the given component.
12
+ #
13
+ # component - A component instance.
14
+ # prompt - An i18n string to show as prompt
15
+ #
16
+ # Returns a String.
17
+ def bulk_templates_select(component, prompt, id: nil)
18
+ options_for_select = find_templates_for_select(component)
19
+ select(:template, :template_id, options_for_select, prompt:, id:)
20
+ end
21
+
22
+ def find_templates_for_select(component)
23
+ return [] unless Decidim.module_installed? :templates
24
+ return @templates_for_select if @templates_for_select
25
+
26
+ templates = Decidim::Templates::Template.where(
27
+ target: :proposal_answer,
28
+ templatable: component
29
+ ).order(:templatable_id)
30
+
31
+ @templates_for_select = templates.map do |template|
32
+ [translated_attribute(template.name), template.id]
33
+ end
34
+ end
35
+
11
36
  # find the valuators for the current space.
12
37
  def find_valuators_for_select(participatory_space, current_user)
13
38
  valuator_roles = participatory_space.user_roles(:valuator).order_by_name
@@ -7,7 +7,6 @@ module Decidim
7
7
  # in order to use them in select forms for Proposals.
8
8
  #
9
9
  module ProposalsHelper
10
- include Decidim::Admin::ResourceScopeHelper
11
10
  include Decidim::TranslatableAttributes
12
11
 
13
12
  def available_states
@@ -30,6 +30,7 @@ module Decidim
30
30
 
31
31
  def proposal_state_css_style(proposal)
32
32
  return "" if proposal.emendation?
33
+ return "" if proposal.withdrawn?
33
34
 
34
35
  proposal.proposal_state&.css_style
35
36
  end
@@ -144,8 +145,16 @@ module Decidim
144
145
  ).count
145
146
  end
146
147
 
148
+ def layout_item_classes
149
+ if show_voting_rules?
150
+ "layout-item lg:pt-4"
151
+ else
152
+ "layout-item"
153
+ end
154
+ end
155
+
147
156
  def show_voting_rules?
148
- return false if !votes_enabled? || current_settings.votes_blocked?
157
+ return false if !votes_enabled? || votes_blocked?
149
158
 
150
159
  return true if vote_limit_enabled?
151
160
  return true if threshold_per_proposal_enabled?
@@ -206,41 +215,38 @@ module Decidim
206
215
  end
207
216
 
208
217
  # rubocop:disable Metrics/CyclomaticComplexity
209
- # rubocop:disable Metrics/PerceivedComplexity
210
218
  def filter_sections
211
219
  @filter_sections ||= begin
212
220
  items = []
213
221
  if component_settings.proposal_answering_enabled && current_settings.proposal_answering_enabled
214
- items.append(method: :with_any_state, collection: filter_proposals_state_values, label_scope: "decidim.proposals.proposals.filters", id: "state")
222
+ items.append(method: :with_any_state, collection: filter_proposals_state_values, label: t("decidim.proposals.proposals.filters.state"), id: "state")
215
223
  end
216
- if current_component.has_subscopes?
217
- items.append(method: :with_any_scope, collection: filter_scopes_values, label_scope: "decidim.proposals.proposals.filters", id: "scope")
218
- end
219
- if current_component.categories.any?
220
- items.append(method: :with_any_category, collection: filter_categories_values, label_scope: "decidim.proposals.proposals.filters", id: "category")
224
+ current_component.available_taxonomy_filters.each do |taxonomy_filter|
225
+ items.append(method: "with_any_taxonomies[#{taxonomy_filter.root_taxonomy_id}]",
226
+ collection: filter_taxonomy_values_for(taxonomy_filter),
227
+ label: decidim_sanitize_translated(taxonomy_filter.name),
228
+ id: "taxonomy-#{taxonomy_filter.root_taxonomy_id}")
221
229
  end
222
230
  if component_settings.official_proposals_enabled
223
- items.append(method: :with_any_origin, collection: filter_origin_values, label_scope: "decidim.proposals.proposals.filters", id: "origin")
231
+ items.append(method: :with_any_origin, collection: filter_origin_values, label: t("decidim.proposals.proposals.filters.origin"), id: "origin")
224
232
  end
225
233
  if current_user
226
- items.append(method: :activity, collection: activity_filter_values, label_scope: "decidim.proposals.proposals.filters", id: "activity", type: :radio_buttons)
234
+ items.append(method: :activity, collection: activity_filter_values, label: t("decidim.proposals.proposals.filters.activity"), id: "activity", type: :radio_buttons)
227
235
  end
228
236
  if @proposals.only_emendations.any?
229
- items.append(method: :type, collection: filter_type_values, label_scope: "decidim.proposals.proposals.filters", id: "amendment_type", type: :radio_buttons)
237
+ items.append(method: :type, collection: filter_type_values, label: t("decidim.proposals.proposals.filters.amendment_type"), id: "amendment_type", type: :radio_buttons)
230
238
  end
231
239
  if linked_classes_for(Decidim::Proposals::Proposal).any?
232
240
  items.append(
233
241
  method: :related_to,
234
242
  collection: linked_classes_filter_values_for(Decidim::Proposals::Proposal),
235
- label_scope: "decidim.proposals.proposals.filters",
243
+ label: t("decidim.proposals.proposals.filters.related_to"),
236
244
  id: "related_to",
237
245
  type: :radio_buttons
238
246
  )
239
247
  end
240
248
  end
241
- # rubocop:enable Metrics/PerceivedComplexity
242
249
  # rubocop:enable Metrics/CyclomaticComplexity
243
-
244
250
  items.reject { |item| item[:collection].blank? }
245
251
  end
246
252
 
@@ -248,6 +254,10 @@ module Decidim
248
254
  i18n_key = controller_name == "collaborative_drafts" ? "decidim.proposals.collaborative_drafts.name" : "decidim.components.proposals.name"
249
255
  (defined?(current_component) && translated_attribute(current_component&.name).presence) || t(i18n_key)
250
256
  end
257
+
258
+ def templates_available?
259
+ Decidim.module_installed?(:templates) && defined?(Decidim::Templates::Template) && Decidim::Templates::Template.exists?(templatable: current_component)
260
+ end
251
261
  end
252
262
  end
253
263
  end
@@ -43,20 +43,20 @@ module Decidim
43
43
  items = [{
44
44
  method: :with_any_state,
45
45
  collection: filter_collaborative_drafts_state_values,
46
- label_scope: "decidim.proposals.collaborative_drafts.filters",
46
+ label: t("decidim.proposals.collaborative_drafts.filters.state"),
47
47
  id: "state"
48
48
  }]
49
- if current_component.has_subscopes?
50
- items.append(method: :with_any_scope, collection: filter_scopes_values, label_scope: "decidim.proposals.collaborative_drafts.filters", id: "scope")
51
- end
52
- if current_component.categories.any?
53
- items.append(method: :with_any_category, collection: filter_categories_values, label_scope: "decidim.proposals.collaborative_drafts.filters", id: "category")
49
+ current_component.available_taxonomy_filters.each do |taxonomy_filter|
50
+ items.append(method: "with_any_taxonomies[#{taxonomy_filter.root_taxonomy_id}]",
51
+ collection: filter_taxonomy_values_for(taxonomy_filter),
52
+ label: decidim_sanitize_translated(taxonomy_filter.name),
53
+ id: "taxonomy-#{taxonomy_filter.root_taxonomy_id}")
54
54
  end
55
55
  if linked_classes_for(Decidim::Proposals::CollaborativeDraft).any?
56
56
  items.append(
57
57
  method: :related_to,
58
58
  collection: linked_classes_filter_values_for(Decidim::Proposals::CollaborativeDraft),
59
- label_scope: "decidim.proposals.collaborative_drafts.filters",
59
+ label: t("decidim.proposals.collaborative_drafts.filters.related_to"),
60
60
  id: "related_to",
61
61
  type: :radio_buttons
62
62
  )
@@ -5,24 +5,6 @@ module Decidim
5
5
  # This helper include some methods for rendering proposals dynamic maps.
6
6
  module MapHelper
7
7
  include Decidim::ApplicationHelper
8
- # Serialize a collection of geocoded proposals to be used by the dynamic map component
9
- #
10
- # geocoded_proposals - A collection of geocoded proposals
11
- def proposals_data_for_map(geocoded_proposals)
12
- geocoded_proposals.select(&:geocoded_and_valid?).map do |proposal|
13
- proposal_data_for_map(proposal)
14
- end
15
- end
16
-
17
- def proposal_data_for_map(proposal)
18
- proposal
19
- .slice(:latitude, :longitude, :address)
20
- .merge(
21
- title: decidim_html_escape(present(proposal).title),
22
- link: proposal_path(proposal),
23
- items: cell("decidim/proposals/proposal_metadata", proposal).send(:proposal_items_for_map).to_json
24
- )
25
- end
26
8
 
27
9
  def proposal_preview_data_for_map(proposal)
28
10
  {
@@ -47,14 +47,14 @@ module Decidim
47
47
  #
48
48
  # Returns true if enabled, false otherwise.
49
49
  def votes_enabled?
50
- current_settings.votes_enabled
50
+ current_settings.respond_to?(:votes_enabled) && current_settings.votes_enabled
51
51
  end
52
52
 
53
53
  # Public: Checks if voting is blocked in this step.
54
54
  #
55
55
  # Returns true if blocked, false otherwise.
56
56
  def votes_blocked?
57
- current_settings.votes_blocked
57
+ current_settings.respond_to?(:votes_blocked) && current_settings.votes_blocked
58
58
  end
59
59
 
60
60
  # Public: Checks if the current user is allowed to vote in this step.
@@ -76,6 +76,19 @@ module Decidim
76
76
  votes_count = ProposalVote.where(author: user, proposal: proposals).size
77
77
  component_settings.vote_limit - votes_count
78
78
  end
79
+
80
+ # Return the remaining minimum votes for a user if the current component has a vote limit
81
+ #
82
+ # user - A User object
83
+ #
84
+ # Returns a number with the remaining minimum votes for that user
85
+ def remaining_minimum_votes_count_for(user)
86
+ return 0 unless vote_limit_enabled?
87
+
88
+ votes_count = Decidim::Proposals::ProposalVote.joins(:proposal).where(decidim_proposals_proposals: { decidim_component_id: current_component.id }).where(author: user).count
89
+
90
+ component_settings.minimum_votes_per_user - votes_count
91
+ end
79
92
  end
80
93
  end
81
94
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Proposals
5
+ module Admin
6
+ class ProposalAnswerJob < ApplicationJob
7
+ queue_as :default
8
+
9
+ def perform(proposal, attributes, context)
10
+ answer_form = ProposalAnswerForm.from_params(attributes).with_context(**context)
11
+
12
+ Admin::AnswerProposal.call(answer_form, proposal) do
13
+ on(:ok) { Rails.logger.info "Proposal #{proposal.id} answered successfully." }
14
+ on(:invalid) { Rails.logger.error "Proposal ID #{proposal.id} could not be updated. Errors: #{answer_form.errors.full_messages}" }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -5,6 +5,7 @@ module Decidim
5
5
  class CollaborativeDraft < Proposals::ApplicationRecord
6
6
  include Decidim::Resourceable
7
7
  include Decidim::Coauthorable
8
+ include Decidim::Taxonomizable
8
9
  include Decidim::HasComponent
9
10
  include Decidim::ScopableResource
10
11
  include Decidim::HasReference
@@ -64,7 +65,15 @@ module Decidim
64
65
  ransacker_text_multi :search_text, [:title, :body]
65
66
 
66
67
  def self.ransackable_scopes(_auth_object = nil)
67
- [:with_any_state, :related_to, :with_any_scope, :with_any_category]
68
+ [:with_any_state, :related_to, :with_any_taxonomies]
69
+ end
70
+
71
+ def self.ransackable_attributes(_auth_object = nil)
72
+ %w(id_string search_text title body)
73
+ end
74
+
75
+ def self.ransackable_associations(_auth_object = nil)
76
+ %w(taxonomies)
68
77
  end
69
78
  end
70
79
  end
@@ -7,6 +7,7 @@ module Decidim
7
7
  include Decidim::Resourceable
8
8
  include Decidim::Coauthorable
9
9
  include Decidim::HasComponent
10
+ include Decidim::Taxonomizable
10
11
  include Decidim::ScopableResource
11
12
  include Decidim::HasReference
12
13
  include Decidim::HasCategory
@@ -28,6 +29,7 @@ module Decidim
28
29
  include Decidim::TranslatableResource
29
30
  include Decidim::TranslatableAttributes
30
31
  include Decidim::FilterableResource
32
+ include Decidim::SoftDeletable
31
33
 
32
34
  def assign_state(token)
33
35
  proposal_state = Decidim::Proposals::ProposalState.where(component:, token:).first
@@ -403,13 +405,21 @@ module Decidim
403
405
  end
404
406
 
405
407
  def self.ransackable_scopes(_auth_object = nil)
406
- [:with_any_origin, :with_any_state, :state_eq, :voted_by, :coauthored_by, :related_to, :with_any_scope, :with_any_category, :valuator_role_ids_has]
408
+ [:with_any_origin, :with_any_state, :state_eq, :voted_by, :coauthored_by, :related_to, :with_any_taxonomies, :valuator_role_ids_has]
407
409
  end
408
410
 
409
411
  # Create i18n ransackers for :title and :body.
410
412
  # Create the :search_text ransacker alias for searching from both of these.
411
413
  ransacker_i18n_multi :search_text, [:title, :body]
412
414
 
415
+ def self.ransackable_attributes(_auth_object = nil)
416
+ %w(id_string search_text title body is_emendation comments_count proposal_votes_count published_at proposal_notes_count)
417
+ end
418
+
419
+ def self.ransackable_associations(_auth_object = nil)
420
+ %w(taxonomies proposal_state)
421
+ end
422
+
413
423
  ransacker :state_published do
414
424
  Arel.sql("CASE
415
425
  WHEN EXISTS (
@@ -455,7 +465,7 @@ module Decidim
455
465
  end
456
466
 
457
467
  def self.export_serializer
458
- Decidim::Proposals::ProposalSerializer
468
+ Decidim::Proposals::DownloadYourDataProposalSerializer
459
469
  end
460
470
 
461
471
  def self.download_your_data_images(user)
@@ -472,8 +482,18 @@ module Decidim
472
482
  return true if draft?
473
483
  return true if component.settings.proposal_edit_time == "infinite"
474
484
 
475
- limit = updated_at + component.settings.proposal_edit_before_minutes.minutes
476
- Time.current < limit
485
+ time_value, time_unit = component.settings.edit_time
486
+
487
+ limit_time = case time_unit
488
+ when "minutes"
489
+ updated_at + time_value.minutes
490
+ when "hours"
491
+ updated_at + time_value.hours
492
+ else
493
+ updated_at + time_value.days
494
+ end
495
+
496
+ Time.current < limit_time
477
497
  end
478
498
 
479
499
  def process_amendment_state_change!
@@ -486,10 +506,51 @@ module Decidim
486
506
  end
487
507
  end
488
508
 
509
+ def user_has_actions?(user)
510
+ return false if authors.include?(user)
511
+ return false if user&.blocked?
512
+ return false if user&.deleted?
513
+ return false unless user&.confirmed?
514
+
515
+ true
516
+ end
517
+
518
+ def actions_for_comment(comment, current_user)
519
+ return if comment.commentable != self
520
+ return unless authors.include?(current_user)
521
+ return unless user_has_actions?(comment.author)
522
+
523
+ if coauthor_invitations_for(comment.author).any?
524
+ [
525
+ {
526
+ label: I18n.t("decidim.proposals.actions.cancel_coauthor_invitation"),
527
+ url: EngineRouter.main_proxy(component).cancel_proposal_invite_coauthors_path(proposal_id: id, id: comment.author.id),
528
+ icon: "user-forbid-line",
529
+ method: :delete,
530
+ data: { confirm: I18n.t("decidim.proposals.actions.cancel_coauthor_invitation_confirm") }
531
+ }
532
+ ]
533
+ else
534
+ [
535
+ {
536
+ label: I18n.t("decidim.proposals.actions.mark_as_coauthor"),
537
+ url: EngineRouter.main_proxy(component).proposal_invite_coauthors_path(proposal_id: id, id: comment.author.id),
538
+ icon: "user-add-line",
539
+ method: :post,
540
+ data: { confirm: I18n.t("decidim.proposals.actions.mark_as_coauthor_confirm") }
541
+ }
542
+ ]
543
+ end
544
+ end
545
+
546
+ def coauthor_invitations_for(user)
547
+ Decidim::Notification.where(event_class: "Decidim::Proposals::CoauthorInvitedEvent", resource: self, user:)
548
+ end
549
+
489
550
  private
490
551
 
491
552
  def copied_from_other_component?
492
- linked_resources(:proposals, "copied_from_component").any?
553
+ linked_resources(:proposals, %w(splitted_from_component merged_from_component copied_from_component)).any?
493
554
  end
494
555
  end
495
556
  end
@@ -9,12 +9,23 @@ module Decidim
9
9
 
10
10
  belongs_to :proposal, foreign_key: "decidim_proposal_id", class_name: "Decidim::Proposals::Proposal", counter_cache: true
11
11
  belongs_to :author, foreign_key: "decidim_author_id", class_name: "Decidim::User"
12
+ has_many :replies, foreign_key: "parent_id", class_name: "Decidim::Proposals::ProposalNote", inverse_of: :parent, dependent: :destroy
13
+ belongs_to :parent, class_name: "Decidim::Proposals::ProposalNote", inverse_of: :replies, optional: true
12
14
 
15
+ scope :not_reply, -> { where(parent_id: nil) }
13
16
  default_scope { order(created_at: :asc) }
14
17
 
15
18
  def self.log_presenter_class_for(_log)
16
19
  Decidim::Proposals::AdminLog::ProposalNotePresenter
17
20
  end
21
+
22
+ def reply?
23
+ parent.present?
24
+ end
25
+
26
+ def formatted_body
27
+ Decidim::ContentProcessor.render_without_format(body)
28
+ end
18
29
  end
19
30
  end
20
31
  end
@@ -38,7 +38,7 @@ module Decidim
38
38
  protected
39
39
 
40
40
  def generate_token
41
- self.token = ensure_unique_token(translated_attribute(title).parameterize(separator: "_"))
41
+ self.token = ensure_unique_token(token.presence || translated_attribute(title).parameterize(separator: "_"))
42
42
  end
43
43
 
44
44
  def ensure_unique_token(token)
@@ -1,6 +1,7 @@
1
1
  import "src/decidim/proposals/utils"
2
2
  import "src/decidim/proposals/add_proposal"
3
3
  import "src/decidim/proposals/choose_proposals"
4
+ import "src/decidim/proposals/exit_handler"
4
5
 
5
6
  // Images
6
7
  require.context("../images", true)
@@ -0,0 +1,2 @@
1
+ import "src/decidim/proposals/reverse_geocoding.js"
2
+ import "stylesheets/decidim/proposals/geocoding_addons.scss";
@@ -13,13 +13,20 @@ $(() => {
13
13
  return $(".table-list [data-published-state=false] .js-check-all-proposal:checked").length
14
14
  }
15
15
 
16
+ const selectedProposalsAllowsAnswerCount = function() {
17
+ return $(".table-list [data-allow-answer=true] .js-check-all-proposal:checked").length
18
+ }
19
+
16
20
  const selectedProposalsCountUpdate = function() {
17
21
  const selectedProposals = selectedProposalsCount();
18
22
  const selectedProposalsNotPublishedAnswer = selectedProposalsNotPublishedAnswerCount();
23
+ const allowAnswerProposals = selectedProposalsAllowsAnswerCount();
24
+
19
25
  if (selectedProposals === 0) {
20
26
  $("#js-selected-proposals-count").text("")
21
27
  $("#js-assign-proposals-to-valuator-actions").addClass("hide");
22
28
  $("#js-unassign-proposals-from-valuator-actions").addClass("hide");
29
+ $("#js-taxonomy-change-proposals-actions").addClass("hide");
23
30
  } else {
24
31
  $("#js-selected-proposals-count").text(selectedProposals);
25
32
  }
@@ -36,6 +43,13 @@ $(() => {
36
43
  } else {
37
44
  $('button[data-action="publish-answers"]').parent().hide();
38
45
  }
46
+
47
+ if (allowAnswerProposals > 0) {
48
+ $('button[data-action="apply-answer-template"]').parent().show();
49
+ $("#js-form-apply-answer-template-number").text(allowAnswerProposals);
50
+ } else {
51
+ $('button[data-action="apply-answer-template"]').parent().hide();
52
+ }
39
53
  }
40
54
 
41
55
  const showBulkActionsButton = function() {
@@ -91,7 +105,8 @@ $(() => {
91
105
  let action = $(e.target).data("action");
92
106
  const panelActions = [
93
107
  "assign-proposals-to-valuator",
94
- "unassign-proposals-from-valuator"
108
+ "unassign-proposals-from-valuator",
109
+ "taxonomy-change-proposals"
95
110
  ];
96
111
 
97
112
  if (!action) {
@@ -0,0 +1,73 @@
1
+ const allowExitFrom = (el) => {
2
+ if (el.id === "exit-proposal-notification-link" || el.classList.contains("no-modal")) {
3
+ return true;
4
+ }
5
+
6
+ return false;
7
+ };
8
+
9
+ document.addEventListener("DOMContentLoaded", () => {
10
+ const exitNotification = document.getElementById("exit-proposal-notification");
11
+ const exitLink = document.getElementById("exit-proposal-notification-link");
12
+ if (!exitLink) {
13
+ return;
14
+ }
15
+ const defaultExitUrl = exitLink.href;
16
+ const defaultExitLinkText = exitLink.textContent;
17
+ const signOutPath = window.Decidim.config.get("sign_out_path");
18
+ let exitLinkText = defaultExitLinkText;
19
+
20
+ if (!exitNotification) {
21
+ // Do not apply when not inside the voting pipeline
22
+ return;
23
+ }
24
+
25
+ const openExitNotification = (url, method = null) => {
26
+ if (method && method !== "get") {
27
+ exitLink.setAttribute("data-method", method);
28
+ } else {
29
+ exitLink.removeAttribute("data-method");
30
+ }
31
+
32
+ exitLink.setAttribute("href", url);
33
+ exitLink.textContent = exitLinkText;
34
+ window.Decidim.currentDialogs["exit-proposal-notification"].open();
35
+ };
36
+
37
+ const handleClicks = (link) => {
38
+ link.addEventListener("click", (event) => {
39
+ exitLinkText = defaultExitLinkText;
40
+
41
+ if (
42
+ !allowExitFrom(link) &&
43
+ ((window.Decidim.currentDialogs["exit-proposal-notification"].dialog.querySelector("[data-dialog-container]")).dataset.minimumVotesReached !== "true") &&
44
+ ((window.Decidim.currentDialogs["exit-proposal-notification"].dialog.querySelector("[data-dialog-container]")).dataset.minimumVotesCount > 0)
45
+ ) {
46
+ event.preventDefault();
47
+ openExitNotification(link.getAttribute("href"), link.dataset.method);
48
+ }
49
+ });
50
+ };
51
+
52
+ document.querySelectorAll("a").forEach(handleClicks);
53
+ // Custom handling for the header sign-out link
54
+ const signOutLink = document.querySelector(`[href='${signOutPath}']`);
55
+ if (signOutLink) {
56
+ signOutLink.addEventListener("click", (event) => {
57
+ event.preventDefault();
58
+ event.stopPropagation();
59
+
60
+ exitLinkText = signOutLink.textContent;
61
+ openExitNotification(signOutLink.getAttribute("href"), signOutLink.dataset.method);
62
+ });
63
+ }
64
+
65
+ // Custom handling for links that open the exit notification dialog
66
+ const dialogOpenLinks = document.querySelectorAll("a[data-dialog-open='exit-proposal-notification']");
67
+ dialogOpenLinks.forEach((link) => {
68
+ link.addEventListener("click", () => {
69
+ exitLinkText = defaultExitLinkText;
70
+ openExitNotification(defaultExitUrl);
71
+ });
72
+ });
73
+ });