decidim-participatory_processes 0.19.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/app/commands/decidim/participatory_processes/admin/create_participatory_process.rb +0 -11
  3. data/app/commands/decidim/participatory_processes/admin/import_participatory_process.rb +72 -0
  4. data/app/controllers/decidim/participatory_processes/admin/participatory_process_exports_controller.rb +20 -0
  5. data/app/controllers/decidim/participatory_processes/admin/participatory_process_imports_controller.rb +31 -0
  6. data/app/forms/decidim/participatory_processes/admin/participatory_process_import_form.rb +80 -0
  7. data/app/models/decidim/participatory_process.rb +13 -0
  8. data/app/permissions/decidim/participatory_processes/permissions.rb +6 -2
  9. data/app/presenters/decidim/participatory_processes/participatory_process_group_presenter.rb +22 -0
  10. data/app/presenters/decidim/participatory_processes/participatory_process_presenter.rb +25 -0
  11. data/app/serializers/decidim/participatory_processes/participatory_process_importer.rb +178 -0
  12. data/app/serializers/decidim/participatory_processes/participatory_process_serializer.rb +153 -0
  13. data/app/views/decidim/participatory_processes/admin/participatory_process_groups/index.html.erb +8 -0
  14. data/app/views/decidim/participatory_processes/admin/participatory_process_imports/_form.html.erb +49 -0
  15. data/app/views/decidim/participatory_processes/admin/participatory_process_imports/new.html.erb +7 -0
  16. data/app/views/decidim/participatory_processes/admin/participatory_processes/index.html.erb +12 -0
  17. data/app/views/decidim/participatory_processes/admin/shared/_secondary_nav.html.erb +17 -0
  18. data/app/views/layouts/decidim/admin/participatory_process_groups.html.erb +0 -11
  19. data/config/locales/ca.yml +19 -0
  20. data/config/locales/cs.yml +19 -0
  21. data/config/locales/en.yml +19 -0
  22. data/config/locales/fi-plain.yml +19 -0
  23. data/config/locales/fi.yml +19 -0
  24. data/config/locales/fr.yml +19 -0
  25. data/config/locales/hu.yml +19 -0
  26. data/config/locales/it.yml +19 -0
  27. data/config/locales/nl.yml +19 -0
  28. data/config/locales/no.yml +4 -0
  29. data/config/locales/sv.yml +10 -0
  30. data/lib/decidim/participatory_processes/admin_engine.rb +9 -9
  31. data/lib/decidim/participatory_processes/participatory_space.rb +10 -0
  32. data/lib/decidim/participatory_processes/version.rb +1 -1
  33. metadata +19 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26796587924ddffda370cd31f84fe2356197e4a772670d73621b4d49eb3b48ec
4
- data.tar.gz: a580378c67f87b97d994f45afbe1633fc7dc75df0e2deb0489d74f8470be41aa
3
+ metadata.gz: 5f959ee9321869b81c37005639c7a0a97f1b2722c77761e79306efa8ecfa1066
4
+ data.tar.gz: ec371806ff5ead53ee47a376df07dff624e5339ed753a152b0368d8bfe2d8c8e
5
5
  SHA512:
6
- metadata.gz: 69e91a8fd76b7fe47d34fab56a3607713654efebbabedd88a21624ec23763c5f6524a418a3313372adac0417834174e9541ff798cadf26704dfa3766164462a7
7
- data.tar.gz: e864877ec2478044dafda6763b939e197dc426541ed0d7a6889c5b0c2e52e7c1ea5f0a91978d0aa38d0190eb01ff4a346c3fded112e2df73685cea6fb1293237
6
+ metadata.gz: be8f140181b6a2c49468bfdd39cdcea9acca454b75b0797ad0d1fb8f8e5c0b657e3cb375f905625bfd9acb3fcbebe71e20acff98de7617870172711869d77d14
7
+ data.tar.gz: d0bffa4750c27e6ac2f61904ece20a42847f86dc69a2cea5ad8858e3a51087f9c45bbf8e073be72cd8b4bab4e82096ad812d5503c92a5bbdb69d7ed4723ee43a
@@ -105,17 +105,6 @@ module Decidim
105
105
  Decidim::CreateFollow.new(form, admin).call
