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,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # A command with all the business logic when a user destroys a draft plan.
6
+ class DestroyPlan < Rectify::Command
7
+ # Public: Initializes the command.
8
+ #
9
+ # plan - The plan to destroy.
10
+ # current_user - The current user.
11
+ def initialize(plan, current_user)
12
+ @plan = plan
13
+ @current_user = current_user
14
+ end
15
+
16
+ # Executes the command. Broadcasts these events:
17
+ #
18
+ # - :ok when everything is valid and the plan is deleted.
19
+ # - :invalid if the plan is not a draft.
20
+ # - :invalid if the plan's author is not the current user.
21
+ #
22
+ # Returns nothing.
23
+ def call
24
+ return broadcast(:invalid) unless @plan.draft?
25
+ return broadcast(:invalid) unless @plan.authored_by?(@current_user)
26
+
27
+ @plan.destroy!
28
+
29
+ broadcast(:ok, @plan)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # Updater for nested models.
6
+ module NestedUpdater
7
+ private
8
+
9
+ def update_nested_model(form, attributes, parent_association)
10
+ record = parent_association.find_by(id: form.id) || parent_association.build(attributes)
11
+
12
+ yield record if block_given?
13
+
14
+ if record.persisted?
15
+ if form.deleted?
16
+ record.destroy!
17
+ else
18
+ record.update!(attributes)
19
+ end
20
+ elsif !form.deleted?
21
+ record.save!
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,79 @@
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 PublishPlan < Rectify::Command
7
+ # Public: Initializes the command.
8
+ #
9
+ # plan - The plan to publish.
10
+ # current_user - The current user.
11
+ def initialize(plan, current_user)
12
+ @plan = plan
13
+ @current_user = current_user
14
+ end
15
+
16
+ # Executes the command. Broadcasts these events:
17
+ #
18
+ # - :ok when everything is valid and the plan is published.
19
+ # - :invalid if the plan's author is not the current user.
20
+ #
21
+ # Returns nothing.
22
+ def call
23
+ return broadcast(:invalid) unless @plan.authored_by?(@current_user)
24
+
25
+ transaction do
26
+ publish_plan
27
+ send_notification
28
+ send_notification_to_participatory_space
29
+ end
30
+
31
+ broadcast(:ok, @plan)
32
+ end
33
+
34
+ private
35
+
36
+ def publish_plan
37
+ Decidim.traceability.perform_action!(
38
+ "publish",
39
+ @plan,
40
+ @current_user,
41
+ visibility: "public-only"
42
+ ) do
43
+ @plan.update published_at: Time.current
44
+ end
45
+ end
46
+
47
+ def send_notification
48
+ return if @plan.coauthorships.empty?
49
+
50
+ Decidim::EventsManager.publish(
51
+ event: "decidim.events.plans.plan_published",
52
+ event_class: Decidim::Plans::PublishPlanEvent,
53
+ resource: @plan,
54
+ recipient_ids: coauthors_followers
55
+ )
56
+ end
57
+
58
+ def send_notification_to_participatory_space
59
+ Decidim::EventsManager.publish(
60
+ event: "decidim.events.plans.plan_published",
61
+ event_class: Decidim::Plans::PublishPlanEvent,
62
+ resource: @plan,
63
+ recipient_ids: @plan.participatory_space.followers.pluck(:id) - coauthors_followers,
64
+ extra: {
65
+ participatory_space: true
66
+ }
67
+ )
68
+ end
69
+
70
+ def coauthors_followers
71
+ followers_ids = []
72
+ @plan.authors.each do |author|
73
+ followers_ids += author.followers.pluck(:id)
74
+ end
75
+ followers_ids
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # A command with all the business logic to reject a user request to
6
+ # contribute to a plan.
7
+ class RejectAccessToPlan < 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
+ @plan.requesters.delete @requester_user
19
+
20
+ notify_plan_requester
21
+ notify_plan_authors
22
+ broadcast(:ok, @requester_user)
23
+ end
24
+
25
+ def recipient_ids
26
+ @plan.authors.pluck(:id)
27
+ end
28
+
29
+ def authors_event
30
+ "decidim.events.plans.plan_access_rejected"
31
+ end
32
+
33
+ def authors_event_class
34
+ Decidim::Plans::PlanAccessRejectedEvent
35
+ end
36
+
37
+ def requester_event
38
+ "decidim.events.plans.plan_access_requester_rejected"
39
+ end
40
+
41
+ def requester_event_class
42
+ Decidim::Plans::PlanAccessRequesterRejectedEvent
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # A command with all the business logic when a user requests
6
+ # access to edit a plan.
7
+ class RequestAccessToPlan < Rectify::Command
8
+ # Public: Initializes the command.
9
+ #
10
+ # form - A form object with the params.
11
+ # plan - A Decidim::Plans::Plan object.
12
+ # current_user - The current user and requester user
13
+ def initialize(form, current_user)
14
+ @form = form
15
+ @plan = form.plan
16
+ @current_user = current_user
17
+ end
18
+
19
+ # Executes the command. Broadcasts these events:
20
+ #
21
+ # - :ok when everything is valid.
22
+ # - :invalid if it wasn't valid and we couldn't proceed.
23
+ #
24
+ # Returns nothing.
25
+ def call
26
+ return broadcast(:invalid) if @form.invalid?
27
+ return broadcast(:invalid) if @current_user.nil?
28
+
29
+ @plan.collaborator_requests.create!(user: @current_user)
30
+ notify_plan_authors
31
+ broadcast(:ok, @plan)
32
+ end
33
+
34
+ private
35
+
36
+ def notify_plan_authors
37
+ recipient_ids = @plan.authors.pluck(:id)
38
+ Decidim::EventsManager.publish(
39
+ event: "decidim.events.plans.plan_access_requested",
40
+ event_class: Decidim::Plans::PlanAccessRequestedEvent,
41
+ resource: @plan,
42
+ recipient_ids: recipient_ids.uniq,
43
+ extra: {
44
+ requester_id: @current_user.id
45
+ }
46
+ )
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # Common functionality for {Accept,Reject}AccessToPlan.
6
+ class RespondToAccessRequest < Rectify::Command
7
+ # Public: Initializes the command.
8
+ #
9
+ # form - A form object with the params.
10
+ # plan - A Decidim::Plans::Plan object.
11
+ # current_user - The current user.
12
+ # requester_user - The user that requested to collaborate.
13
+ def initialize(form, current_user)
14
+ @form = form
15
+ @plan = form.plan
16
+ @current_user = current_user
17
+ @requester_user = form.requester_user
18
+ end
19
+
20
+ # Executes the command. Broadcasts these events:
21
+ #
22
+ # - :ok when everything is valid.
23
+ # - :invalid if it wasn't valid and we couldn't proceed.
24
+ #
25
+ # Returns nothing.
26
+ def call
27
+ return broadcast(:invalid) if @form.invalid?
28
+ return broadcast(:invalid) if @current_user.nil?
29
+
30
+ transaction do
31
+ @plan.requesters.delete @requester_user
32
+
33
+ Decidim::Coauthorship.create(
34
+ coauthorable: @plan,
35
+ author: @requester_user
36
+ )
37
+ end
38
+
39
+ notify_plan_requester
40
+ notify_plan_authors
41
+ broadcast(:ok, @requester_user)
42
+ end
43
+
44
+ def notify_plan_authors
45
+ Decidim::EventsManager.publish(
46
+ event: authors_event,
47
+ event_class: authors_event_class,
48
+ resource: @plan,
49
+ recipient_ids: recipient_ids.uniq,
50
+ extra: {
51
+ requester_id: @requester_user.id
52
+ }
53
+ )
54
+ end
55
+
56
+ def notify_plan_requester
57
+ Decidim::EventsManager.publish(
58
+ event: requester_event,
59
+ event_class: requester_event_class,
60
+ resource: @plan,
61
+ recipient_ids: [@requester_user.id]
62
+ )
63
+ end
64
+
65
+ def recipient_ids; end
66
+
67
+ def authors_event; end
68
+
69
+ def authors_event_class; end
70
+
71
+ def requester_event; end
72
+
73
+ def requester_event_class; end
74
+ end
75
+ end
76
+ 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 updates a plan.
6
+ class UpdatePlan < Rectify::Command
7
+ include AttachmentMethods
8
+ include NestedUpdater
9
+
10
+ # Public: Initializes the command.
11
+ #
12
+ # form - A form object with the params.
13
+ # current_user - The current user.
14
+ # plan - the plan to update.
15
+ def initialize(form, current_user, plan)
16
+ @form = form
17
+ @current_user = current_user
18
+ @plan = plan
19
+ @attached_to = plan
20
+ end
21
+
22
+ # Executes the command. Broadcasts these events:
23
+ #
24
+ # - :ok when everything is valid, together with the plan.
25
+ # - :invalid if the form wasn't valid and we couldn't proceed.
26
+ #
27
+ # Returns nothing.
28
+ def call
29
+ return broadcast(:invalid) unless plan.editable_by?(current_user)
30
+
31
+ if process_attachments?
32
+ prepare_attachments
33
+ return broadcast(:invalid) if attachments_invalid?
34
+ end
35
+
36
+ if form.invalid?
37
+ mark_attachment_reattachment
38
+ return broadcast(:invalid)
39
+ end
40
+
41
+ Decidim::Plans.tracer.trace!(@current_user) do
42
+ transaction do
43
+ update_plan
44
+ update_plan_contents
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, :current_user, :attachments
55
+
56
+ def update_plan
57
+ Decidim::Plans.loggability.update!(
58
+ @plan,
59
+ @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 attributes
77
+ {
78
+ title: @form.title,
79
+ category: @form.category,
80
+ scope: @form.scope,
81
+ proposals: @form.proposals,
82
+ # The update token ensures a new version is always created even if
83
+ # the other attributes have not changed. This is needed to force a new
84
+ # version to show the changes to associated models.
85
+ update_token: Time.now.to_f
86
+ }
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # A command with all the business logic when a user withdraws a plan.
6
+ class WithdrawPlan < Rectify::Command
7
+ # Public: Initializes the command.
8
+ #
9
+ # plan - The plan to withdraw.
10
+ # current_user - The current user.
11
+ def initialize(plan, current_user)
12
+ @plan = plan
13
+ @current_user = current_user
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 proposal already has supports or does not belong to current user.
20
+ #
21
+ # Returns nothing.
22
+ def call
23
+ # return broadcast(:invalid) if @plan.votes.any?
24
+
25
+ transaction do
26
+ change_plan_state_to_withdrawn
27
+ # reject_emendations_if_any # 0.16+
28
+ end
29
+
30
+ broadcast(:ok, @plan)
31
+ end
32
+
33
+ private
34
+
35
+ def change_plan_state_to_withdrawn
36
+ @plan.update state: "withdrawn"
37
+ end
38
+
39
+ def reject_emendations_if_any
40
+ return if @plan.emendations.empty?
41
+
42
+ @plan.emendations.each do |emendation|
43
+ @form = form(Decidim::Amendable::RejectForm).from_params(id: emendation.amendment.id)
44
+ result = Decidim::Amendable::Reject.call(@form)
45
+ return result[:ok] if result[:ok]
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Decidim
6
+ module Plans
7
+ # Common logic to ordering resources
8
+ module Orderable
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ helper_method :order, :available_orders, :random_seed
13
+
14
+ private
15
+
16
+ # Gets how the plans should be ordered based on the choice made by the user.
17
+ def order
18
+ @order ||= detect_order(params[:order]) || default_order
19
+ end
20
+
21
+ # Available orders based on enabled settings
22
+ def available_orders
23
+ @available_orders ||= begin
24
+ available_orders = %w(random recent)
25
+ available_orders
26
+ end
27
+ end
28
+
29
+ def default_order
30
+ "random"
31
+ end
32
+
33
+ # Returns: A random float number between -1 and 1 to be used as a random seed at the database.
34
+ def random_seed
35
+ @random_seed ||= (params[:random_seed] ? params[:random_seed].to_f : (rand * 2 - 1))
36
+ end
37
+
38
+ def detect_order(candidate)
39
+ available_orders.detect { |order| order == candidate }
40
+ end
41
+
42
+ def reorder(plans)
43
+ case order
44
+ when "random"
45
+ plans.order_randomly(random_seed)
46
+ when "recent"
47
+ plans.order(created_at: :desc)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ module Admin
6
+ # This controller is the abstract class from which all other controllers of
7
+ # this engine inherit.
8
+ #
9
+ # Note that it inherits from `Decidim::Admin::Components::BaseController`, which
10
+ # override its layout and provide all kinds of useful methods.
11
+ class ApplicationController < Decidim::Admin::Components::BaseController
12
+ helper Decidim::ApplicationHelper
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ module Admin
6
+ # This controller allows admins to answer plans in a participatory process.
7
+ class PlanAnswersController < Admin::ApplicationController
8
+ helper_method :plan
9
+
10
+ def edit
11
+ enforce_permission_to :create, :plan_answer
12
+ @form = form(Admin::PlanAnswerForm).from_model(plan)
13
+ end
14
+
15
+ def update
16
+ enforce_permission_to :create, :plan_answer
17
+ @form = form(Admin::PlanAnswerForm).from_params(params)
18
+
19
+ Admin::AnswerPlan.call(@form, plan) do
20
+ on(:ok) do
21
+ flash[:notice] = I18n.t("plans.answer.success", scope: "decidim.plans.admin")
22
+ redirect_to plans_path
23
+ end
24
+
25
+ on(:invalid) do
26
+ flash.now[:alert] = I18n.t("plans.answer.invalid", scope: "decidim.plans.admin")
27
+ render action: "edit"
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def plan
35
+ @plan ||= Plan.where(component: current_component).find(params[:id])
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ module Admin
6
+ # This controller allows admins to manage plans in a participatory process.
7
+ class PlansController < Admin::ApplicationController
8
+ include Decidim::ApplicationHelper
9
+ include Plans::AttachedProposalsHelper
10
+
11
+ helper Plans::ApplicationHelper
12
+ helper Plans::AttachmentsHelper
13
+ helper Plans::RemainingCharactersHelper
14
+ helper_method :plans, :query, :form_presenter, :attached_proposals_picker_field
15
+
16
+ def new
17
+ enforce_permission_to :create, :plans
18
+ @form = form(Admin::PlanForm).from_model(Plan.new(component: current_component))
19
+ end
20
+
21
+ def create
22
+ enforce_permission_to :create, :plan
23
+ @form = form(Admin::PlanForm).from_params(params)
24
+
25
+ Admin::CreatePlan.call(@form) do
26
+ on(:ok) do
27
+ flash[:notice] = I18n.t("plans.create.success", scope: "decidim")
28
+ redirect_to plans_path
29
+ end
30
+
31
+ on(:invalid) do
32
+ flash.now[:alert] = I18n.t("plans.create.invalid", scope: "decidim")
33
+ render action: "new"
34
+ end
35
+ end
36
+ end
37
+
38
+ def edit
39
+ enforce_permission_to :edit, :plan, plan: plan
40
+ @form = form(Admin::PlanForm).from_model(plan)
41
+ end
42
+
43
+ def update
44
+ enforce_permission_to :edit, :plan, plan: plan
45
+
46
+ @form = form(Admin::PlanForm).from_params(params)
47
+ Admin::UpdatePlan.call(@form, @plan) do
48
+ on(:ok) do
49
+ flash[:notice] = I18n.t("plans.update.success", scope: "decidim")
50
+ redirect_to plans_path
51
+ end
52
+
53
+ on(:invalid) do
54
+ flash.now[:alert] = I18n.t("plans.update.invalid", scope: "decidim")
55
+ render :edit
56
+ end
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def query
63
+ @query ||= Plan.where(component: current_component).ransack(params[:q])
64
+ end
65
+
66
+ def plans
67
+ @plans ||= query.result.page(params[:page]).per(15)
68
+ end
69
+
70
+ def plan
71
+ @plan ||= Plan.where(component: current_component).find(params[:id])
72
+ end
73
+
74
+ def form_presenter
75
+ @form_presenter ||= present(@form, presenter_class: Decidim::Plans::PlanPresenter)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end