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,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ module Log
6
+ class ResourcePresenter < Decidim::Log::ResourcePresenter
7
+ private
8
+
9
+ # Private: Presents resource name.
10
+ #
11
+ # Returns an HTML-safe String.
12
+ def present_resource_name
13
+ Decidim::Plans::PlanPresenter.new(resource).title if resource
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ #
6
+ # A dummy presenter to abstract out the author of an official plan.
7
+ #
8
+ class OfficialAuthorPresenter
9
+ def name
10
+ I18n.t("decidim.plans.models.plan.fields.official_plan")
11
+ end
12
+
13
+ def nickname
14
+ ""
15
+ end
16
+
17
+ def badge
18
+ ""
19
+ end
20
+
21
+ def profile_path
22
+ ""
23
+ end
24
+
25
+ def avatar_url
26
+ ActionController::Base.helpers.asset_path("decidim/default-avatar.svg")
27
+ end
28
+
29
+ def deleted?
30
+ false
31
+ end
32
+
33
+ def can_be_contacted?
34
+ false
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ #
6
+ # Decorator for plans
7
+ #
8
+ class PlanPresenter < SimpleDelegator
9
+ include Rails.application.routes.mounted_helpers
10
+ include ActionView::Helpers::UrlHelper
11
+ include TranslatableAttributes
12
+
13
+ def author
14
+ coauthorship = coauthorships.first
15
+ @author ||= if coauthorship.user_group
16
+ Decidim::UserGroupPresenter.new(coauthorship.user_group)
17
+ else
18
+ Decidim::UserPresenter.new(coauthorship.author)
19
+ end
20
+ end
21
+
22
+ def plan
23
+ __getobj__
24
+ end
25
+
26
+ def plan_path
27
+ Decidim::ResourceLocatorPresenter.new(plan).path
28
+ end
29
+
30
+ def title
31
+ translated_attribute(plan.title).html_safe
32
+ end
33
+
34
+ def body
35
+ fields = plan.sections.map do |section|
36
+ content = plan.contents.find_by(section: section)
37
+ next if content.nil?
38
+
39
+ title = translated_attribute(content.title)
40
+ body = translated_attribute(content.body)
41
+ "<dt>#{title}</dt> <dd>#{body}</dd>"
42
+ end
43
+
44
+ "<dl>#{fields.join("\n")}</dl>".html_safe
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # A class used to find plans filtered by components and a date range
6
+ class FilteredPlans < Rectify::Query
7
+ # Syntactic sugar to initialize the class and return the queried objects.
8
+ #
9
+ # components - An array of Decidim::Component
10
+ # start_at - A date to filter resources created after it
11
+ # end_at - A date to filter resources created before it.
12
+ def self.for(components, start_at = nil, end_at = nil)
13
+ new(components, start_at, end_at).query
14
+ end
15
+
16
+ # Initializes the class.
17
+ #
18
+ # components - An array of Decidim::Component
19
+ # start_at - A date to filter resources created after it
20
+ # end_at - A date to filter resources created before it.
21
+ def initialize(components, start_at = nil, end_at = nil)
22
+ @components = components
23
+ @start_at = start_at
24
+ @end_at = end_at
25
+ end
26
+
27
+ # Finds the Plans scoped to an array of components and filtered
28
+ # by a range of dates.
29
+ def query
30
+ plans = Decidim::Plans::Plan.where(component: @components)
31
+ plans = plans.where("created_at >= ?", @start_at) if @start_at.present?
32
+ plans = plans.where("created_at <= ?", @end_at) if @end_at.present?
33
+ plans
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ module DiffRenderer
6
+ class Base
7
+ include ::Decidim::ApplicationHelper
8
+
9
+ def initialize(version, locale)
10
+ @version = version
11
+ @display_locale = locale
12
+ end
13
+
14
+ # Renders the diff of the given changeset. Doesn't take into account
15
+ # translatable fields.
16
+ #
17
+ # Returns a Hash, where keys are the fields that have changed and values
18
+ # are an array, the first element being the previous value and the last
19
+ # being the new one.
20
+ def diff
21
+ version.changeset.inject({}) do |diff, (attribute, values)|
22
+ attribute = attribute.to_sym
23
+ type = attribute_types[attribute]
24
+
25
+ if type.blank?
26
+ diff
27
+ elsif [:i18n, :i18n_html].include?(type)
28
+ parse_i18n_changeset(attribute, values, type, diff)
29
+ else
30
+ parse_changeset(attribute, values, type, diff)
31
+ end
32
+ end
33
+ end
34
+
35
+ protected
36
+
37
+ attr_reader :version, :display_locale
38
+
39
+ def i18n_scope
40
+ "activemodel.attributes"
41
+ end
42
+
43
+ def generate_label(attribute)
44
+ I18n.t(attribute, scope: i18n_scope)
45
+ end
46
+
47
+ def generate_i18n_label(attribute, locale)
48
+ label = I18n.t(attribute, scope: i18n_scope)
49
+ return label if display_locale
50
+ "#{label} (#{locale_name(locale)})"
51
+ end
52
+
53
+ def locale_name(locale)
54
+ if I18n.available_locales.include?(locale.to_sym)
55
+ I18n.t("locale.name", locale: locale)
56
+ else
57
+ locale
58
+ end
59
+ end
60
+
61
+ def translated_attribute(attribute, locale = nil)
62
+ return "" if attribute.nil?
63
+ return attribute unless attribute.is_a?(Hash)
64
+
65
+ attribute = attribute.dup.stringify_keys
66
+ locale ||= I18n.locale.to_s
67
+
68
+ attribute[locale].presence ||
69
+ attribute[I18n.locale.to_s].presence ||
70
+ attribute[attribute.keys.first].presence ||
71
+ ""
72
+ end
73
+
74
+ def parse_changeset(attribute, values, type, diff)
75
+ diff.update(
76
+ attribute => {
77
+ type: type,
78
+ label: generate_label(attribute),
79
+ old_value: values[0],
80
+ new_value: values[1]
81
+ }
82
+ )
83
+ end
84
+
85
+ def parse_i18n_changeset(attribute, values, type, diff)
86
+ values.last.each_key do |locale, _value|
87
+ next if display_locale && display_locale != locale
88
+
89
+ first_value = values.first.try(:[], locale) if values.first.is_a? Hash
90
+ last_value = values.last.try(:[], locale) if values.last.is_a? Hash
91
+ next if first_value == last_value
92
+
93
+ attribute_locale = "#{attribute}_#{locale}".to_sym
94
+ diff.update(
95
+ attribute_locale => {
96
+ type: :string,
97
+ label: generate_i18n_label(attribute, locale),
98
+ old_value: first_value,
99
+ new_value: last_value
100
+ }
101
+ )
102
+ end
103
+ diff
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ module DiffRenderer
6
+ class Categorization < Base
7
+ protected
8
+
9
+ def i18n_scope
10
+ "activemodel.attributes.plan"
11
+ end
12
+
13
+ # Lists which attributes will be diffable and how
14
+ # they should be rendered.
15
+ def attribute_types
16
+ {
17
+ decidim_category_id: :category
18
+ }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ module DiffRenderer
6
+ class Content < Base
7
+ protected
8
+
9
+ def i18n_scope
10
+ "activemodel.attributes.plan"
11
+ end
12
+
13
+ # Lists which attributes will be diffable and how
14
+ # they should be rendered.
15
+ def attribute_types
16
+ {
17
+ body: :i18n
18
+ }
19
+ end
20
+
21
+ def generate_label(_attribute)
22
+ translated_attribute(version.item.section.body)
23
+ end
24
+
25
+ def generate_i18n_label(_attribute, locale)
26
+ label = translated_attribute(version.item.section.body, locale)
27
+ return label if display_locale
28
+ "#{label} (#{locale_name(locale)})"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ module DiffRenderer
6
+ class Plan < Base
7
+ protected
8
+
9
+ def i18n_scope
10
+ "activemodel.attributes.plan"
11
+ end
12
+
13
+ # Lists which attributes will be diffable and how
14
+ # they should be rendered.
15
+ def attribute_types
16
+ {
17
+ title: :i18n,
18
+ decidim_scope_id: :scope,
19
+ state: :string
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # The loggability class modifies the core's Traceability class by removing
6
+ # the PaperTrail tracing from the objects. This is needed because we want to
7
+ # handle the PaperTrail tracing manually for the plans.
8
+ class Loggability < ::Decidim::Traceability
9
+ def perform_action!(action, resource, author, extra_log_info = {})
10
+ Decidim::ApplicationRecord.transaction do
11
+ result = block_given? ? yield : nil
12
+ loggable_resource = resource.is_a?(Class) ? result : resource
13
+ log(action, author, loggable_resource, extra_log_info)
14
+ return result
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # A factory class to ensure we always create Plans the same way since it involves some logic.
6
+ module PlanBuilder
7
+ # Public: Creates a new Plan.
8
+ #
9
+ # attributes - The Hash of attributes to create the Plan with.
10
+ # author - An Authorable the will be the first coauthor of the Plan.
11
+ # user_group_author - A User Group to, optionally, set it as the author too.
12
+ # action_user - The User to be used as the user who is creating the plan in the traceability logs.
13
+ #
14
+ # Returns a Plan.
15
+ def create(attributes:, author:, action_user:, user_group_author: nil)
16
+ Decidim.traceability.perform_action!(:create, Plan, action_user, visibility: "all") do
17
+ plan = Plan.new(attributes)
18
+ plan.add_coauthor(author, user_group: user_group_author)
19
+ plan.save!
20
+ plan
21
+ end
22
+ end
23
+
24
+ module_function :create
25
+
26
+ # Public: Creates a new Plan by copying the attributes from another one.
27
+ #
28
+ # original_plan - The Plan to be used as base to create the new one.
29
+ # author - An Authorable the will be the first coauthor of the Plan.
30
+ # user_group_author - A User Group to, optionally, set it as the author too.
31
+ # action_user - The User to be used as the user who is creating the plan in the traceability logs.
32
+ # extra_attributes - A Hash of attributes to create the new plan, will overwrite the original ones.
33
+ # skip_link - Whether to skip linking the two plans or not (default false).
34
+ #
35
+ # Returns a Plan
36
+ #
37
+ # rubocop:disable Metrics/ParameterLists
38
+ def copy(original_plan, author:, action_user:, user_group_author: nil, extra_attributes: {}, skip_link: false)
39
+ origin_attributes = original_plan.attributes.except(
40
+ "id",
41
+ "created_at",
42
+ "updated_at",
43
+ "state",
44
+ "answer",
45
+ "answered_at",
46
+ "decidim_component_id"
47
+ ).merge(
48
+ "category" => original_plan.category
49
+ ).merge(
50
+ extra_attributes
51
+ )
52
+
53
+ plan = create(
54
+ attributes: origin_attributes,
55
+ author: author,
56
+ user_group_author: user_group_author,
57
+ action_user: action_user
58
+ )
59
+
60
+ plan.link_resources(original_plan, "copied_from_component") unless skip_link
61
+ plan
62
+ end
63
+ # rubocop:enable Metrics/ParameterLists
64
+
65
+ module_function :copy
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # A service to encapsualte all the logic when searching and filtering
6
+ # plans in a participatory process.
7
+ class PlanSearch < ResourceSearch
8
+ # Public: Initializes the service.
9
+ # component - A Decidim::Component to get the plans from.
10
+ # page - The page number to paginate the results.
11
+ # per_page - The number of plans to return per page.
12
+ def initialize(options = {})
13
+ super(Plan.all, options)
14
+ end
15
+
16
+ # Handle the search_text filter
17
+ def search_search_text
18
+ query.where(localized_search_text_in(:title), text: "%#{search_text}%")
19
+ end
20
+
21
+ # Handle the origin filter
22
+ # The 'official' plans doesn't have an author id
23
+ def search_origin
24
+ if origin == "official"
25
+ query.where.not(coauthorships_count: 0).joins(:coauthorships).where(decidim_coauthorships: { decidim_author_type: "Decidim::Organization" })
26
+ elsif origin == "citizens"
27
+ query.where.not(coauthorships_count: 0).joins(:coauthorships).where.not(decidim_coauthorships: { decidim_author_type: "Decidim::Organization" })
28
+ else # Assume 'all'
29
+ query
30
+ end
31
+ end
32
+
33
+ # Handle the activity filter
34
+ def search_activity
35
+ query
36
+ end
37
+
38
+ # Handle the state filter
39
+ def search_state
40
+ case state
41
+ when "accepted"
42
+ query.accepted
43
+ when "rejected"
44
+ query.rejected
45
+ when "evaluating"
46
+ query.evaluating
47
+ when "withdrawn"
48
+ query.withdrawn
49
+ when "except_rejected"
50
+ query.except_rejected.except_withdrawn
51
+ else # Assume 'not_withdrawn'
52
+ query.except_withdrawn
53
+ end
54
+ end
55
+
56
+ # Filters Plans by the name of the classes they are linked to. By default,
57
+ # returns all Plans. When a `related_to` param is given, then it camelcases item
58
+ # to find the real class name and checks the links for the Plans.
59
+ #
60
+ # The `related_to` param is expected to be in this form:
61
+ #
62
+ # "decidim/meetings/meeting"
63
+ #
64
+ # This can be achieved by performing `klass.name.underscore`.
65
+ #
66
+ # Returns only those plans that are linked to the given class name.
67
+ def search_related_to
68
+ from = query
69
+ .joins(:resource_links_from)
70
+ .where(decidim_resource_links: { to_type: related_to.camelcase })
71
+
72
+ to = query
73
+ .joins(:resource_links_to)
74
+ .where(decidim_resource_links: { from_type: related_to.camelcase })
75
+
76
+ query.where(id: from).or(query.where(id: to))
77
+ end
78
+
79
+ private
80
+
81
+ # Internal: builds the needed query to search for a text in the organization's
82
+ # available locales. Note that it is intended to be used as follows:
83
+ #
84
+ # Example:
85
+ # Resource.where(localized_search_text_for(:title, text: "my_query"))
86
+ #
87
+ # The Hash with the `:text` key is required or it won't work.
88
+ def localized_search_text_in(field)
89
+ options[:organization].available_locales.map do |l|
90
+ "#{field} ->> '#{l}' ILIKE :text"
91
+ end.join(" OR ")
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # The Tracer class allows performing PaperTrail requests where all records
6
+ # saved during the yield will be stored against the same transaction ID.
7
+ # This allows saving the main record and its associated records so that the
8
+ # same changeset can be easily identified afterwards.
9
+ class Tracer
10
+ def trace!(author)
11
+ return unless block_given?
12
+
13
+ ::PaperTrail.request(whodunnit: gid(author)) do
14
+ yield
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ # Calculates the GlobalID of the version author. If the object does not
21
+ # respond to `to_gid`, then it returns the object itself.
22
+ def gid(author)
23
+ return if author.blank?
24
+ return author.to_gid if author.respond_to?(:to_gid)
25
+ author
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ <%= render "decidim/plans/admin/shared/info_plan" %>
2
+ <%= decidim_form_for(@form, url: plan_plan_answer_path(plan, @form), html: { class: "form edit_plan_answer" }) do |f| %>
3
+ <div class="card">
4
+ <div class="card-divider">
5
+ <h2 class="card-title"><%= t ".title", title: present(plan).title %></h2>
6
+ </div>
7
+
8
+ <div class="card-section">
9
+ <div class="row column">
10
+ <%= f.collection_radio_buttons :state, [["accepted", t(".accepted")], ["rejected", t(".rejected")], ["evaluating", t(".evaluating")]], :first, :last, prompt: true %>
11
+ </div>
12
+
13
+ <div class="row column">
14
+ <%= f.translated :editor, :answer, autofocus: true, rows: 15 %>
15
+ </div>
16
+ </div>
17
+ </div>
18
+
19
+ <div class="button--double form-general-submit">
20
+ <%= f.submit t(".answer_plan") %>
21
+ </div>
22
+ <% end %>
@@ -0,0 +1,9 @@
1
+ <div class="flex--cc flex-gap--1">
2
+ <div id="js-other-actions-wrapper">
3
+ <%= link_to t("actions.sections", scope: "decidim.plans"), sections_path, class: "button tiny button--simple" %>
4
+
5
+ <% if allowed_to? :create, :plan %>
6
+ <%= link_to t("actions.new", scope: "decidim.plans"), new_plan_path, class: "button tiny button--simple" %>
7
+ <% end %>
8
+ </div>
9
+ </div>
@@ -0,0 +1,62 @@
1
+ <%
2
+ # Force the form to be multipart for the file uploads.
3
+ # Calling Decidim form builder's own `upload` builder method should already do
4
+ # this but it is not working in our case since we have multiple file fields.
5
+ form.multipart = true
6
+ %>
7
+
8
+ <div class="card">
9
+ <div class="card-divider">
10
+ <h2 class="card-title"><%= title %></h2>
11
+ </div>
12
+
13
+ <div class="card-section">
14
+ <div class="row column">
15
+ <%= attached_proposals_picker_field form, :proposals %>
16
+ </div>
17
+
18
+ <%= javascript_include_tag "decidim/plans/proposal_picker" %>
19
+ <%= stylesheet_link_tag "decidim/plans/proposal_picker" %>
20
+
21
+ <div class="row column">
22
+ <%= remaining_characters("plans_title", component_settings.plan_title_length) do |opts| %>
23
+ <% if component_settings.multilingual_answers? %>
24
+ <%= form.translated :text_field, :title, opts.merge(tabs_id: "title-tabs") %>
25
+ <% else %>
26
+ <%= form.text_field(
27
+ "title_#{current_locale}".to_sym,
28
+ {
29
+ label: form.label_for(:title)
30
+ }.merge(opts)
31
+ ) %>
32
+ <% end %>
33
+ <% end %>
34
+ </div>
35
+
36
+ <% if @form.categories&.any? %>
37
+ <div class="row column">
38
+ <%= form.categories_select :category_id, @form.categories, prompt: t(".select_a_category") %>
39
+ </div>
40
+ <% end %>
41
+
42
+ <% if current_participatory_space.has_subscopes? %>
43
+ <div class="row column">
44
+ <%= scopes_picker_field form, :scope_id %>
45
+ </div>
46
+ <% end %>
47
+
48
+ <% @form.contents.each_with_index do |content, idx| %>
49
+ <%= fields_for "contents[]", content do |content_form| %>
50
+ <%= render "decidim/plans/shared/section_content_field", form: content_form, id: tabs_id_for_content(idx) %>
51
+ <% end %>
52
+ <% end %>
53
+
54
+ <% if component_settings.attachments_allowed? %>
55
+ <div class="row column">
56
+ <%= render partial: "decidim/plans/shared/attachments", object: form, locals: { form: form, blank_attachment: blank_attachment, component_settings: component_settings } %>
57
+ </div>
58
+ <% end %>
59
+ </div>
60
+ </div>
61
+
62
+ <%= javascript_include_tag "decidim/plans/plans_form" %>
@@ -0,0 +1,6 @@
1
+ <div class="callout callout--full <%= css %>" data-closable>
2
+ <%= text %>
3
+ <button class="close-button" type="button" data-close="">
4
+ <span aria-hidden="true">×</span>
5
+ </button>
6
+ </div>