106
106
  end
107
107
  end
108
-
109
- def create_participatory_process_users(process)
110
- return unless form.private_process
111
-
112
- form.users.each do |user|
113
- ParticipatoryProcessUser.create!(
114
- participatory_process: process,
115
- user: user
116
- )
117
- end
118
- end
119
108
  end
120
109
  end
121
110
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ParticipatoryProcesses
5
+ module Admin
6
+ # A command with all the business logic when copying a new participatory
7
+ # process in the system.
8
+ class ImportParticipatoryProcess < Rectify::Command
9
+ # Public: Initializes the command.
10
+ #
11
+ # form - A form object with the params.
12
+ # participatory_process - A participatory_process we want to duplicate
13
+ def initialize(form)
14
+ @form = form
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
+ transaction do
27
+ import_participatory_process
28
+ add_admins_as_followers(@imported_process)
29
+ end
30
+
31
+ broadcast(:ok, @imported_process)
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :form
37
+
38
+ def import_participatory_process
39
+ importer = Decidim::ParticipatoryProcesses::ParticipatoryProcessImporter.new(form.current_organization, form.current_user)
40
+ participatory_processes.each do |original_process|
41
+ @imported_process = importer.import(original_process, form.current_user, title: form.title, slug: form.slug)
42
+ importer.import_participatory_process_steps(original_process["participatory_process_steps"]) if form.import_steps?
43
+ importer.import_categories(original_process["participatory_process_categories"]) if form.import_categories?
44
+ importer.import_folders_and_attachments(original_process["attachments"]) if form.import_attachments?
45
+ importer.import_components(original_process["components"]) if form.import_components?
46
+ end
47
+ end
48
+
49
+ def participatory_processes
50
+ document_parsed(form.document_text)
51
+ end
52
+
53
+ def document_parsed(document_text)
54
+ JSON.parse(document_text)
55
+ end
56
+
57
+ def add_admins_as_followers(process)
58
+ process.organization.admins.each do |admin|
59
+ form = Decidim::FollowForm
60
+ .from_params(followable_gid: process.to_signed_global_id.to_s)
61
+ .with_context(
62
+ current_organization: process.organization,
63
+ current_user: admin
64
+ )
65
+
66
+ Decidim::CreateFollow.new(form, admin).call
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ParticipatoryProcesses
5
+ module Admin
6
+ class ParticipatoryProcessExportsController < Decidim::Admin::ApplicationController
7
+ include Concerns::ParticipatoryProcessAdmin
8
+ include Decidim::Admin::ParticipatorySpaceExport
9
+
10
+ def exportable_space
11
+ current_participatory_process
12
+ end
13
+
14
+ def after_export_path
15
+ participatory_processes_path
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ParticipatoryProcesses
5
+ module Admin
6
+ class ParticipatoryProcessImportsController < Decidim::ParticipatoryProcesses::Admin::ApplicationController
7
+ def new
8
+ enforce_permission_to :import, :process
9
+ @form = form(ParticipatoryProcessImportForm).instance
10
+ end
11
+
12
+ def create
13
+ enforce_permission_to :import, :process
14
+ @form = form(ParticipatoryProcessImportForm).from_params(params)
15
+
16
+ ImportParticipatoryProcess.call(@form) do
17
+ on(:ok) do
18
+ flash[:notice] = I18n.t("participatory_process_imports.create.success", scope: "decidim.admin")
19
+ redirect_to participatory_processes_path
20
+ end
21
+
22
+ on(:invalid) do
23
+ flash.now[:alert] = I18n.t("participatory_process_imports.create.error", scope: "decidim.admin")
24
+ render :new
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ParticipatoryProcesses
5
+ module Admin
6
+ # A form object used to import a participatory processes from the admin
7
+ # dashboard.
8
+ #
9
+ class ParticipatoryProcessImportForm < Form
10
+ include TranslatableAttributes
11
+
12
+ JSON_MIME_TYPE = "application/json"
13
+ # Accepted mime types
14
+ # keys: are used for dynamic help text on admin form.
15
+ # values: are used to validate the file format of imported document.
16
+ ACCEPTED_TYPES = {
17
+ json: JSON_MIME_TYPE
18
+ }.freeze
19
+
20
+ translatable_attribute :title, String
21
+
22
+ mimic :participatory_process
23
+
24
+ attribute :slug, String
25
+ attribute :import_steps, Boolean, default: true
26
+ attribute :import_categories, Boolean, default: true
27
+ attribute :import_attachments, Boolean, default: true
28
+ attribute :import_components, Boolean, default: true
29
+ attribute :document
30
+
31
+ validates :document, presence: true
32
+
33
+ validates :slug, presence: true, format: { with: Decidim::ParticipatoryProcess.slug_format }
34
+ validates :title, translatable_presence: true
35
+ validate :slug_uniqueness
36
+
37
+ validate :document_type_must_be_valid, if: :document
38
+
39
+ def document_text
40
+ @document_text ||= document&.read
41
+ end
42
+
43
+ def document_type_must_be_valid
44
+ return if valid_mime_types.include?(document_type)
45
+
46
+ errors.add(:document, i18n_invalid_document_type_text)
47
+ end
48
+
49
+ # Return ACCEPTED_MIME_TYPES plus `text/plain` for better markdown support
50
+ def valid_mime_types
51
+ ACCEPTED_TYPES.values
52
+ end
53
+
54
+ def document_type
55
+ document.content_type
56
+ end
57
+
58
+ def i18n_invalid_document_type_text
59
+ I18n.t("invalid_document_type",
60
+ scope: "activemodel.errors.models.participatory_process.attributes.document",
61
+ valid_mime_types: i18n_valid_mime_types_text)
62
+ end
63
+
64
+ def i18n_valid_mime_types_text
65
+ ACCEPTED_TYPES.keys.map do |mime_type|
66
+ I18n.t(mime_type, scope: "decidim.participatory_processes.admin.new_import.accepted_types")
67
+ end.join(", ")
68
+ end
69
+
70
+ private
71
+
72
+ def slug_uniqueness
73
+ return unless OrganizationParticipatoryProcesses.new(current_organization).query.where(slug: slug).where.not(id: id).any?
74
+
75
+ errors.add(:slug, :taken)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -18,6 +18,7 @@ module Decidim
18
18
  include Decidim::HasPrivateUsers
