decidim-plans 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (182) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE-AGPLv3.txt +661 -0
  3. data/README.md +157 -0
  4. data/Rakefile +48 -0
  5. data/app/assets/config/admin/decidim_plans_manifest.js +3 -0
  6. data/app/assets/config/decidim_plans_manifest.css +4 -0
  7. data/app/assets/config/decidim_plans_manifest.js +2 -0
  8. data/app/assets/images/decidim/plans/icon.svg +3 -0
  9. data/app/assets/javascripts/decidim/plans/admin/plans.js.es6 +3 -0
  10. data/app/assets/javascripts/decidim/plans/admin/sections.js.es6 +78 -0
  11. data/app/assets/javascripts/decidim/plans/multifield/component.js.es6 +101 -0
  12. data/app/assets/javascripts/decidim/plans/multifield.js.es6 +5 -0
  13. data/app/assets/javascripts/decidim/plans/plans_form.js.es6 +9 -0
  14. data/app/assets/javascripts/decidim/plans/proposal_picker.js.es6 +41 -0
  15. data/app/assets/javascripts/decidim/plans/remaining_characters.es6 +42 -0
  16. data/app/assets/javascripts/decidim/plans/social_share.js +2 -0
  17. data/app/assets/javascripts/decidim/plans/tab_focus.es6 +20 -0
  18. data/app/assets/stylesheets/decidim/plans/plans_form.scss +26 -0
  19. data/app/assets/stylesheets/decidim/plans/proposal_picker.scss +3 -0
  20. data/app/assets/stylesheets/decidim/plans/social_share.css.scss +22 -0
  21. data/app/cells/decidim/plans/author/flag.erb +5 -0
  22. data/app/cells/decidim/plans/author/withdraw.erb +6 -0
  23. data/app/cells/decidim/plans/author_cell.rb +9 -0
  24. data/app/cells/decidim/plans/coauthorships_cell.rb +23 -0
  25. data/app/cells/decidim/plans/collapsible_authors/show.erb +17 -0
  26. data/app/cells/decidim/plans/collapsible_authors_cell.rb +9 -0
  27. data/app/cells/decidim/plans/plan_activity_cell.rb +19 -0
  28. data/app/cells/decidim/plans/plan_cell.rb +59 -0
  29. data/app/cells/decidim/plans/plan_m/footer.erb +8 -0
  30. data/app/cells/decidim/plans/plan_m_cell.rb +62 -0
  31. data/app/commands/decidim/plans/accept_access_to_plan.rb +53 -0
  32. data/app/commands/decidim/plans/admin/answer_plan.rb +82 -0
  33. data/app/commands/decidim/plans/admin/create_plan.rb +102 -0
  34. data/app/commands/decidim/plans/admin/update_plan.rb +98 -0
  35. data/app/commands/decidim/plans/admin/update_sections.rb +54 -0
  36. data/app/commands/decidim/plans/attachment_methods.rb +78 -0
  37. data/app/commands/decidim/plans/create_plan.rb +90 -0
  38. data/app/commands/decidim/plans/destroy_plan.rb +33 -0
  39. data/app/commands/decidim/plans/nested_updater.rb +26 -0
  40. data/app/commands/decidim/plans/publish_plan.rb +79 -0
  41. data/app/commands/decidim/plans/reject_access_to_plan.rb +46 -0
  42. data/app/commands/decidim/plans/request_access_to_plan.rb +50 -0
  43. data/app/commands/decidim/plans/respond_to_access_request.rb +76 -0
  44. data/app/commands/decidim/plans/update_plan.rb +90 -0
  45. data/app/commands/decidim/plans/withdraw_plan.rb +50 -0
  46. data/app/controllers/concerns/decidim/plans/orderable.rb +53 -0
  47. data/app/controllers/decidim/plans/admin/application_controller.rb +16 -0
  48. data/app/controllers/decidim/plans/admin/plan_answers_controller.rb +40 -0
  49. data/app/controllers/decidim/plans/admin/plans_controller.rb +80 -0
  50. data/app/controllers/decidim/plans/admin/sections_controller.rb +75 -0
  51. data/app/controllers/decidim/plans/application_controller.rb +30 -0
  52. data/app/controllers/decidim/plans/plan_collaborator_requests_controller.rb +53 -0
  53. data/app/controllers/decidim/plans/plan_widgets_controller.rb +19 -0
  54. data/app/controllers/decidim/plans/plans_controller.rb +192 -0
  55. data/app/controllers/decidim/plans/versions_controller.rb +51 -0
  56. data/app/events/decidim/plans/accepted_plan_event.rb +9 -0
  57. data/app/events/decidim/plans/evaluating_plan_event.rb +8 -0
  58. data/app/events/decidim/plans/plan_access_accepted_event.rb +8 -0
  59. data/app/events/decidim/plans/plan_access_rejected_event.rb +8 -0
  60. data/app/events/decidim/plans/plan_access_request_event.rb +27 -0
  61. data/app/events/decidim/plans/plan_access_requested_event.rb +8 -0
  62. data/app/events/decidim/plans/plan_access_requester_accepted_event.rb +8 -0
  63. data/app/events/decidim/plans/plan_access_requester_rejected_event.rb +8 -0
  64. data/app/events/decidim/plans/publish_plan_event.rb +25 -0
  65. data/app/events/decidim/plans/rejected_plan_event.rb +9 -0
  66. data/app/forms/decidim/plans/accept_access_to_plan_form.rb +10 -0
  67. data/app/forms/decidim/plans/access_to_plan_form.rb +35 -0
  68. data/app/forms/decidim/plans/admin/plan_answer_form.rb +19 -0
  69. data/app/forms/decidim/plans/admin/plan_form.rb +88 -0
  70. data/app/forms/decidim/plans/admin/plan_sections_form.rb +20 -0
  71. data/app/forms/decidim/plans/admin/section_form.rb +34 -0
  72. data/app/forms/decidim/plans/attachment_form.rb +20 -0
  73. data/app/forms/decidim/plans/content_form.rb +58 -0
  74. data/app/forms/decidim/plans/plan_form.rb +85 -0
  75. data/app/forms/decidim/plans/reject_access_to_plan_form.rb +9 -0
  76. data/app/forms/decidim/plans/request_access_to_plan_form.rb +20 -0
  77. data/app/helpers/decidim/plans/admin/application_helper.rb +19 -0
  78. data/app/helpers/decidim/plans/application_helper.rb +78 -0
  79. data/app/helpers/decidim/plans/attached_proposals_helper.rb +44 -0
  80. data/app/helpers/decidim/plans/attachments_helper.rb +59 -0
  81. data/app/helpers/decidim/plans/cells_helper.rb +26 -0
  82. data/app/helpers/decidim/plans/plan_cells_helper.rb +67 -0
  83. data/app/helpers/decidim/plans/remaining_characters_helper.rb +32 -0
  84. data/app/helpers/decidim/plans/traceability_helper.rb +79 -0
  85. data/app/helpers/decidim/plans/user_group_helper.rb +23 -0
  86. data/app/models/concerns/decidim/plans/traceable.rb +48 -0
  87. data/app/models/decidim/plans/application_record.rb +10 -0
  88. data/app/models/decidim/plans/attached_proposal.rb +12 -0
  89. data/app/models/decidim/plans/content.rb +21 -0
  90. data/app/models/decidim/plans/paper_trail/version.rb +26 -0
  91. data/app/models/decidim/plans/paper_trail/version_association.rb +14 -0
  92. data/app/models/decidim/plans/plan.rb +206 -0
  93. data/app/models/decidim/plans/plan_collaborator_request.rb +13 -0
  94. data/app/models/decidim/plans/section.rb +16 -0
  95. data/app/permissions/decidim/plans/admin/permissions.rb +44 -0
  96. data/app/permissions/decidim/plans/permissions.rb +67 -0
  97. data/app/presenters/decidim/plans/admin_log/plan_presenter.rb +49 -0
  98. data/app/presenters/decidim/plans/admin_log/value_types/plan_state_presenter.rb +16 -0
  99. data/app/presenters/decidim/plans/content_presenter.rb +25 -0
  100. data/app/presenters/decidim/plans/log/resource_presenter.rb +18 -0
  101. data/app/presenters/decidim/plans/official_author_presenter.rb +38 -0
  102. data/app/presenters/decidim/plans/plan_presenter.rb +48 -0
  103. data/app/queries/decidim/plans/filtered_plans.rb +37 -0
  104. data/app/services/decidim/plans/diff_renderer/base.rb +108 -0
  105. data/app/services/decidim/plans/diff_renderer/categorization.rb +23 -0
  106. data/app/services/decidim/plans/diff_renderer/content.rb +33 -0
  107. data/app/services/decidim/plans/diff_renderer/plan.rb +25 -0
  108. data/app/services/decidim/plans/loggability.rb +19 -0
  109. data/app/services/decidim/plans/plan_builder.rb +68 -0
  110. data/app/services/decidim/plans/plan_search.rb +95 -0
  111. data/app/services/decidim/plans/tracer.rb +29 -0
  112. data/app/views/decidim/plans/admin/plan_answers/edit.html.erb +22 -0
  113. data/app/views/decidim/plans/admin/plans/_bulk-actions.html.erb +9 -0
  114. data/app/views/decidim/plans/admin/plans/_form.html.erb +62 -0
  115. data/app/views/decidim/plans/admin/plans/_js-callout.html.erb +6 -0
  116. data/app/views/decidim/plans/admin/plans/_plan-tr.html.erb +53 -0
  117. data/app/views/decidim/plans/admin/plans/edit.html.erb +7 -0
  118. data/app/views/decidim/plans/admin/plans/index.html.erb +64 -0
  119. data/app/views/decidim/plans/admin/plans/new.html.erb +7 -0
  120. data/app/views/decidim/plans/admin/sections/_bulk-actions.html.erb +7 -0
  121. data/app/views/decidim/plans/admin/sections/_form.html.erb +27 -0
  122. data/app/views/decidim/plans/admin/sections/_section.html.erb +66 -0
  123. data/app/views/decidim/plans/admin/sections/index.html.erb +7 -0
  124. data/app/views/decidim/plans/admin/shared/_info_plan.html.erb +17 -0
  125. data/app/views/decidim/plans/attached_proposals/_proposals.html.erb +12 -0
  126. data/app/views/decidim/plans/plan_widgets/show.html.erb +3 -0
  127. data/app/views/decidim/plans/plans/_accept_request_access_form.html.erb +8 -0
  128. data/app/views/decidim/plans/plans/_attached_proposals.html.erb +10 -0
  129. data/app/views/decidim/plans/plans/_collaborator_requests.html.erb +20 -0
  130. data/app/views/decidim/plans/plans/_content.html.erb +6 -0
  131. data/app/views/decidim/plans/plans/_contents.html.erb +8 -0
  132. data/app/views/decidim/plans/plans/_count.html.erb +1 -0
  133. data/app/views/decidim/plans/plans/_filters.html.erb +32 -0
  134. data/app/views/decidim/plans/plans/_filters_small_view.html.erb +18 -0
  135. data/app/views/decidim/plans/plans/_form.html.erb +57 -0
  136. data/app/views/decidim/plans/plans/_new_plan_button.html.erb +11 -0
  137. data/app/views/decidim/plans/plans/_plan.html.erb +1 -0
  138. data/app/views/decidim/plans/plans/_plan_preview.html.erb +1 -0
  139. data/app/views/decidim/plans/plans/_plans.html.erb +14 -0
  140. data/app/views/decidim/plans/plans/_reject_request_access_form.html.erb +10 -0
  141. data/app/views/decidim/plans/plans/_request_access_form.html.erb +7 -0
  142. data/app/views/decidim/plans/plans/edit.html.erb +21 -0
  143. data/app/views/decidim/plans/plans/index.html.erb +28 -0
  144. data/app/views/decidim/plans/plans/index.js.erb +10 -0
  145. data/app/views/decidim/plans/plans/new.html.erb +15 -0
  146. data/app/views/decidim/plans/plans/preview.html.erb +15 -0
  147. data/app/views/decidim/plans/plans/show.html.erb +133 -0
  148. data/app/views/decidim/plans/shared/_attachment_fields.html.erb +45 -0
  149. data/app/views/decidim/plans/shared/_attachments.html.erb +23 -0
  150. data/app/views/decidim/plans/shared/_remaining_characters_container.html.erb +12 -0
  151. data/app/views/decidim/plans/shared/_section_content_field.html.erb +39 -0
  152. data/app/views/decidim/plans/versions/_version.html.erb +20 -0
  153. data/app/views/decidim/plans/versions/index.html.erb +34 -0
  154. data/app/views/decidim/plans/versions/show.html.erb +41 -0
  155. data/config/locales/en.yml +341 -0
  156. data/config/locales/fi.yml +341 -0
  157. data/config/locales/sv.yml +341 -0
  158. data/db/migrate/20181230110225_enable_pg_trgm_extension_for_plans.rb +18 -0
  159. data/db/migrate/20181230110848_create_decidim_plans.rb +20 -0
  160. data/db/migrate/20181230111731_create_decidim_plans_sections.rb +13 -0
  161. data/db/migrate/20181230111931_create_decidim_plans_plan_contents.rb +14 -0
  162. data/db/migrate/20181230122035_create_decidim_plans_plan_collaborator_requests.rb +12 -0
  163. data/db/migrate/20190110125855_create_decidim_plans_attached_proposals.rb +12 -0
  164. data/db/migrate/20190129140441_add_help_to_decidim_plans_sections.rb +7 -0
  165. data/db/migrate/20190129143607_add_mandatory_to_decidim_plans_sections.rb +7 -0
  166. data/db/migrate/20190129172040_add_section_type_to_decidim_plans_sections.rb +12 -0
  167. data/db/migrate/20190129231439_add_answer_length_to_decidim_plans_sections.rb +11 -0
  168. data/db/migrate/20190202123044_create_version_associations.rb +25 -0
  169. data/db/migrate/20190202123045_add_transaction_id_column_to_versions.rb +15 -0
  170. data/db/migrate/20190202200716_add_update_token_to_decidim_plans.rb +7 -0
  171. data/lib/decidim/plans/admin.rb +10 -0
  172. data/lib/decidim/plans/admin_engine.rb +29 -0
  173. data/lib/decidim/plans/component.rb +163 -0
  174. data/lib/decidim/plans/engine.rb +43 -0
  175. data/lib/decidim/plans/locale_aware.rb +32 -0
  176. data/lib/decidim/plans/optionally_translatable_attributes.rb +85 -0
  177. data/lib/decidim/plans/paper_trail/record_trail.rb +24 -0
  178. data/lib/decidim/plans/paper_trail.rb +10 -0
  179. data/lib/decidim/plans/test/factories.rb +130 -0
  180. data/lib/decidim/plans/version.rb +8 -0
  181. data/lib/decidim/plans.rb +24 -0
  182. metadata +323 -0
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # A cell to display when a plan has been published.
6
+ class PlanActivityCell < ActivityCell
7
+ def title
8
+ I18n.t(
9
+ "decidim.plans.last_activity.new_plan_at_html",
10
+ link: participatory_space_link
11
+ )
12
+ end
13
+
14
+ def resource_link_text
15
+ Decidim::Plans::PlanPresenter.new(resource).title
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cell/partial"
4
+
5
+ module Decidim
6
+ module Plans
7
+ # This cell renders the plan card for an instance of a Plan
8
+ # the default size is the Medium Card (:m)
9
+ class PlanCell < Decidim::ViewModel
10
+ include PlanCellsHelper
11
+ include Cell::ViewModel::Partial
12
+ include Messaging::ConversationHelper
13
+
14
+ delegate :user_signed_in?, to: :parent_controller
15
+
16
+ def show
17
+ cell card_size, model, options
18
+ end
19
+
20
+ private
21
+
22
+ def current_user
23
+ context[:current_user]
24
+ end
25
+
26
+ def card_size
27
+ "decidim/plans/plan_m"
28
+ end
29
+
30
+ def resource_path
31
+ resource_locator(model).path
32
+ end
33
+
34
+ def current_participatory_space
35
+ model.component.participatory_space
36
+ end
37
+
38
+ def current_component
39
+ model.component
40
+ end
41
+
42
+ def component_name
43
+ translated_attribute model.component.name
44
+ end
45
+
46
+ def component_type_name
47
+ model.class.model_name.human
48
+ end
49
+
50
+ def participatory_space_name
51
+ translated_attribute current_participatory_space.title
52
+ end
53
+
54
+ def participatory_space_type_name
55
+ translated_attribute current_participatory_space.model_name.human
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,8 @@
1
+ <div class="card__footer">
2
+ <div class="card__support">
3
+ <div class="card__support__data"></div>
4
+ <% if has_link_to_resource? %>
5
+ <%= link_to t("decidim.plans.plans.plan.view_plan"), resource_path, class: "card__button button--sc light button small secondary" %>
6
+ <% end %>
7
+ </div>
8
+ </div>
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cell/partial"
4
+
5
+ module Decidim
6
+ module Plans
7
+ # This cell renders a plan with its M-size card.
8
+ class PlanMCell < Decidim::CardMCell
9
+ include PlanCellsHelper
10
+
11
+ def badge
12
+ render if has_badge?
13
+ end
14
+
15
+ private
16
+
17
+ def title
18
+ present(model).title
19
+ end
20
+
21
+ def body
22
+ present(model).body
23
+ end
24
+
25
+ def has_state?
26
+ model.published?
27
+ end
28
+
29
+ def has_badge?
30
+ answered? || withdrawn?
31
+ end
32
+
33
+ def has_link_to_resource?
34
+ model.published?
35
+ end
36
+
37
+ def description
38
+ model_body = strip_tags(body)
39
+
40
+ if options[:full_description]
41
+ model_body.gsub(/\n/, "<br>")
42
+ else
43
+ truncate(model_body, length: 100)
44
+ end
45
+ end
46
+
47
+ def badge_classes
48
+ return super unless options[:full_badge]
49
+
50
+ state_classes.concat(["label", "plan-status"]).join(" ")
51
+ end
52
+
53
+ def statuses
54
+ [:creation_date, :follow, :comments_count]
55
+ end
56
+
57
+ def creation_date_status
58
+ l(model.published_at.to_date, format: :decidim_short)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # A command with all the business logic to accept a user request to
6
+ # contribute to a plan.
7
+ class AcceptAccessToPlan < RespondToAccessRequest
8
+ # Executes the command. Broadcasts these events:
9
+ #
10
+ # - :ok when everything is valid.
11
+ # - :invalid if it wasn't valid and we couldn't proceed.
12
+ #
13
+ # Returns nothing.
14
+ def call
15
+ return broadcast(:invalid) if @form.invalid?
16
+ return broadcast(:invalid) if @current_user.nil?
17
+
18
+ transaction do
19
+ @plan.requesters.delete @requester_user
20
+
21
+ Decidim::Coauthorship.create(
22
+ coauthorable: @plan,
23
+ author: @requester_user
24
+ )
25
+ end
26
+
27
+ notify_plan_requester
28
+ notify_plan_authors
29
+ broadcast(:ok, @requester_user)
30
+ end
31
+
32
+ def recipient_ids
33
+ @plan.authors.pluck(:id) - [@requester_user.id]
34
+ end
35
+
36
+ def authors_event
37
+ "decidim.events.plans.plan_access_accepted"
38
+ end
39
+
40
+ def authors_event_class
41
+ Decidim::Plans::PlanAccessAcceptedEvent
42
+ end
43
+
44
+ def requester_event
45
+ "decidim.events.plans.plan_access_requester_accepted"
46
+ end
47
+
48
+ def requester_event_class
49
+ Decidim::Plans::PlanAccessRequesterAcceptedEvent
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ module Admin
6
+ # A command with all the business logic when an admin answers a plan.
7
+ class AnswerPlan < Rectify::Command
8
+ # Public: Initializes the command.
9
+ #
10
+ # form - A form object with the params.
11
+ # plan - The plan to write the answer for.
12
+ def initialize(form, plan)
13
+ @form = form
14
+ @plan = plan
15
+ end
16
+
17
+ # Executes the command. Broadcasts these events:
18
+ #
19
+ # - :ok when everything is valid.
20
+ # - :invalid if the form wasn't valid and we couldn't proceed.
21
+ #
22
+ # Returns nothing.
23
+ def call
24
+ return broadcast(:invalid) if form.invalid?
25
+
26
+ answer_plan
27
+ notify_followers
28
+
29
+ broadcast(:ok)
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :form, :plan
35
+
36
+ def answer_plan
37
+ Decidim.traceability.perform_action!(
38
+ "answer",
39
+ plan,
40
+ form.current_user
41
+ ) do
42
+ plan.update!(
43
+ state: @form.state,
44
+ answer: @form.answer,
45
+ answered_at: Time.current
46
+ )
47
+ end
48
+ end
49
+
50
+ def notify_followers
51
+ return if (plan.previous_changes.keys & %w(state)).empty?
52
+
53
+ if plan.accepted?
54
+ publish_event(
55
+ "decidim.events.plans.plan_accepted",
56
+ Decidim::Plans::AcceptedPlanEvent
57
+ )
58
+ elsif plan.rejected?
59
+ publish_event(
60
+ "decidim.events.plans.plan_rejected",
61
+ Decidim::Plans::RejectedPlanEvent
62
+ )
63
+ elsif plan.evaluating?
64
+ publish_event(
65
+ "decidim.events.plans.plan_evaluating",
66
+ Decidim::Plans::EvaluatingPlanEvent
67
+ )
68
+ end
69
+ end
70
+
71
+ def publish_event(event, event_class)
72
+ Decidim::EventsManager.publish(
73
+ event: event,
74
+ event_class: event_class,
75
+ resource: plan,
76
+ recipient_ids: plan.followers.pluck(:id)
77
+ )
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ module Admin
6
+ # A command with all the business logic when a user creates a new plan.
7
+ class CreatePlan < Rectify::Command
8
+ include AttachmentMethods
9
+
10
+ # Public: Initializes the command.
11
+ #
12
+ # form - A form object with the params.
13
+ def initialize(form)
14
+ @form = form
15
+ end
16
+
17
+ # Executes the command. Broadcasts these events:
18
+ #
19
+ # - :ok when everything is valid, together with the plan.
20
+ # - :invalid if the form wasn't valid and we couldn't proceed.
21
+ #
22
+ # Returns nothing.
23
+ def call
24
+ if process_attachments?
25
+ return broadcast(:invalid) if attachments_invalid?
26
+ end
27
+
28
+ if form.invalid?
29
+ mark_attachment_reattachment
30
+ return broadcast(:invalid)
31
+ end
32
+
33
+ Decidim::Plans.tracer.trace!(@form.current_user) do
34
+ transaction do
35
+ create_plan
36
+ create_plan_contents
37
+ update_attachments if process_attachments?
38
+ send_notification
39
+ end
40
+ end
41
+
42
+ broadcast(:ok, plan)
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :form, :plan, :attachment
48
+
49
+ def create_plan
50
+ @plan = Decidim::Plans.loggability.perform_action!(
51
+ :create,
52
+ Plan,
53
+ @form.current_user
54
+ ) do
55
+ plan = Plan.new(
56
+ { published_at: Time.current }.merge(attributes)
57
+ )
58
+ plan.coauthorships.build(author: form.author)
59
+ plan.save!
60
+ plan.proposals << form.proposals
61
+ plan
62
+ end
63
+
64
+ @attached_to = @plan
65
+ end
66
+
67
+ def create_plan_contents
68
+ @form.contents.each do |content|
69
+ @plan.contents.create!(
70
+ body: content.body,
71
+ section: content.section,
72
+ user: form.current_user
73
+ )
74
+ end
75
+ end
76
+
77
+ def attributes
78
+ {
79
+ title: form.title,
80
+ category: form.category,
81
+ scope: form.scope,
82
+ component: form.component,
83
+ state: "open",
84
+ published_at: Time.current
85
+ }
86
+ end
87
+
88
+ def send_notification
89
+ Decidim::EventsManager.publish(
90
+ event: "decidim.events.plans.plan_published",
91
+ event_class: Decidim::Plans::PublishPlanEvent,
92
+ resource: plan,
93
+ recipient_ids: @plan.participatory_space.followers.pluck(:id),
94
+ extra: {
95
+ participatory_space: true
96
+ }
97
+ )
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ module Admin
6
+ # A command with all the business logic when a user updates a plan.
7
+ class UpdatePlan < Rectify::Command
8
+ include AttachmentMethods
9
+ include NestedUpdater
10
+
11
+ # Public: Initializes the command.
12
+ #
13
+ # form - A form object with the params.
14
+ # plan - the plan to update.
15
+ def initialize(form, plan)
16
+ @form = form
17
+ @plan = plan
18
+ @attached_to = plan
19
+ end
20
+
21
+ # Executes the command. Broadcasts these events:
22
+ #
23
+ # - :ok when everything is valid, together with the plan.
24
+ # - :invalid if the form wasn't valid and we couldn't proceed.
25
+ #
26
+ # Returns nothing.
27
+ def call
28
+ return broadcast(:invalid) if form.invalid?
29
+
30
+ if process_attachments?
31
+ prepare_attachments
32
+ return broadcast(:invalid) if attachments_invalid?
33
+ end
34
+
35
+ if form.invalid?
36
+ mark_attachment_reattachment
37
+ return broadcast(:invalid)
38
+ end
39
+
40
+ Decidim::Plans.tracer.trace!(form.current_user) do
41
+ transaction do
42
+ update_plan
43
+ update_plan_contents
44
+ update_plan_author
45
+ update_attachments if process_attachments?
46
+ end
47
+ end
48
+
49
+ broadcast(:ok, plan)
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :form, :plan, :attachment
55
+
56
+ def update_plan
57
+ Decidim::Plans.loggability.update!(
58
+ plan,
59
+ form.current_user,
60
+ attributes
61
+ )
62
+ end
63
+
64
+ def update_plan_contents
65
+ @form.contents.each do |content|
66
+ update_nested_model(
67
+ content,
68
+ { body: content.body,
69
+ section: content.section,
70
+ user: @form.current_user },
71
+ @plan.contents
72
+ )
73
+ end
74
+ end
75
+
76
+ def update_plan_author
77
+ plan.coauthorships.clear
78
+ plan.add_coauthor(form.author)
79
+ plan.save!
80
+ plan
81
+ end
82
+
83
+ def attributes
84
+ {
85
+ title: form.title,
86
+ category: form.category,
87
+ scope: form.scope,
88
+ proposals: form.proposals,
89
+ # The update token ensures a new version is always created even if
90
+ # the other attributes have not changed. This is needed to force a
91
+ # new version to show the changes to associated models.
92
+ update_token: Time.now.to_f
93
+ }
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ module Admin
6
+ # This command is executed when the user modifies sections from
7
+ # the admin panel.
8
+ class UpdateSections < Rectify::Command
9
+ include NestedUpdater
10
+
11
+ # Initializes a UpdateSections Command.
12
+ #
13
+ # form - The form from which to get the data.
14
+ # sections - The current set of the sections to be updated.
15
+ def initialize(form, sections)
16
+ @form = form
17
+ @sections = sections
18
+ end
19
+
20
+ # Updates the sections if valid.
21
+ #
22
+ # Broadcasts :ok if successful, :invalid otherwise.
23
+ def call
24
+ return broadcast(:invalid) if @form.invalid?
25
+
26
+ Decidim::Plans::Section.transaction do
27
+ update_sections
28
+ end
29
+
30
+ broadcast(:ok)
31
+ end
32
+
33
+ private
34
+
35
+ def update_sections
36
+ @form.sections.each do |form_section|
37
+ update_nested_model(
38
+ form_section,
39
+ {
40
+ body: form_section.body,
41
+ help: form_section.help,
42
+ mandatory: form_section.mandatory,
43
+ answer_length: form_section.answer_length,
44
+ position: form_section.position,
45
+ section_type: form_section.section_type
46
+ },
47
+ @sections
48
+ )
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # A module with all the attachment methods for plan commands.
6
+ module AttachmentMethods
7
+ include NestedUpdater
8
+
9
+ private
10
+
11
+ def prepare_attachments
12
+ @form.attachments.each do |atform|
13
+ next if atform.file.present? || atform.id.blank?
14
+
15
+ attachment = @plan.attachments.find_by(id: atform.id)
16
+ atform.file = attachment.file if attachment
17
+ end
18
+ end
19
+
20
+ def mark_attachment_reattachment
21
+ if @form.invalid? || @form.errors.any?
22
+ @form.attachments.each do |at|
23
+ at.errors.add(:file, :needs_to_be_reattached) if at.present? && at.id.blank?
24
+ end
25
+ end
26
+ end
27
+
28
+ def attachments_invalid?
29
+ @form.attachments.each do |atform|
30
+ next if atform.deleted?
31
+
32
+ attachment = Attachment.new(attachment_params(atform))
33
+
34
+ next if attachment.valid? || !attachment.errors.has_key?(:file)
35
+
36
+ atform.errors.add :file, attachment.errors[:file]
37
+ end
38
+
39
+ @form.attachments.any? { |at| at.errors.any? }
40
+ end
41
+
42
+ def attachments_present?
43
+ attachments_allowed? && @form.attachments.any? do |at|
44
+ at.title.present? || at.file.present? || at.id.present?
45
+ end
46
+ end
47
+
48
+ def update_attachments
49
+ @form.attachments.each do |attachment|
50
+ update_nested_model(
51
+ attachment,
52
+ attachment_params(attachment),
53
+ @plan.attachments
54
+ )
55
+ end
56
+ end
57
+
58
+ def attachments_allowed?
59
+ @form.current_component.settings.attachments_allowed?
60
+ end
61
+
62
+ def process_attachments?
63
+ attachments_allowed? && attachments_present?
64
+ end
65
+
66
+ def attachment_params(form)
67
+ params = {
68
+ weight: form.weight,
69
+ title: form.title,
70
+ attached_to: @attached_to
71
+ }
72
+ params[:file] = form.file if form.file.present?
73
+
74
+ params
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # A command with all the business logic when a user creates a new plan.
6
+ class CreatePlan < Rectify::Command
7
+ include AttachmentMethods
8
+ # Public: Initializes the command.
9
+ #
10
+ # form - A form object with the params.
11
+ # current_user - The current user.
12
+ def initialize(form, current_user)
13
+ @form = form
14
+ @current_user = current_user
15
+ @attached_to = nil
16
+ end
17
+
18
+ # Executes the command. Broadcasts these events:
19
+ #
20
+ # - :ok when everything is valid, together with the plan.
21
+ # - :invalid if the form wasn't valid and we couldn't proceed.
22
+ #
23
+ # Returns nothing.
24
+ def call
25
+ if process_attachments?
26
+ return broadcast(:invalid) if attachments_invalid?
27
+ end
28
+
29
+ if form.invalid?
30
+ mark_attachment_reattachment
31
+ return broadcast(:invalid)
32
+ end
33
+
34
+ Decidim::Plans.tracer.trace!(@form.current_user) do
35
+ transaction do
36
+ create_plan
37
+ create_plan_contents
38
+ update_attachments if process_attachments?
39
+ end
40
+ end
41
+
42
+ broadcast(:ok, plan)
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :form, :plan, :attachments
48
+
49
+ def create_plan
50
+ @plan = Decidim::Plans.loggability.perform_action!(
51
+ :create,
52
+ Plan,
53
+ @form.current_user
54
+ ) do
55
+ plan = Plan.new(
56
+ title: form.title,
57
+ category: form.category,
58
+ scope: form.scope,
59
+ component: form.component,
60
+ state: "open"
61
+ )
62
+ plan.coauthorships.build(author: @current_user, user_group: @form.user_group)
63
+ plan.save!
64
+ plan.proposals << form.proposals
65
+ plan
66
+ end
67
+
68
+ @attached_to = @plan
69
+ end
70
+
71
+ def create_plan_contents
72
+ @form.contents.each do |content|
73
+ @plan.contents.create!(
74
+ body: content.body,
75
+ section: content.section,
76
+ user: @current_user
77
+ )
78
+ end
79
+ end
80
+
81
+ def user_group
82
+ @user_group ||= Decidim::UserGroup.find_by(organization: organization, id: form.user_group_id)
83
+ end
84
+
85
+ def organization
86
+ @organization ||= @current_user.organization
87
+ end
88
+ end
89
+ end
90
+ end