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,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # A Helper to find and render the author of a version.
6
+ module TraceabilityHelper
7
+ include Decidim::TraceabilityHelper
8
+
9
+ # Caches a DiffRenderer instance for the `current_version`.
10
+ def item_diff_renderers
11
+ @item_diff_renderers ||= item_versions.map do |version|
12
+ renderer_for(version)
13
+ end.compact
14
+ end
15
+
16
+ def associated_diff_renderers
17
+ @associated_diff_renderers ||= associated_versions.map do |version|
18
+ renderer_for(version)
19
+ end.compact
20
+ end
21
+
22
+ def content_diff_renderers
23
+ @content_diff_renderers ||= content_versions.map do |version|
24
+ renderer_for(version)
25
+ end.compact
26
+ end
27
+
28
+ def diff_renderers
29
+ item_diff_renderers + associated_diff_renderers + content_diff_renderers
30
+ end
31
+
32
+ private
33
+
34
+ def renderer_for(version)
35
+ locale = current_locale unless component_settings.multilingual_answers?
36
+
37
+ renderer_klass =
38
+ case version.item.class.name
39
+ when "Decidim::Plans::Plan"
40
+ Decidim::Plans::DiffRenderer::Plan
41
+ when "Decidim::Plans::Content"
42
+ Decidim::Plans::DiffRenderer::Content
43
+ when "Decidim::Categorization"
44
+ Decidim::Plans::DiffRenderer::Categorization
45
+ end
46
+
47
+ renderer_klass.new(version, locale)
48
+ end
49
+
50
+ # Renders the given value in a user-friendly way based on the value class.
51
+ #
52
+ # value - an object to be rendered
53
+ #
54
+ # Returns an HTML-ready String.
55
+ def render_diff_value(value, type, action, options = {})
56
+ return "".html_safe if value.blank?
57
+
58
+ value_to_render = case type
59
+ when :date
60
+ l value, format: :long
61
+ when :percentage
62
+ number_to_percentage value, precision: 2
63
+ when :translatable
64
+ value.to_s
65
+ else
66
+ value
67
+ end
68
+
69
+ content_tag(:div, class: "card--list__item #{action}") do
70
+ content_tag(:div, class: "card--list__text") do
71
+ content_tag(:div, { class: "diff__value" }.merge(options)) do
72
+ value_to_render
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ module UserGroupHelper
6
+ # Renders a user_group select field in a form.
7
+ # form - FormBuilder object
8
+ # name - attribute user_group_id
9
+ #
10
+ # Returns nothing.
11
+ def user_group_select_field(form, name)
12
+ selected = @form.user_group_id.presence
13
+ user_groups = Decidim::UserGroups::ManageableUserGroups.for(current_user).verified
14
+ form.select(
15
+ name,
16
+ user_groups.map { |g| [g.name, g.id] },
17
+ selected: selected,
18
+ include_blank: current_user.name
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ require_dependency "paper_trail/frameworks/active_record"
6
+
7
+ module Decidim
8
+ module Plans
9
+ # A concern that adds traceabilty capability to the given model. Including
10
+ # this allows you the keep track of changes in the model attributes and
11
+ # changes authorship.
12
+ #
13
+ # Example:
14
+ #
15
+ # class MyModel < ApplicationRecord
16
+ # include Decidim::Plans::Traceable
17
+ # end
18
+ module Traceable
19
+ extend ActiveSupport::Concern
20
+
21
+ included do
22
+ # This is customized with the `:ignore` option since we don't want to
23
+ # store another version when the item is published.
24
+ has_paper_trail ignore: [:published_at],
25
+ class_name: "Decidim::Plans::PaperTrail::Version"
26
+
27
+ delegate :count, to: :versions, prefix: true
28
+
29
+ def last_whodunnit
30
+ versions.last.try(:whodunnit)
31
+ end
32
+
33
+ def last_editor
34
+ Decidim.traceability.version_editor(versions.last)
35
+ end
36
+ end
37
+
38
+ # This is needed for the action logs to work properly. They are only
39
+ # stored against models that implement Decidim::Traceable. However, we
40
+ # cannot directly include that module because we want to modify its
41
+ # functionality.
42
+ def is_a?(klass)
43
+ return true if klass == Decidim::Traceable
44
+ super
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # Abstract class from which all models in this engine inherit.
6
+ class ApplicationRecord < ActiveRecord::Base
7
+ self.abstract_class = true
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # This class holds the association between a Plan and a
6
+ # Decidim::Proposals::Proposal.
7
+ class AttachedProposal < ApplicationRecord
8
+ belongs_to :plan, class_name: "Decidim::Plans::Plan", foreign_key: "decidim_plan_id"
9
+ belongs_to :proposal, class_name: "Decidim::Proposals::Proposal", foreign_key: "decidim_proposal_id"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # The content record is the actual content for each plan section for a
6
+ # single plan.
7
+ class Content < Plans::ApplicationRecord
8
+ include Decidim::Plans::Traceable
9
+
10
+ self.table_name = "decidim_plans_plan_contents"
11
+
12
+ belongs_to :plan, class_name: "Decidim::Plans::Plan", foreign_key: "decidim_plan_id"
13
+ belongs_to :section, class_name: "Decidim::Plans::Section", foreign_key: "decidim_section_id"
14
+ belongs_to :user, class_name: "Decidim::User", foreign_key: "decidim_user_id"
15
+
16
+ def title
17
+ section.body
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ module PaperTrail
6
+ class Version < ::PaperTrail::Version
7
+ self.table_name = "versions"
8
+
9
+ # Since the test suite has test coverage for this, we want to declare
10
+ # the association when the test suite is running. This makes it pass
11
+ # when DB is not initialized prior to test runs such as when we run on
12
+ # Travis CI Ex. (there won't be a db in `spec/dummy_app/db/`).
13
+ has_many :version_associations,
14
+ class_name: "Decidim::Plans::PaperTrail::VersionAssociation",
15
+ dependent: :destroy
16
+
17
+ scope(:within_transaction, ->(id) { where transaction_id: id })
18
+
19
+ # Track the associations on the plans object
20
+ def track_associations?
21
+ true
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ module PaperTrail
6
+ class VersionAssociation < ::ActiveRecord::Base
7
+ self.table_name = "version_associations"
8
+
9
+ belongs_to :version,
10
+ class_name: "Decidim::Plans::PaperTrail::Version"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # A plan is similar to a proposal but allows the users to fill in multiple
6
+ # custom fields that the admin users define for each plans component. The
7
+ # fields are the plan sections and the contents are in plan contents.
8
+ class Plan < Plans::ApplicationRecord
9
+ include Decidim::Resourceable
10
+ include Decidim::Coauthorable
11
+ include Decidim::HasComponent
12
+ include Decidim::ScopableComponent
13
+ include Decidim::HasCategory
14
+ include Decidim::Reportable
15
+ include Decidim::HasAttachments
16
+ include Decidim::Followable
17
+ include Decidim::Comments::Commentable
18
+ include Decidim::Searchable
19
+ include Decidim::Plans::Traceable
20
+ include Decidim::Loggable
21
+
22
+ component_manifest_name "plans"
23
+
24
+ has_many :collaborator_requests,
25
+ class_name: "Decidim::Plans::PlanCollaboratorRequest",
26
+ foreign_key: :decidim_plan_id,
27
+ dependent: :destroy
28
+
29
+ has_many :requesters,
30
+ through: :collaborator_requests,
31
+ source: :user,
32
+ class_name: "Decidim::User",
33
+ foreign_key: :decidim_user_id
34
+
35
+ has_many :attached_proposals, foreign_key: :decidim_plan_id, dependent: :destroy
36
+ has_many :proposals, through: :attached_proposals
37
+
38
+ has_many :contents, foreign_key: :decidim_plan_id, dependent: :destroy
39
+
40
+ scope :open, -> { where(state: "open") }
41
+ scope :accepted, -> { where(state: "accepted") }
42
+ scope :rejected, -> { where(state: "rejected") }
43
+ scope :evaluating, -> { where(state: "evaluating") }
44
+ scope :withdrawn, -> { where(state: "withdrawn") }
45
+ scope :except_rejected, -> { where.not(state: "rejected").or(where(state: nil)) }
46
+ scope :except_withdrawn, -> { where.not(state: "withdrawn").or(where(state: nil)) }
47
+ scope :drafts, -> { where(published_at: nil) }
48
+ scope :published, -> { where.not(published_at: nil) }
49
+
50
+ acts_as_list scope: :decidim_component_id
51
+
52
+ searchable_fields(
53
+ {
54
+ scope_id: :decidim_scope_id,
55
+ participatory_space: { component: :participatory_space },
56
+ A: :search_title,
57
+ datetime: :published_at
58
+ },
59
+ index_on_create: ->(proposal) { proposal.visible? },
60
+ index_on_update: ->(proposal) { proposal.visible? }
61
+ )
62
+
63
+ def sections
64
+ Section.where(component: component).order(:position)
65
+ end
66
+
67
+ def self.order_randomly(seed)
68
+ transaction do
69
+ connection.execute("SELECT setseed(#{connection.quote(seed)})")
70
+ order(Arel.sql("RANDOM()")).load
71
+ end
72
+ end
73
+
74
+ def self.log_presenter_class_for(_log)
75
+ Decidim::Plans::AdminLog::PlanPresenter
76
+ end
77
+
78
+ # Returns a collection scoped by an author.
79
+ # Overrides this method in DataPortability to support Coauthorable.
80
+ def self.user_collection(author)
81
+ return unless author.is_a?(Decidim::User)
82
+
83
+ joins(:coauthorships)
84
+ .where("decidim_coauthorships.coauthorable_type = ?", name)
85
+ .where("decidim_coauthorships.decidim_author_id = ? AND decidim_coauthorships.decidim_author_type = ? ", author.id, author.class.base_class.name)
86
+ end
87
+
88
+ # Public: Checks if the plan is open for edits.
89
+ #
90
+ # Returns Boolean.
91
+ def open?
92
+ state == "open"
93
+ end
94
+
95
+ # Public: Checks if the plan has been published or not.
96
+ #
97
+ # Returns Boolean.
98
+ def published?
99
+ published_at.present?
100
+ end
101
+
102
+ # Public: Checks if the organization has given an answer for the plan.
103
+ #
104
+ # Returns Boolean.
105
+ def answered?
106
+ answered_at.present? && state.present?
107
+ end
108
+
109
+ # Public: Checks if the organization has accepted a plan.
110
+ #
111
+ # Returns Boolean.
112
+ def accepted?
113
+ answered? && state == "accepted"
114
+ end
115
+
116
+ # Public: Checks if the organization has rejected a plan.
117
+ #
118
+ # Returns Boolean.
119
+ def rejected?
120
+ answered? && state == "rejected"
121
+ end
122
+
123
+ # Public: Checks if the organization has marked the plan as evaluating it.
124
+ #
125
+ # Returns Boolean.
126
+ def evaluating?
127
+ answered? && state == "evaluating"
128
+ end
129
+
130
+ # Public: Checks if the author has withdrawn the plan.
131
+ #
132
+ # Returns Boolean.
133
+ def withdrawn?
134
+ state == "withdrawn"
135
+ end
136
+
137
+ # Public: Overrides the `reported_content_url` Reportable concern method.
138
+ def reported_content_url
139
+ ResourceLocatorPresenter.new(self).url
140
+ end
141
+
142
+ # Checks whether the user can edit the given plan.
143
+ #
144
+ # user - the user to check for authorship
145
+ def editable_by?(user)
146
+ !answered? && !copied_from_other_component? && authored_by?(user)
147
+ end
148
+
149
+ # Checks whether the user can withdraw the given plan.
150
+ #
151
+ # user - the user to check for withdrawability.
152
+ def withdrawable_by?(user)
153
+ user && !withdrawn? && creator_author == user && !copied_from_other_component?
154
+ end
155
+
156
+ # Public: Whether the plan is a draft or not.
157
+ def draft?
158
+ published_at.nil?
159
+ end
160
+
161
+ def commentable?
162
+ component.settings.comments_enabled?
163
+ end
164
+
165
+ def accepts_new_comments?
166
+ commentable? && !component.current_settings.comments_blocked
167
+ end
168
+
169
+ def comments_have_alignment?
170
+ true
171
+ end
172
+
173
+ def comments_have_votes?
174
+ true
175
+ end
176
+
177
+ def users_to_notify_on_comment_created
178
+ followers
179
+ end
180
+
181
+ # method for sort_link by number of comments
182
+ ransacker :commentable_comments_count do
183
+ query = <<-SQL
184
+ (SELECT COUNT(decidim_comments_comments.id)
185
+ FROM decidim_comments_comments
186
+ WHERE decidim_comments_comments.decidim_commentable_id = decidim_plans_plans.id
187
+ AND decidim_comments_comments.decidim_commentable_type = 'Decidim::Plans::Plan'
188
+ GROUP BY decidim_comments_comments.decidim_commentable_id
189
+ )
190
+ SQL
191
+ Arel.sql(query)
192
+ end
193
+
194
+ # Checks whether the plan is inside the time window to be editable or not once published.
195
+ def within_edit_time_limit?
196
+ true
197
+ end
198
+
199
+ private
200
+
201
+ def copied_from_other_component?
202
+ linked_resources(:plans, "copied_from_component").any?
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # A plan can accept requests to coauthor and contribute
6
+ class PlanCollaboratorRequest < Plans::ApplicationRecord
7
+ validates :plan, :user, presence: true
8
+
9
+ belongs_to :plan, class_name: "Decidim::Plans::Plan", foreign_key: :decidim_plan_id
10
+ belongs_to :user, class_name: "Decidim::User", foreign_key: :decidim_user_id
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ # A plan section is a record that stores the question data and order of
6
+ # items that the users need to fill for each plan. The actual contents of
7
+ # the plans are stored in the Decidim::Plans::Content records.
8
+ class Section < Plans::ApplicationRecord
9
+ include Decidim::HasComponent
10
+
11
+ TYPES = %w(field_text_multiline field_text).freeze
12
+
13
+ validates :section_type, inclusion: { in: TYPES }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ module Admin
6
+ class Permissions < Decidim::DefaultPermissions
7
+ def permissions
8
+ return permission_action unless user
9
+
10
+ return permission_action if permission_action.scope != :admin
11
+
12
+ toggle_allow(admin_plan_answering_is_enabled?) if
13
+ permission_action.action == :create &&
14
+ permission_action.subject == :plan_answer
15
+
16
+ return permission_action if permission_action.subject != :plan &&
17
+ permission_action.subject != :plans &&
18
+ permission_action.subject != :section &&
19
+ permission_action.subject != :sections
20
+
21
+ case permission_action.action
22
+ when :create
23
+ permission_action.allow!
24
+ when :edit, :update, :destroy
25
+ permission_action.allow! if plan.present? || section.present?
26
+ end
27
+
28
+ permission_action
29
+ end
30
+
31
+ private
32
+
33
+ def plan
34
+ @plan ||= context.fetch(:plan, nil)
35
+ end
36
+
37
+ def admin_plan_answering_is_enabled?
38
+ current_settings.try(:plan_answering_enabled) &&
39
+ component_settings.try(:plan_answering_enabled)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ class Permissions < Decidim::DefaultPermissions
6
+ def permissions
7
+ return permission_action unless user
8
+
9
+ # Delegate the admin permission checks to the admin permissions class
10
+ return Decidim::Plans::Admin::Permissions.new(user, permission_action, context).permissions if permission_action.scope == :admin
11
+ return permission_action if permission_action.scope != :public
12
+
13
+ if permission_action.subject == :plan
14
+ apply_plan_permissions(permission_action)
15
+ else
16
+ permission_action
17
+ end
18
+
19
+ permission_action
20
+ end
21
+
22
+ private
23
+
24
+ def plan
25
+ @plan ||= context.fetch(:plan, nil)
26
+ end
27
+
28
+ def apply_plan_permissions(permission_action)
29
+ case permission_action.action
30
+ when :create
31
+ can_create_plan?
32
+ when :edit
33
+ can_edit_plan?
34
+ when :withdraw
35
+ can_withdraw_plan?
36
+ when :publish
37
+ can_publish_plan?
38
+ when :request_access
39
+ can_request_access_plan?
40
+ end
41
+ end
42
+
43
+ def can_create_plan?
44
+ toggle_allow(authorized?(:create) && current_settings&.creation_enabled?)
45
+ end
46
+
47
+ def can_edit_plan?
48
+ toggle_allow(plan.open? && plan.editable_by?(user))
49
+ end
50
+
51
+ def can_withdraw_plan?
52
+ toggle_allow(plan && plan.withdrawable_by?(user))
53
+ end
54
+
55
+ def can_publish_plan?
56
+ toggle_allow(plan.open? && plan.editable_by?(user))
57
+ end
58
+
59
+ def can_request_access_plan?
60
+ return toggle_allow(false) unless plan.open?
61
+ return toggle_allow(false) if plan.editable_by?(user)
62
+ return toggle_allow(false) if plan.requesters.include? user
63
+ toggle_allow(plan && !plan.editable_by?(user))
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ module AdminLog
6
+ # This class holds the logic to present a `Decidim::Plans::Plan`
7
+ # for the `AdminLog` log.
8
+ #
9
+ # Usage should be automatic and you shouldn't need to call this class
10
+ # directly, but here's an example:
11
+ #
12
+ # action_log = Decidim::ActionLog.last
13
+ # view_helpers # => this comes from the views
14
+ # PlanPresenter.new(action_log, view_helpers).present
15
+ class PlanPresenter < Decidim::Log::BasePresenter
16
+ private
17
+
18
+ def resource_presenter
19
+ @resource_presenter ||= Decidim::Plans::Log::ResourcePresenter.new(action_log.resource, h, action_log.extra["resource"])
20
+ end
21
+
22
+ def diff_fields_mapping
23
+ {
24
+ state: "Decidim::Plans::AdminLog::ValueTypes::PlanStatePresenter",
25
+ answered_at: :date,
26
+ answer: :i18n
27
+ }
28
+ end
29
+
30
+ def action_string
31
+ case action
32
+ when "answer", "create", "update"
33
+ "decidim.plans.admin_log.plan.#{action}"
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ def i18n_labels_scope
40
+ "activemodel.attributes.plan"
41
+ end
42
+
43
+ def has_diff?
44
+ action == "answer" || super
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ module AdminLog
6
+ module ValueTypes
7
+ class PlanStatePresenter < Decidim::Log::ValueTypes::DefaultPresenter
8
+ def present
9
+ return unless value
10
+ h.t(value, scope: "decidim.plans.admin.plan_answers.edit", default: value)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Plans
5
+ #
6
+ # Decorator for contents
7
+ #
8
+ class ContentPresenter < SimpleDelegator
9
+ include Rails.application.routes.mounted_helpers
10
+ include TranslatableAttributes
11
+
12
+ def content
13
+ __getobj__
14
+ end
15
+
16
+ def title
17
+ translated_attribute(content.title).html_safe
18
+ end
19
+
20
+ def body
21
+ translated_attribute(content.body).html_safe
22
+ end
23
+ end
24
+ end
25
+ end