19
19
  include Decidim::Loggable
20
20
  include Decidim::ParticipatorySpaceResourceable
21
+ include Decidim::Searchable
21
22
 
22
23
  belongs_to :organization,
23
24
  foreign_key: "decidim_organization_id",
@@ -63,6 +64,18 @@ module Decidim
63
64
  scope :upcoming, -> { where(arel_table[:start_date].gt(Date.current)) }
64
65
  scope :active, -> { where(arel_table[:start_date].lteq(Date.current).and(arel_table[:end_date].gt(Date.current).or(arel_table[:end_date].eq(nil)))) }
65
66
 
67
+ searchable_fields({
68
+ scope_id: :decidim_scope_id,
69
+ participatory_space: :itself,
70
+ A: :title,
71
+ B: :subtitle,
72
+ C: :short_description,
73
+ D: :description,
74
+ datetime: :published_at
75
+ },
76
+ index_on_create: ->(_process) { false },
77
+ index_on_update: ->(process) { process.visible? })
78
+
66
79
  # Scope to return only the promoted processes.
67
80
  #
68
81
  # Returns an ActiveRecord::Relation.
@@ -223,7 +223,9 @@ module Decidim
223
223
  :moderation,
224
224
  :process,
225
225
  :process_step,
226
- :process_user_role
226
+ :process_user_role,
227
+ :export_space,
228
+ :import
227
229
  ].include?(permission_action.subject)
228
230
  allow! if is_allowed
229
231
  end
@@ -241,7 +243,9 @@ module Decidim
241
243
  :process,
242
244
  :process_step,
243
245
  :process_user_role,
