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
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Proposals
5
+ module Admin
6
+ # A module with common methods of proposal notes commands
7
+ module ProposalNotesMethods
8
+ private
9
+
10
+ def parsed_body
11
+ @parsed_body ||= Decidim::ContentProcessor.parse_with_processor(:user, form.body, current_organization: form.current_organization)
12
+ end
13
+
14
+ def mentioned_users
15
+ @mentioned_users ||= parsed_body.metadata[:user].users
16
+ end
17
+
18
+ def rewritten_body
19
+ @rewritten_body ||= parsed_body.rewrite
20
+ end
21
+
22
+ def proposal_valuators
23
+ @proposal_valuators ||= Decidim::Proposals::ValuationAssignment.where(proposal:).filter_map do |assignment|
24
+ assignment.valuator unless assignment.valuator == form.current_user
25
+ end
26
+ end
27
+
28
+ def admins
29
+ @admins ||= Decidim::User.org_admins_except_me(form.current_user).all
30
+ end
31
+
32
+ def space_admins
33
+ @space_admins ||= proposal.participatory_space.user_roles("admin").includes(:user).filter_map do |role|
34
+ role.user unless role.user == form.current_user
35
+ end
36
+ end
37
+
38
+ def mentioned_admins_or_valuators
39
+ mentioned_users.select do |user|
40
+ admins.exists?(user.id) ||
41
+ space_admins.include?(user) ||
42
+ proposal_valuators.include?(user)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Proposals
5
+ module Admin
6
+ # A command with all the business logic when an admin creates a private note proposal reply.
7
+ class ReplyProposalNote < Decidim::Command
8
+ include ProposalNotesMethods
9
+
10
+ # Public: Initializes the command.
11
+ #
12
+ # form - A form object with the params.
13
+ # parent - the note to reply.
14
+ def initialize(form, parent)
15
+ @form = form
16
+ @parent = parent
17
+ @proposal = parent.proposal
18
+ end
19
+
20
+ # Executes the command. Broadcasts these events:
21
+ #
22
+ # - :ok when everything is valid, together with the note proposal.
23
+ # - :invalid if the form was not valid and we could not proceed.
24
+ #
25
+ # Returns nothing.
26
+ def call
27
+ return broadcast(:invalid) if form.invalid? || invalid_parent?
28
+
29
+ create_proposal_note_reply
30
+ notify_parent_author
31
+ notify_mentioned
32
+
33
+ broadcast(:ok, proposal_note)
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :form, :proposal_note, :parent, :proposal
39
+
40
+ def invalid_parent?
41
+ parent.blank? || parent.reply?
42
+ end
43
+
44
+ def create_proposal_note_reply
45
+ @proposal_note = Decidim.traceability.create!(
46
+ ProposalNote,
47
+ form.current_user,
48
+ {
49
+ body: rewritten_body,
50
+ proposal:,
51
+ parent:,
52
+ author: form.current_user
53
+ },
54
+ resource: {
55
+ title: proposal.title
56
+ }
57
+ )
58
+ end
59
+
60
+ def parent_author
61
+ @parent_author ||= parent.author
62
+ end
63
+
64
+ def notify_parent_author
65
+ return if form.current_user == parent_author
66
+
67
+ Decidim::EventsManager.publish(
68
+ event: "decidim.events.proposals.admin.proposal_note_replied",
69
+ event_class: Decidim::Proposals::Admin::ProposalNoteCreatedEvent,
70
+ resource: proposal,
71
+ affected_users: [parent_author],
72
+ extra: { note_author_id: form.current_user.id }
73
+ )
74
+ end
75
+
76
+ def notify_mentioned
77
+ affected_users = mentioned_admins_or_valuators - [parent_author]
78
+
79
+ return if affected_users.blank?
80
+
81
+ Decidim::EventsManager.publish(
82
+ event: "decidim.events.proposals.admin.proposal_note_mentioned",
83
+ event_class: Decidim::Proposals::Admin::ProposalNoteCreatedEvent,
84
+ resource: proposal,
85
+ affected_users:,
86
+ extra: { note_author_id: form.current_user.id }
87
+ )
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -53,13 +53,13 @@ module Decidim
53
53
  )
54
54
 
55
55
  proposals_to_link = links_for(original_proposal)
