decidim-admin 0.25.2 → 0.26.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of decidim-admin might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/app/commands/decidim/admin/create_import_example.rb +21 -0
- data/app/commands/decidim/admin/create_static_page.rb +9 -3
- data/app/commands/decidim/admin/update_organization_appearance.rb +2 -1
- data/app/controllers/concerns/decidim/admin/user_groups/filterable.rb +45 -0
- data/app/controllers/concerns/decidim/moderated_users/admin/filterable.rb +51 -0
- data/app/controllers/decidim/admin/imports_controller.rb +48 -10
- data/app/controllers/decidim/admin/moderated_users_controller.rb +2 -2
- data/app/controllers/decidim/admin/scopes_controller.rb +2 -1
- data/app/controllers/decidim/admin/user_groups_controller.rb +13 -5
- data/app/forms/decidim/admin/import_example_form.rb +50 -0
- data/app/forms/decidim/admin/import_form.rb +46 -40
- data/app/forms/decidim/admin/organization_appearance_form.rb +1 -0
- data/app/helpers/decidim/admin/imports_helper.rb +16 -6
- data/app/helpers/decidim/admin/resource_scope_helper.rb +9 -0
- data/app/helpers/decidim/admin/settings_helper.rb +13 -0
- data/app/packs/entrypoints/decidim_admin.js +0 -1
- data/app/packs/src/decidim/admin/dynamic_fields.component.js +8 -0
- data/app/permissions/decidim/admin/permissions.rb +1 -0
- data/app/views/decidim/admin/imports/_dropdown.html.erb +9 -4
- data/app/views/decidim/admin/imports/new.html.erb +24 -22
- data/app/views/decidim/admin/moderated_users/_report.html.erb +0 -1
- data/app/views/decidim/admin/moderated_users/index.html.erb +5 -2
- data/app/views/decidim/admin/moderations/_report.html.erb +0 -1
- data/app/views/decidim/admin/moderations/index.html.erb +1 -1
- data/app/views/decidim/admin/organization_appearance/form/_colors.html.erb +7 -0
- data/app/views/decidim/admin/participatory_space_private_users/index.html.erb +12 -4
- data/app/views/decidim/admin/user_groups/index.html.erb +8 -44
- data/app/views/layouts/decidim/admin/_callouts_full.html.erb +4 -0
- data/app/views/layouts/decidim/admin/_title_bar.html.erb +2 -2
- data/config/brakeman.ignore +26 -0
- data/config/locales/ar.yml +0 -7
- data/config/locales/ca.yml +0 -13
- data/config/locales/cs.yml +50 -13
- data/config/locales/de.yml +8 -13
- data/config/locales/el.yml +0 -15
- data/config/locales/en.yml +46 -17
- data/config/locales/es-MX.yml +0 -13
- data/config/locales/es-PY.yml +0 -13
- data/config/locales/es.yml +45 -16
- data/config/locales/eu.yml +8 -13
- data/config/locales/fi-plain.yml +33 -13
- data/config/locales/fi.yml +46 -17
- data/config/locales/fr-CA.yml +42 -13
- data/config/locales/fr.yml +48 -19
- data/config/locales/ga-IE.yml +1 -6
- data/config/locales/gl.yml +33 -13
- data/config/locales/hu.yml +1 -9
- data/config/locales/id-ID.yml +0 -7
- data/config/locales/is-IS.yml +0 -7
- data/config/locales/it.yml +6 -13
- data/config/locales/ja.yml +92 -67
- data/config/locales/lb-LU.yml +1034 -0
- data/config/locales/lb.yml +6 -15
- data/config/locales/lv.yml +0 -7
- data/config/locales/nl.yml +73 -13
- data/config/locales/no.yml +0 -7
- data/config/locales/pl.yml +7 -13
- data/config/locales/pt-BR.yml +1 -14
- data/config/locales/pt.yml +8 -15
- data/config/locales/ro-RO.yml +40 -16
- data/config/locales/ru.yml +0 -7
- data/config/locales/sk.yml +0 -7
- data/config/locales/sr-CS.yml +0 -6
- data/config/locales/sv.yml +33 -13
- data/config/locales/tr-TR.yml +0 -7
- data/config/locales/uk.yml +0 -7
- data/config/locales/val-ES.yml +14 -0
- data/config/locales/zh-CN.yml +0 -7
- data/lib/decidim/admin/engine.rb +5 -0
- data/lib/decidim/admin/form_builder.rb +8 -1
- data/lib/decidim/admin/import/creator.rb +32 -12
- data/lib/decidim/admin/import/importer.rb +32 -14
- data/lib/decidim/admin/import/readers/base.rb +23 -0
- data/lib/decidim/admin/import/readers/csv.rb +15 -0
- data/lib/decidim/admin/import/readers/json.rb +73 -3
- data/lib/decidim/admin/import/readers/xlsx.rb +25 -1
- data/lib/decidim/admin/import/readers.rb +7 -1
- data/lib/decidim/admin/import/verifier.rb +169 -0
- data/lib/decidim/admin/import.rb +3 -0
- data/lib/decidim/admin/test/filters_participatory_space_user_roles_examples.rb +165 -0
- data/lib/decidim/admin/test/manage_paginated_collection_examples.rb +8 -2
- data/lib/decidim/admin/test.rb +1 -0
- data/lib/decidim/admin/version.rb +1 -1
- metadata +19 -12
- data/app/packs/src/decidim/admin/import_guidance.js +0 -28
@@ -63,7 +63,14 @@ module Decidim
|
|
63
63
|
|
64
64
|
# Calls Decidim::FormBuilder#editor with default options for admin.
|
65
65
|
def editor(name, options = {})
|
66
|
-
super(
|
66
|
+
super(
|
67
|
+
name,
|
68
|
+
{
|
69
|
+
toolbar: :full,
|
70
|
+
lines: 25,
|
71
|
+
editor_images: true
|
72
|
+
}.merge(options)
|
73
|
+
)
|
67
74
|
end
|
68
75
|
end
|
69
76
|
end
|
@@ -11,6 +11,33 @@ module Decidim
|
|
11
11
|
# in order to parse relevant fields. Every import should specify their
|
12
12
|
# own creator or this default will be used.
|
13
13
|
class Creator
|
14
|
+
class << self
|
15
|
+
# Retuns the resource class to be created with the provided data.
|
16
|
+
def resource_klass
|
17
|
+
raise NotImplementedError, "#{self.class.name} does not define resource class"
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the verifier class to be used to ensure the data is valid
|
21
|
+
# for the import.
|
22
|
+
def verifier_klass
|
23
|
+
Decidim::Admin::Import::Verifier
|
24
|
+
end
|
25
|
+
|
26
|
+
def required_headers
|
27
|
+
[]
|
28
|
+
end
|
29
|
+
|
30
|
+
def localize_headers(header, locales)
|
31
|
+
@localize_headers ||= begin
|
32
|
+
localize_headers = []
|
33
|
+
locales.each do |locale|
|
34
|
+
localize_headers << "#{header}/#{locale}".to_sym
|
35
|
+
end
|
36
|
+
localize_headers
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
14
41
|
attr_reader :data
|
15
42
|
|
16
43
|
# Initializes the creator with a resource.
|
@@ -18,15 +45,10 @@ module Decidim
|
|
18
45
|
# data - The data hash to parse.
|
19
46
|
# context - The context needed by the producer
|
20
47
|
def initialize(data, context = nil)
|
21
|
-
@data = data
|
48
|
+
@data = data
|
22
49
|
@context = context
|
23
50
|
end
|
24
51
|
|
25
|
-
# Retuns the resource class to be created with the provided data.
|
26
|
-
def self.resource_klass
|
27
|
-
raise NotImplementedError, "#{self.class.name} does not define resource class"
|
28
|
-
end
|
29
|
-
|
30
52
|
# Can be used to convert the data hash to the resource attributes in
|
31
53
|
# case the data hash to be imported has different column names than the
|
32
54
|
# resource object to be created of it.
|
@@ -50,7 +72,9 @@ module Decidim
|
|
50
72
|
resource.save!
|
51
73
|
end
|
52
74
|
|
53
|
-
|
75
|
+
protected
|
76
|
+
|
77
|
+
attr_reader :context
|
54
78
|
|
55
79
|
def resource
|
56
80
|
raise NotImplementedError, "#{self.class.name} does not define resource"
|
@@ -65,14 +89,10 @@ module Decidim
|
|
65
89
|
# Returns the hash including locale-imported_data pairs. eg. {en: "Heading", ca: "Cap", es: "Bóveda"}
|
66
90
|
#
|
67
91
|
def locale_hasher(field, locales)
|
68
|
-
return data[field.to_sym] if data.has_key?(field.to_sym)
|
69
|
-
|
70
92
|
hash = {}
|
71
93
|
locales.each do |locale|
|
72
94
|
parsed = data[:"#{field}/#{locale}"]
|
73
|
-
|
74
|
-
|
75
|
-
hash[locale] = parsed
|
95
|
+
hash[locale] = parsed unless parsed.nil?
|
76
96
|
end
|
77
97
|
hash
|
78
98
|
end
|
@@ -10,17 +10,24 @@ module Decidim
|
|
10
10
|
# You can also use the ImporterFactory class to create an Importer
|
11
11
|
# instance.
|
12
12
|
class Importer
|
13
|
+
delegate :errors, to: :verifier
|
14
|
+
|
13
15
|
# Public: Initializes an Importer.
|
14
16
|
#
|
15
17
|
# file - A file with the data to be imported.
|
16
18
|
# reader - A Reader to be used to read the data from the file.
|
17
|
-
# creator - A Creator to be used during the import.
|
19
|
+
# creator - A Creator class to be used during the import.
|
18
20
|
# context - A hash including component specific data.
|
19
21
|
def initialize(file:, reader: Readers::Base, creator: Creator, context: nil)
|
20
22
|
@file = file
|
21
23
|
@reader = reader
|
22
24
|
@creator = creator
|
23
25
|
@context = context
|
26
|
+
@data_headers = []
|
27
|
+
end
|
28
|
+
|
29
|
+
def verify
|
30
|
+
verifier.valid?
|
24
31
|
end
|
25
32
|
|
26
33
|
# Import data and create resources
|
@@ -40,27 +47,38 @@ module Decidim
|
|
40
47
|
@collection ||= collection_data.map { |item| creator.new(item, context) }
|
41
48
|
end
|
42
49
|
|
43
|
-
|
44
|
-
|
45
|
-
|
50
|
+
def invalid_file?
|
51
|
+
collection.blank?
|
52
|
+
rescue Decidim::Admin::Import::InvalidFileError
|
53
|
+
true
|
46
54
|
end
|
47
55
|
|
48
56
|
private
|
49
57
|
|
50
|
-
attr_reader :file, :reader, :creator, :context
|
58
|
+
attr_reader :file, :reader, :creator, :context, :data_headers
|
59
|
+
|
60
|
+
def verifier
|
61
|
+
# Prepare needs to be called so that data headers become available.
|
62
|
+
data = prepare
|
63
|
+
@verifier ||= creator.verifier_klass.new(
|
64
|
+
headers: data_headers.map(&:to_s),
|
65
|
+
data: data,
|
66
|
+
reader: reader,
|
67
|
+
context: context
|
68
|
+
)
|
69
|
+
end
|
51
70
|
|
52
71
|
def collection_data
|
53
72
|
return @collection_data if @collection_data
|
54
73
|
|
55
74
|
@collection_data = []
|
56
|
-
data_headers = []
|
57
75
|
reader.new(file).read_rows do |rowdata, index|
|
58
76
|
if index.zero?
|
59
|
-
data_headers = rowdata.map
|
77
|
+
@data_headers = rowdata.map { |d| d.to_s.to_sym }
|
60
78
|
else
|
61
79
|
@collection_data << Hash[
|
62
80
|
rowdata.each_with_index.map do |val, ind|
|
63
|
-
[data_headers[ind], val]
|
81
|
+
[@data_headers[ind], val]
|
64
82
|
end
|
65
83
|
]
|
66
84
|
end
|
@@ -69,12 +87,12 @@ module Decidim
|
|
69
87
|
@collection_data
|
70
88
|
end
|
71
89
|
|
72
|
-
def
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
90
|
+
def component
|
91
|
+
context[:current_component]
|
92
|
+
end
|
93
|
+
|
94
|
+
def available_locales
|
95
|
+
@available_locales ||= component.participatory_space.organization.available_locales
|
78
96
|
end
|
79
97
|
end
|
80
98
|
end
|
@@ -7,6 +7,13 @@ module Decidim
|
|
7
7
|
# Abstract class with a very naive default implementation. Each importable
|
8
8
|
# file type should have it's own reader.
|
9
9
|
class Base
|
10
|
+
# Defines which index of the records defines the first line of actual
|
11
|
+
# data. E.g. with spreadsheet formats, the first row contains column
|
12
|
+
# name information.
|
13
|
+
def self.first_data_index
|
14
|
+
0
|
15
|
+
end
|
16
|
+
|
10
17
|
def initialize(file)
|
11
18
|
@file = file
|
12
19
|
end
|
@@ -17,10 +24,26 @@ module Decidim
|
|
17
24
|
# which can be later used to map the data to correct attributes.
|
18
25
|
#
|
19
26
|
# This needs to be implemented by the extending classes.
|
27
|
+
#
|
28
|
+
# Returns an array of the import data where the first row should
|
29
|
+
# contain the columns.
|
20
30
|
def read_rows
|
21
31
|
raise NotImplementedError
|
22
32
|
end
|
23
33
|
|
34
|
+
# The example_file should produce an example data file for the user to
|
35
|
+
# download and take example from to produce their import files. The
|
36
|
+
# data provided for the example file generation should be the same as
|
37
|
+
# what is returned by the read_rows method.
|
38
|
+
#
|
39
|
+
# _data - An array of data to produce the file from
|
40
|
+
#
|
41
|
+
# Returns an IO stream that can be saved to a file or sent to the
|
42
|
+
# browser to produce the import file.
|
43
|
+
def example_file(_data)
|
44
|
+
raise NotImplementedError
|
45
|
+
end
|
46
|
+
|
24
47
|
protected
|
25
48
|
|
26
49
|
attr_reader :file
|
@@ -11,11 +11,26 @@ module Decidim
|
|
11
11
|
class CSV < Base
|
12
12
|
MIME_TYPE = "text/csv"
|
13
13
|
|
14
|
+
def self.first_data_index
|
15
|
+
1
|
16
|
+
end
|
17
|
+
|
14
18
|
def read_rows
|
15
19
|
::CSV.read(file, col_sep: ";").each_with_index do |row, index|
|
16
20
|
yield row, index
|
17
21
|
end
|
18
22
|
end
|
23
|
+
|
24
|
+
# Returns a StringIO
|
25
|
+
def example_file(data)
|
26
|
+
csv_data = ::CSV.generate(col_sep: ";") do |csv|
|
27
|
+
data.each do |row|
|
28
|
+
csv << row
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
::StringIO.new(csv_data)
|
33
|
+
end
|
19
34
|
end
|
20
35
|
end
|
21
36
|
end
|
@@ -13,9 +13,79 @@ module Decidim
|
|
13
13
|
|
14
14
|
def read_rows
|
15
15
|
json_string = File.read(file)
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
columns = []
|
17
|
+
data = ::JSON.parse(json_string)
|
18
|
+
data.each_with_index do |row, index|
|
19
|
+
row = flat_hash(row)
|
20
|
+
if index.zero?
|
21
|
+
columns = row.keys
|
22
|
+
yield columns.map(&:to_s), index
|
23
|
+
end
|
24
|
+
|
25
|
+
values = columns.map { |c| row[c] }
|
26
|
+
last_present = values.rindex { |v| !v.nil? }
|
27
|
+
if last_present
|
28
|
+
yield values[0..last_present], index + 1
|
29
|
+
else
|
30
|
+
yield [], index + 1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
rescue ::JSON::ParserError
|
34
|
+
raise Decidim::Admin::Import::InvalidFileError, "The provided JSON file is not valid"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns a StringIO
|
38
|
+
def example_file(data)
|
39
|
+
columns = data.shift
|
40
|
+
json_data = data.map do |row|
|
41
|
+
deep_hash(
|
42
|
+
columns.each_with_index.map { |col, ind| [col, row[ind]] }.to_h
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
::StringIO.new(::JSON.pretty_generate(json_data))
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Converts e.g. structure as follows:
|
52
|
+
# { title: { en: => "Foo", es: => "Bar" } }
|
53
|
+
#
|
54
|
+
# Into:
|
55
|
+
# { "title/en": "Foo", "title/es": "Bar" }
|
56
|
+
def flat_hash(data)
|
57
|
+
{}.tap do |final|
|
58
|
+
data.each do |key, value|
|
59
|
+
if value.is_a?(Hash)
|
60
|
+
flat_hash(value).each do |subkey, subvalue|
|
61
|
+
final["#{key}/#{subkey}".to_sym] = subvalue
|
62
|
+
end
|
63
|
+
else
|
64
|
+
final[key.to_sym] = value
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Converts e.g. structure as follows:
|
71
|
+
# { "title/en": "Foo", "title/es": "Bar" }
|
72
|
+
#
|
73
|
+
# Into:
|
74
|
+
# { title: { en: "Foo", es: "Bar" } }
|
75
|
+
def deep_hash(data)
|
76
|
+
{}.tap do |final|
|
77
|
+
data.each do |key, value|
|
78
|
+
keyparts = key.to_s.split("/")
|
79
|
+
current = final
|
80
|
+
while (keypart = keyparts.shift&.to_sym)
|
81
|
+
if keyparts.any?
|
82
|
+
current[keypart] ||= {}
|
83
|
+
current = current[keypart]
|
84
|
+
else
|
85
|
+
current[keypart] = value
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
19
89
|
end
|
20
90
|
end
|
21
91
|
end
|
@@ -11,12 +11,36 @@ module Decidim
|
|
11
11
|
class XLSX < Base
|
12
12
|
MIME_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
13
13
|
|
14
|
+
def self.first_data_index
|
15
|
+
1
|
16
|
+
end
|
17
|
+
|
14
18
|
def read_rows
|
15
19
|
workbook = RubyXL::Parser.parse(file)
|
16
20
|
sheet = workbook.worksheets[0]
|
17
21
|
sheet.each_with_index do |row, index|
|
18
|
-
|
22
|
+
if row
|
23
|
+
yield row.cells.map { |c| c && c.value }, index
|
24
|
+
else
|
25
|
+
yield [], index
|
26
|
+
end
|
27
|
+
end
|
28
|
+
rescue Zip::Error
|
29
|
+
raise Decidim::Admin::Import::InvalidFileError, "The provided XLSX file is not valid"
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns a StringIO
|
33
|
+
def example_file(data)
|
34
|
+
workbook = RubyXL::Workbook.new
|
35
|
+
sheet = workbook.worksheets[0]
|
36
|
+
|
37
|
+
data.each_with_index do |row, rowi|
|
38
|
+
row.each_with_index do |col, coli|
|
39
|
+
sheet.add_cell(rowi, coli, col)
|
40
|
+
end
|
19
41
|
end
|
42
|
+
|
43
|
+
workbook.stream
|
20
44
|
end
|
21
45
|
end
|
22
46
|
end
|
@@ -13,8 +13,8 @@ module Decidim
|
|
13
13
|
# keys: are used for dynamic help text on admin form.
|
14
14
|
# values: are used to validate the file format of imported document.
|
15
15
|
ACCEPTED_MIME_TYPES = {
|
16
|
-
json: Readers::JSON::MIME_TYPE,
|
17
16
|
csv: Readers::CSV::MIME_TYPE,
|
17
|
+
json: Readers::JSON::MIME_TYPE,
|
18
18
|
xlsx: Readers::XLSX::MIME_TYPE
|
19
19
|
}.freeze
|
20
20
|
|
@@ -33,6 +33,12 @@ module Decidim
|
|
33
33
|
|
34
34
|
nil
|
35
35
|
end
|
36
|
+
|
37
|
+
def self.search_by_file_extension(extension)
|
38
|
+
return unless ACCEPTED_MIME_TYPES.has_key?(extension.to_sym)
|
39
|
+
|
40
|
+
search_by_mime_type(ACCEPTED_MIME_TYPES[extension.to_sym])
|
41
|
+
end
|
36
42
|
end
|
37
43
|
end
|
38
44
|
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Admin
|
5
|
+
module Import
|
6
|
+
# This is the default verifier class that verifies the import data is
|
7
|
+
# valid before starting the import process. It makes sure the data is in
|
8
|
+
# correct format, contains the correct data headers, etc.
|
9
|
+
#
|
10
|
+
# Individual importers can extend this class to customize the verification
|
11
|
+
# process.
|
12
|
+
class Verifier
|
13
|
+
include ActiveModel::Validations
|
14
|
+
|
15
|
+
validate :validate_headers
|
16
|
+
validate :validate_data, if: -> { errors.blank? }
|
17
|
+
|
18
|
+
# Public: Initializes an Importer.
|
19
|
+
#
|
20
|
+
# headers - An array of the data headers for the import.
|
21
|
+
# data - An array of the generated data records to be imported.
|
22
|
+
# reader - A Reader class that was used to read the raw data.
|
23
|
+
# context - A hash including component specific data.
|
24
|
+
def initialize(headers:, data:, reader:, context: nil)
|
25
|
+
@headers = headers
|
26
|
+
@data = data
|
27
|
+
@reader = reader
|
28
|
+
@context = context
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
attr_reader :headers, :data, :reader, :context
|
34
|
+
|
35
|
+
def validate_headers
|
36
|
+
if missing_headers.any?
|
37
|
+
message = [
|
38
|
+
I18n.t(
|
39
|
+
"decidim.admin.imports.data_errors.missing_headers.message",
|
40
|
+
count: missing_headers.count,
|
41
|
+
columns: humanize_list(missing_headers)
|
42
|
+
),
|
43
|
+
I18n.t("decidim.admin.imports.data_errors.missing_headers.detail")
|
44
|
+
].join(" ")
|
45
|
+
|
46
|
+
errors.add(:headers, message)
|
47
|
+
end
|
48
|
+
|
49
|
+
return unless duplicate_headers.any?
|
50
|
+
|
51
|
+
message = [
|
52
|
+
I18n.t(
|
53
|
+
"decidim.admin.imports.data_errors.duplicate_headers.message",
|
54
|
+
count: duplicate_headers.count,
|
55
|
+
columns: humanize_list(duplicate_headers)
|
56
|
+
),
|
57
|
+
I18n.t("decidim.admin.imports.data_errors.duplicate_headers.detail")
|
58
|
+
].join(" ")
|
59
|
+
|
60
|
+
errors.add(:headers, message)
|
61
|
+
end
|
62
|
+
|
63
|
+
def validate_data
|
64
|
+
return if invalid_indexes.empty?
|
65
|
+
|
66
|
+
indexes = humanize_indexes(invalid_indexes, reader.first_data_index)
|
67
|
+
message =
|
68
|
+
if reader.first_data_index.zero?
|
69
|
+
# If the data starts from index zero we don't want to say to the
|
70
|
+
# user that there are errors on "rows". We want to refer to record
|
71
|
+
# numbers instead. This is the case e.g. with JSON data format.
|
72
|
+
[
|
73
|
+
I18n.t(
|
74
|
+
"decidim.admin.imports.data_errors.invalid_indexes.records.message",
|
75
|
+
count: invalid_indexes.count,
|
76
|
+
indexes: indexes
|
77
|
+
),
|
78
|
+
I18n.t("decidim.admin.imports.data_errors.invalid_indexes.records.detail")
|
79
|
+
].join(" ")
|
80
|
+
else
|
81
|
+
[
|
82
|
+
I18n.t(
|
83
|
+
"decidim.admin.imports.data_errors.invalid_indexes.lines.message",
|
84
|
+
count: invalid_indexes.count,
|
85
|
+
indexes: indexes
|
86
|
+
),
|
87
|
+
I18n.t("decidim.admin.imports.data_errors.invalid_indexes.lines.detail")
|
88
|
+
].join(" ")
|
89
|
+
end
|
90
|
+
|
91
|
+
errors.add(:data, message)
|
92
|
+
end
|
93
|
+
|
94
|
+
def available_locales
|
95
|
+
@available_locales ||= context[:current_organization]&.available_locales || I18n.available_locales.map(&:to_s)
|
96
|
+
end
|
97
|
+
|
98
|
+
def default_locale
|
99
|
+
@default_locale ||= context[:current_organization]&.default_locale || I18n.default_locale.to_s
|
100
|
+
end
|
101
|
+
|
102
|
+
# Individual verifier classes can extend this to provide their required
|
103
|
+
# headers.
|
104
|
+
#
|
105
|
+
# Returns an array of required headers.
|
106
|
+
def required_headers
|
107
|
+
[]
|
108
|
+
end
|
109
|
+
|
110
|
+
def required_localized_headers(name)
|
111
|
+
["#{name}/#{default_locale}"]
|
112
|
+
end
|
113
|
+
|
114
|
+
def missing_headers
|
115
|
+
@missing_headers ||= [].tap do |array|
|
116
|
+
required_headers.each do |required|
|
117
|
+
array << required unless headers.include?(required)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def duplicate_headers
|
123
|
+
@duplicate_headers ||= headers.select { |e| headers.count(e) > 1 }.uniq
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns array of all resource indexes where validations fail.
|
127
|
+
def invalid_indexes
|
128
|
+
@invalid_indexes ||= [].tap do |indexes|
|
129
|
+
data.each_with_index do |record, index|
|
130
|
+
indexes << index unless valid_record?(record)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Validates the record and allows individual verifiers to customize the
|
136
|
+
# validation logic by overriding this method.
|
137
|
+
#
|
138
|
+
# Returns a boolean indicating whether the record to be imported is
|
139
|
+
# valid.
|
140
|
+
def valid_record?(record)
|
141
|
+
record.valid?
|
142
|
+
end
|
143
|
+
|
144
|
+
# Humanizes the index numbers so that it is understandable for humans.
|
145
|
+
# Index zero becomes one and the indexes are included in a single
|
146
|
+
# string with the last item separated with "and". For instance, for
|
147
|
+
# indexes [1, 2, 3] the message would be "1, 2 and 3".
|
148
|
+
#
|
149
|
+
# Returns a String.
|
150
|
+
def humanize_indexes(indexes, start_index)
|
151
|
+
# Humans don't start counting from zero and this message is shown
|
152
|
+
# for humans. This also takes the data start index into account.
|
153
|
+
indexes = indexes.map { |i| i + start_index + 1 }
|
154
|
+
|
155
|
+
humanize_list(indexes)
|
156
|
+
end
|
157
|
+
|
158
|
+
def humanize_list(list)
|
159
|
+
if list.count > 1
|
160
|
+
last = list.pop
|
161
|
+
"#{list.join(", ")} #{I18n.t("decidim.admin.imports.and")} #{last}"
|
162
|
+
else
|
163
|
+
list.join
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
data/lib/decidim/admin/import.rb
CHANGED
@@ -7,6 +7,9 @@ module Decidim
|
|
7
7
|
autoload :Importer, "decidim/admin/import/importer"
|
8
8
|
autoload :Creator, "decidim/admin/import/creator"
|
9
9
|
autoload :Readers, "decidim/admin/import/readers"
|
10
|
+
autoload :Verifier, "decidim/admin/import/verifier"
|
11
|
+
|
12
|
+
class InvalidFileError < StandardError; end
|
10
13
|
end
|
11
14
|
end
|
12
15
|
end
|