244
- :space_private_user
246
+ :space_private_user,
247
+ :export_space,
248
+ :import
245
249
  ].include?(permission_action.subject)
246
250
  allow! if is_allowed
247
251
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ParticipatoryProcesses
5
+ class ParticipatoryProcessGroupPresenter < SimpleDelegator
6
+ include Rails.application.routes.mounted_helpers
7
+ include ActionView::Helpers::UrlHelper
8
+
9
+ delegate :url, to: :hero_image, prefix: true
10
+
11
+ def hero_image_url
12
+ return if process_group.blank?
13
+
14
+ URI.join(decidim.root_url(host: process_group.organization.host), process_group.try(:hero_image_url)).to_s
15
+ end
16
+
17
+ def process_group
18
+ __getobj__
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ParticipatoryProcesses
5
+ class ParticipatoryProcessPresenter < SimpleDelegator
6
+ include Rails.application.routes.mounted_helpers
7
+ include ActionView::Helpers::UrlHelper
8
+
9
+ delegate :url, to: :hero_image, prefix: true
10
+ delegate :url, to: :banner_image, prefix: true
11
+
12
+ def hero_image_url
13
+ URI.join(decidim.root_url(host: process.organization.host), process.hero_image_url).to_s
14
+ end
15
+
16
+ def banner_image_url
17
+ URI.join(decidim.root_url(host: process.organization.host), process.banner_image_url).to_s
18
+ end
19
+
20
+ def process
21
+ __getobj__
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ParticipatoryProcesses
5
+ # A factory class to ensure we always create ParticipatoryProcesses the same way since it involves some logic.
6
+ class ParticipatoryProcessImporter < Decidim::Importers::Importer
7
+ def initialize(organization, user)
8
+ @organization = organization
9
+ @user = user
10
+ end
11
+
12
+ # Public: Creates a new ParticipatoryProcess.
13
+ #
14
+ # attributes - The Hash of attributes to create the ParticipatoryProcess with.
15
+ # user - The user that performs the action.
16
+ # opts - The options MUST contain:
17
+ # - title: The +title+ for the new PartidicpatoryProcess
18
+ # - slug: The +slug+ for the new PartidicpatoryProcess
19
+ #
20
+ # Returns a ParticipatoryProcess.
21
+ def import(attributes, _user, opts)
22
+ title = opts[:title]
23
+ slug = opts[:slug]
24
+ Decidim.traceability.perform_action!(:create, ParticipatoryProcess, @user, visibility: "all") do
25
+ @imported_process = ParticipatoryProcess.new(
26
+ organization: @organization,
27
+ title: title,
28
+ slug: slug,
29
+ subtitle: attributes["subtitle"],
30
+ hashtag: attributes["hashtag"],
31
+ description: attributes["description"],
32
+ short_description: attributes["short_description"],
33
+ promoted: attributes["promoted"],
34
+ developer_group: attributes["developer_group"],
35
+ local_area: attributes["local_area"],
36
+ target: attributes["target"],
37
+ participatory_scope: attributes["participatory_scope"],
38
+ participatory_structure: attributes["participatory_structure"],
39
+ meta_scope: attributes["meta_scope"],
40
+ start_date: attributes["start_date"],
41
+ end_date: attributes["end_date"],
42
+ private_space: attributes["private_space"],
43
+ participatory_process_group: import_process_group(attributes["participatory_process_group"])
44
+ )
45
+ @imported_process.remote_hero_image_url = attributes["remote_hero_image_url"] if remote_file_exists?(attributes["remote_hero_image_url"])
46
+ @imported_process.remote_banner_image_url = attributes["remote_banner_image_url"] if remote_file_exists?(attributes["remote_banner_image_url"])
47
+ @imported_process.save!
48
+ @imported_process
49
+ end
50
+ end
51
+
52
+ def import_process_group(attributes)
53
+ Decidim.traceability.perform_action!("create", ParticipatoryProcessGroup, @user) do
54
+ group = ParticipatoryProcessGroup.find_or_initialize_by(
55
+ name: attributes["name"],
56
+ description: attributes["description"],
57
+ organization: @organization
58
+ )
59
+
60
+ group.remote_hero_image_url = attributes["remote_hero_image_url"] if remote_file_exists?(attributes["remote_hero_image_url"])
61
+ group.save!
62
+ group
63
+ end
64
+ end
65
+
66
+ def import_participatory_process_steps(steps)
67
+ return if steps.nil?
68
+
69
+ steps.map do |step_attributes|
70
+ Decidim.traceability.create!(
71
+ ParticipatoryProcessStep,
72
+ @user,
73
+ title: step_attributes["title"],
74
+ description: step_attributes["description"],
75
+ start_date: step_attributes["start_date"],
76
+ end_date: step_attributes["end_date"],
77
+ participatory_process: @imported_process,
78
+ active: step_attributes["active"],
79
+ position: step_attributes["position"]
80
+ )
81
+ end
82
+ end
83
+
84
+ def import_categories(categories)
85
+ return if categories.nil?
86
+
87
+ categories.map do |category_attributes|
88
+ category = Decidim.traceability.create!(
89
+ Category,
90
+ @user,
91
+ name: category_attributes["name"],
92
+ description: category_attributes["description"],
93
+ parent_id: category_attributes["parent_id"],
94
+ participatory_space: @imported_process
95
+ )
96
+ next if category_attributes["subcategories"].nil?
97
+
98
+ category_attributes["subcategories"].map do |subcategory_attributes|
99
+ Decidim.traceability.create!(
100
+ Category,
101
+ @user,
102
+ name: subcategory_attributes["name"],
103
+ description: subcategory_attributes["description"],
104
+ parent_id: category.id,
105
+ participatory_space: @imported_process
106
+ )
107
+ end
108
+ end
109
+ end
110
+
111
+ def import_folders_and_attachments(attachments)
112
+ return if attachments["files"].nil?
113
+
114
+ attachments["files"].map do |file|
115
+ next unless remote_file_exists?(file["remote_file_url"])
116
+
117
+ file_tmp = URI.open(file["remote_file_url"])
118
+
119
+ Decidim.traceability.perform_action!("create", Attachment, @user) do
120
+ attachment = Attachment.new(
121
+ title: file["title"],
122
+ description: file["description"],
123
+ file: file_tmp,
124
+ file_size: file_tmp.size,
125
+ content_type: file_tmp.content_type,
126
+ attached_to: @imported_process,
127
+ weight: file["weight"]
128
+ )
129
+ attachment.create_attachment_collection(file["attachment_collection"])
130
+ attachment.save!
131
+ attachment
132
+ end
133
+ end
134
+
135
+ attachments["attachment_collections"].map do |collection|
136
+ Decidim.traceability.perform_action!("create", AttachmentCollection, @user) do
137
+ create_attachment_collection(collection)
138
+ end
139
+ end
140
+ end
141
+
142
+ # +components+: An Array of Hashes, each corresponding with the settings of a Decidim::Component.
143
+ def import_components(components)
144
+ return if components.nil?
145
+
146
+ importer = Decidim::Importers::ParticipatorySpaceComponentsImporter.new(@imported_process)
147
+ importer.import(components, @user)
148
+ end
149
+
150
+ private
151
+
152
+ def create_attachment_collection(attributes)
153
+ return unless attributes.compact.any?
154
+
155
+ attachment_collection = AttachmentCollection.find_or_initialize_by(
156
+ name: attributes["name"],
157
+ weight: attributes["weight"],
158
+ description: attributes["description"],
159
+ collection_for: @imported_process
160
+ )
161
+ attachment_collection.save!
162
+ attachment_collection
163
+ end
164
+
165
+ def remote_file_exists?(url)
166
+ return if url.nil?
167
+
168
+ accepted = ["image", "application/pdf"]
169
+ url = URI.parse(url)
170
+ Net::HTTP.start(url.host, url.port) do |http|
171
+ return http.head(url.request_uri)["Content-Type"].start_with?(*accepted)
172
+ end
173
+ rescue StandardError
174
+ nil
175
+ end
176
+ end
177
+ end
178
+ end