56
- split_proposal.link_resources(proposals_to_link, "copied_from_component")
56
+ split_proposal.link_resources(proposals_to_link, "splitted_from_component")
57
57
  end
58
58
 
59
59
  def links_for(proposal)
60
60
  return proposal unless form.same_component?
61
61
 
62
- proposal.linked_resources(:proposals, "copied_from_component")
62
+ proposal.linked_resources(:proposals, "splitted_from_component")
63
63
  end
64
64
  end
65
65
  end
@@ -5,8 +5,7 @@ module Decidim
5
5
  module Admin
6
6
  # A command with all the business logic when a user updates a proposal.
7
7
  class UpdateProposal < Decidim::Command
8
- include ::Decidim::AttachmentMethods
9
- include GalleryMethods
8
+ include ::Decidim::MultipleAttachmentsMethods
10
9
  include HashtagsMethods
11
10
 
12
11
  # Public: Initializes the command.
@@ -31,23 +30,15 @@ module Decidim
31
30
  delete_attachment(form.attachment) if delete_attachment?
32
31
 
33
32
  if process_attachments?
34
- @proposal.attachments.destroy_all
35
-
36
- build_attachment
37
- return broadcast(:invalid) if attachment_invalid?
38
- end
39
-
40
- if process_gallery?
41
- build_gallery
42
- return broadcast(:invalid) if gallery_invalid?
33
+ build_attachments
34
+ return broadcast(:invalid) if attachments_invalid?
43
35
  end
44
36
 
45
37
  transaction do
46
38
  update_proposal
47
39
  update_proposal_author
48
- create_gallery if process_gallery?
49
- create_attachment(weight: first_attachment_weight) if process_attachments?
50
- photo_cleanup!
40
+ document_cleanup!(include_all_attachments: true)
41
+ create_attachments(first_weight: first_attachment_weight) if process_attachments?
51
42
  end
52
43
 
53
44
  broadcast(:ok, proposal)
@@ -69,8 +60,7 @@ module Decidim
69
60
  form.current_user,
70
61
  title: parsed_title,
71
62
  body: parsed_body,
72
- category: form.category,
73
- scope: form.scope,
63
+ taxonomizations: form.taxonomizations,
74
64
  address: form.address,
75
65
  latitude: form.latitude,
76
66
  longitude: form.longitude,
@@ -90,6 +80,10 @@ module Decidim
90
80
 
91
81
  proposal.photos.count
92
82
  end
83
+
84
+ def delete_attachment?
85
+ @form.attachment&.delete_file.present?
86
+ end
93
87
  end
94
88
  end
