decidim-participatory_processes 0.19.1 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
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