decidim-proposals 0.29.2 → 0.30.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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 +50 -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 +23 -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 +16 -23
  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 +10 -22
  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 +214 -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,9 @@ 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
+
8
11
  helper_method :proposal
9
12
 
10
13
  helper Proposals::ApplicationHelper
@@ -14,13 +17,13 @@ module Decidim
14
17
 
15
18
  def edit
16
19
  enforce_permission_to(:create, :proposal_answer, proposal:)
17
- @form = form(Admin::ProposalAnswerForm).from_model(proposal)
20
+ @form = form(ProposalAnswerForm).from_model(proposal)
18
21
  end
19
22
 
20
23
  def update
21
24
  enforce_permission_to(:create, :proposal_answer, proposal:)
22
25
  @notes_form = form(ProposalNoteForm).instance
23
- @answer_form = form(Admin::ProposalAnswerForm).from_params(params)
26
+ @answer_form = form(ProposalAnswerForm).from_params(params)
24
27
 
25
28
  Admin::AnswerProposal.call(@answer_form, proposal) do
26
29
  on(:ok) do
@@ -35,6 +38,33 @@ module Decidim
35
38
  end
36
39
  end
37
40
 
41
+ def update_multiple_answers
42
+ valid_proposals = []
43
+ failed_proposals = []
44
+ proposals.each do |proposal|
45
+ proposal_answer_form = answer_form(proposal)
46
+ if allowed_to?(:create, :proposal_answer, proposal:) && proposal_answer_form.valid? && !proposal.emendation?
47
+ valid_proposals << proposal.id
48
+ ProposalAnswerJob.perform_later(proposal, proposal_answer_form.attributes, { current_organization:, current_component:, current_user: })
49
+ else
50
+ failed_proposals << proposal.id
51
+ end
52
+ end
53
+
54
+ if failed_proposals.any?
55
+ flash[:alert] =
56
+ t("proposals.answer.bulk_answer_error", scope: "decidim.proposals.admin", template: strip_tags(translated_attribute(template&.name)),
57
+ proposals: failed_proposals.join(", "))
58
+ end
59
+ if valid_proposals.any?
60
+ flash[:notice] =
61
+ I18n.t("proposals.answer.bulk_answer_success", scope: "decidim.proposals.admin", template: strip_tags(translated_attribute(template&.name)),
62
+ count: valid_proposals.count)
63
+ end
64
+
65
+ redirect_to EngineRouter.admin_proxy(current_component).root_path
66
+ end
67
+
38
68
  private
39
69
 
40
70
  def skip_manage_component_permission
@@ -44,6 +74,24 @@ module Decidim
44
74
  def proposal
45
75
  @proposal ||= Proposal.where(component: current_component).find(params[:id])
46
76
  end
77
+
78
+ def proposals
79
+ @proposals ||= Proposal.where(component: current_component).where(id: params[:proposal_ids])
80
+ end
81
+
82
+ def template
83
+ return unless Decidim.module_installed?(:templates)
84
+
85
+ @template ||= Decidim::Templates::Template.find_by(id: params[:template][:template_id])
86
+ end
87
+
88
+ def answer_form(proposal)
89
+ form(ProposalAnswerForm).from_params(answer: populate_interpolations(template&.description, proposal), internal_state: proposal_state&.token)
90
+ end
91
+
92
+ def proposal_state
93
+ @proposal_state ||= Decidim::Proposals::ProposalState.find_by(id: template&.field_values&.dig("proposal_state_id"))
94
+ end
47
95
  end
48
96
  end
49
97
  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)