95
89
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Proposals
5
+ module Admin
6
+ # A command with all the business logic when an admin batch updates proposals taxonomies.
7
+ class UpdateProposalTaxonomies < UpdateResourcesTaxonomies
8
+ include TranslatableAttributes
9
+ # Public: Initializes the command.
10
+ #
11
+ # taxonomy_ids - the taxonomy ids to update
12
+ # proposal_ids - the proposals ids to update.
13
+ def initialize(taxonomy_ids, proposal_ids, organization)
14
+ super(taxonomy_ids, Decidim::Proposals::Proposal.where(id: proposal_ids), organization)
15
+ end
16
+
17
+ def run_after_hooks(resource)
18
+ notify_author(resource) if resource.coauthorships.any?
19
+ end
20
+
21
+ private
22
+
23
+ def notify_author(proposal)
24
+ Decidim::EventsManager.publish(
25
+ event: "decidim.events.proposals.proposal_update_taxonomies",
26
+ event_class: Decidim::Proposals::UpdateProposalTaxonomiesEvent,
27
+ resource: proposal,
28
+ affected_users: proposal.notifiable_identities
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Proposals
5
+ # A command with all the business logic when a user cancels an invitation to be a coauthor.
6
+ class CancelCoauthorship < Decidim::Command
7
+ # Public: Initializes the command.
8
+ #
9
+ # proposal - The proposal to add a coauthor to.
10
+ # coauthor - The user to invite as coauthor.
11
+ def initialize(proposal, coauthor)
12
+ @proposal = proposal
13
+ @coauthor = coauthor
14
+ end
15
+
16
+ # Executes the command. Broadcasts these events:
17
+ #
18
+ # - :ok when everything is valid, together with the proposal.
19
+ # - :invalid if the coauthor is not valid.
20
+ #
21
+ # Returns nothing.
22
+ def call
23
+ return broadcast(:invalid) unless @coauthor
24
+ return broadcast(:invalid) if @proposal.authors.include?(@coauthor)
25
+
26
+ @proposal.coauthor_invitations_for(@coauthor).destroy_all
27
+
28
+ broadcast(:ok)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -63,8 +63,7 @@ module Decidim
63
63
  draft = CollaborativeDraft.new(
64
64
  title: title_with_hashtags,
65
65
  body: body_with_hashtags,
66
- category: form.category,
67
- scope: form.scope,
66
+ taxonomizations: form.taxonomizations,
68
67
  component: form.component,
69
68
  address: form.address,
70
69
  latitude: form.latitude,
@@ -82,8 +82,7 @@ module Decidim
82
82
  component: form.component
83
83
  )
84
84
 
85
- proposal.category = form.category if form.category_id.present?
86
- proposal.scope = form.scope if form.scope_id.present?
85
+ proposal.taxonomizations = form.taxonomizations if form.taxonomizations.present?
87
86
  proposal.documents = form.documents if form.documents.present?
88
87
  proposal.address = form.address if form.has_address? && !form.geocoded?
89
88
  proposal.add_coauthor(@current_user, user_group:)
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Proposals
5
+ # A command with all the business logic when a user invites a coauthor to a proposal
6
+ class InviteCoauthor < Decidim::Command
7
+ # Public: Initializes the command.
8
+ #
9
+ # proposal - The proposal to add a coauthor to.
10
+ # coauthor - The user to invite as coauthor.
11
+ def initialize(proposal, coauthor)
12
+ @proposal = proposal
13
+ @coauthor = coauthor
14
+ end
15
+
16
+ # Executes the command. Broadcasts these events:
17
+ #
18
+ # - :ok when everything is valid, together with the proposal.
19
+ # - :invalid if the coauthor is not valid.
20
+ #
21
+ # Returns nothing.
22
+ def call
23
+ return broadcast(:invalid) unless @coauthor
24
+ return broadcast(:invalid) if @proposal.authors.include?(@coauthor)
25
+
26
+ transaction do
27
+ generate_notifications
28
+ end
29
+
30
+ broadcast(:ok)
31
+ end
32
+
33
+ private
34
+
35
+ def generate_notifications
36
+ Decidim::EventsManager.publish(
37
+ event: "decidim.events.proposals.coauthor_invited",
38
+ event_class: Decidim::Proposals::CoauthorInvitedEvent,
39
+ resource: @proposal,
40
+ affected_users: [@coauthor]
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
@@ -62,7 +62,6 @@ module Decidim
62
62
  fields[:title] = { I18n.locale => parsed_title }
63
63
  fields[:body] = { I18n.locale => parsed_body }
64
64
  fields[:component] = @collaborative_draft.component
65
- fields[:scope] = @collaborative_draft.scope
66
65
  fields[:address] = @collaborative_draft.address
67
66
  fields[:published_at] = Time.current
68
67
 
@@ -78,7 +77,7 @@ module Decidim
78
77
  ) do
79
78
  new_proposal = Proposal.new(proposal_attributes)
80
79
  new_proposal.coauthorships = @collaborative_draft.coauthorships
81
- new_proposal.category = @collaborative_draft.category
80
+ new_proposal.taxonomies = @collaborative_draft.taxonomies
82
81
  new_proposal.attachments = @collaborative_draft.attachments
83
82
  new_proposal.save!
84
83
  new_proposal
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Proposals
5
+ # A command with all the business logic when a user rejects and invitation to be a proposal co-author
6
+ class RejectCoauthorship < Decidim::Command
7
+ # Public: Initializes the command.
8
+ #
9
+ # proposal - The proposal to add a coauthor to.
10
+ # coauthor - The user to invite as coauthor.
11
+ def initialize(proposal, coauthor)
12
+ @proposal = proposal
13
+ @coauthor = coauthor
14
+ end
15
+
16
+ # Executes the command. Broadcasts these events:
17
+ #
18
+ # - :ok when everything is valid, together with the proposal.
19
+ # - :invalid if the coauthor is not valid.
20
+ #
21
+ # Returns nothing.
22
+ def call
23
+ return broadcast(:invalid) unless @coauthor
24
+ return broadcast(:invalid) if @proposal.authors.include?(@coauthor)
25
+
26
+ @proposal.coauthor_invitations_for(@coauthor).destroy_all
27
+ generate_notifications
28
+
29
+ broadcast(:ok)
30
+ end
31
+
32
+ private
33
+
34
+ def generate_notifications
35
+ # notify the author that the co-author has rejected the invitation
36
+ Decidim::EventsManager.publish(
37
+ event: "decidim.events.proposals.coauthor_rejected_invite",
38
+ event_class: Decidim::Proposals::CoauthorRejectedInviteEvent,
39
+ resource: @proposal,
40
+ affected_users: @proposal.authors,
41
+ extra: { coauthor_id: @coauthor.id }
42
+ )
43
+
44
+ # notify the co-author of his own decision
45
+ Decidim::EventsManager.publish(
46
+ event: "decidim.events.proposals.rejected_coauthorship",
47
+ event_class: Decidim::Proposals::RejectedCoauthorshipEvent,
48
+ resource: @proposal,
49
+ affected_users: [@coauthor]
50
+ )
51
+ end
52
+ end
53
+ end
54
+ end
@@ -61,8 +61,7 @@ module Decidim
61
61
  {
62
62
  title: title_with_hashtags,
63
63
  body: body_with_hashtags,
64
- category: form.category,
65
- scope: form.scope,
64
+ taxonomizations: form.taxonomizations,
66
65
  address: form.address,
67
66
  latitude: form.latitude,
68
67
  longitude: form.longitude
@@ -98,8 +98,7 @@ module Decidim
98
98
  body: {
99
99
  I18n.locale => body_with_hashtags
100
100
  },
101
- category: form.category,
102
- scope: form.scope,
101
+ taxonomizations: form.taxonomizations,
103
102
  address: form.address,
104
103
  latitude: form.latitude,
105
104
  longitude: form.longitude
@@ -19,7 +19,11 @@ module Decidim
19
19
  def base_query
20
20
  return collection.order(:position) if current_component.settings.participatory_texts_enabled?
21
21
 
22
- accessible_proposals_collection
22
+ return accessible_proposals_collection unless taxonomy_order_or_search?
23
+
24
+ # this is a trick to avoid duplicates when using search in associations as suggested in:
25
+ # https://activerecord-hackery.github.io/ransack/going-further/other-notes/#problem-with-distinct-selects
26
+ accessible_proposals_collection.includes(:taxonomies).joins(:taxonomies)
23
27
  end
24
28
 
25
29
  def accessible_proposals_collection
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Decidim
6
+ module Proposals
7
+ module Admin
8
+ module NeedsInterpolations
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ def populate_interpolations(text, proposal)
13
+ return populate_string_interpolations(text, proposal) if text.is_a?(String)
14
+
15
+ populate_hash_interpolations(text, proposal)
16
+ end
17
+
18
+ def populate_hash_interpolations(hash, proposal)
19
+ return hash unless hash.is_a?(Hash)
20
+
21
+ hash.transform_values do |value|
22
+ populate_interpolations(value, proposal)
23
+ end
24
+ end
25
+
26
+ def populate_string_interpolations(value, proposal)
27
+ value = value.gsub("%{organization}", translated_attribute(proposal.organization.name))
28
+ value = value.gsub("%{name}", author_name(proposal))
29
+ value.gsub("%{admin}", current_user.name)
30
+ end
31
+
32
+ def author_name(proposal)
33
+ name = proposal.creator_author.try(:title) || proposal.creator_author.try(:name)
34
+ translated_attribute(name)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -5,6 +5,10 @@ module Decidim
5
5
  module Admin
6
6
  # This controller allows admins to answer proposals in a participatory process.
7
7
  class ProposalAnswersController < Admin::ApplicationController
8
+ include ActionView::Helpers::SanitizeHelper
9
+ include Decidim::Proposals::Admin::NeedsInterpolations
10
+ include Decidim::Proposals::Admin::Filterable
11
+
8
12
  helper_method :proposal
9
13
 
10
14
  helper Proposals::ApplicationHelper
@@ -14,13 +18,13 @@ module Decidim
14
18
 
15
19
  def edit
16
20
  enforce_permission_to(:create, :proposal_answer, proposal:)
17
- @form = form(Admin::ProposalAnswerForm).from_model(proposal)
21
+ @form = form(ProposalAnswerForm).from_model(proposal)
18
22
  end
19
23
 
20
24
  def update
21
25
  enforce_permission_to(:create, :proposal_answer, proposal:)
22
26
  @notes_form = form(ProposalNoteForm).instance
23
- @answer_form = form(Admin::ProposalAnswerForm).from_params(params)
27
+ @answer_form = form(ProposalAnswerForm).from_params(params)
24
28
 
25
29
  Admin::AnswerProposal.call(@answer_form, proposal) do
26
30
  on(:ok) do
@@ -35,6 +39,33 @@ module Decidim
35
39
  end
36
40
  end
37
41
 
42
+ def update_multiple_answers
43
+ valid_proposals = []
44
+ failed_proposals = []
45
+ proposals.each do |proposal|
46
+ proposal_answer_form = answer_form(proposal)
47
+ if allowed_to?(:create, :proposal_answer, proposal:) && proposal_answer_form.valid? && !proposal.emendation?
48
+ valid_proposals << proposal.id
49
+ ProposalAnswerJob.perform_later(proposal, proposal_answer_form.attributes, { current_organization:, current_component:, current_user: })
50
+ else
51
+ failed_proposals << proposal.id
52
+ end
53
+ end
54
+
55
+ if failed_proposals.any?
56
+ flash[:alert] =
57
+ t("proposals.answer.bulk_answer_error", scope: "decidim.proposals.admin", template: strip_tags(translated_attribute(template&.name)),
58
+ proposals: failed_proposals.join(", "))
59
+ end
60
+ if valid_proposals.any?
61
+ flash[:notice] =
62
+ I18n.t("proposals.answer.bulk_answer_success", scope: "decidim.proposals.admin", template: strip_tags(translated_attribute(template&.name)),
63
+ count: valid_proposals.count)
64
+ end
65
+
66
+ redirect_to EngineRouter.admin_proxy(current_component).root_path
67
+ end
68
+
38
69
  private
39
70
 
40
71
  def skip_manage_component_permission
@@ -44,6 +75,28 @@ module Decidim
44
75
  def proposal
45
76
  @proposal ||= Proposal.where(component: current_component).find(params[:id])
46
77
  end
78
+
79
+ def proposals
80
+ @proposals ||= Proposal.where(component: current_component).where(id: params[:proposal_ids])
81
+ end
82
+
83
+ def collection
84
+ @collection ||= Proposal.where(component: current_component).not_hidden.published
85
+ end
86
+
87
+ def template
88
+ return unless Decidim.module_installed?(:templates)
89
+
90
+ @template ||= Decidim::Templates::Template.find_by(id: params[:template][:template_id])
91
+ end
92
+
93
+ def answer_form(proposal)
94
+ form(ProposalAnswerForm).from_params(answer: populate_interpolations(template&.description, proposal), internal_state: proposal_state&.token)
95
+ end
96
+
97
+ def proposal_state
98
+ @proposal_state ||= Decidim::Proposals::ProposalState.find_by(id: template&.field_values&.dig("proposal_state_id"))
99
+ end
47
100
  end
48
101
  end
49
102
  end
@@ -7,6 +7,24 @@ module Decidim
7
7
  class ProposalNotesController < Admin::ApplicationController
8
8
  helper_method :proposal
9
9
 
10
+ def reply
11
+ enforce_permission_to(:create, :proposal_note, proposal:)
12
+ parent_note = proposal.notes.find(params[:id])
13
+ @form = form(ProposalNoteForm).from_params(params)
14
+
15
+ ReplyProposalNote.call(@form, parent_note) do
16
+ on(:ok) do
17
+ flash[:notice] = I18n.t("proposal_notes.reply.success", scope: "decidim.proposals.admin")
18
+ redirect_to proposal_path(id: proposal.id)
19
+ end
20
+
21
+ on(:invalid) do
22
+ flash.keep[:alert] = I18n.t("proposal_notes.reply.error", scope: "decidim.proposals.admin")
23
+ redirect_to proposal_path(id: proposal.id)
24
+ end
25
+ end
26
+ end
27
+
10
28
  def create
11
29
  enforce_permission_to(:create, :proposal_note, proposal:)
12
30
  @form = form(ProposalNoteForm).from_params(params)