iron-cms 0.5.2 → 0.7.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.
- checksums.yaml +4 -4
- data/app/assets/builds/iron.css +682 -388
- data/app/assets/tailwind/iron/application.css +1 -0
- data/app/assets/tailwind/iron/components/button.css +0 -7
- data/app/assets/tailwind/iron/components/checkbox.css +21 -0
- data/app/assets/tailwind/iron/components/form.css +1 -1
- data/app/assets/tailwind/iron/lexxy.css +165 -51
- data/app/controllers/iron/account/exports_controller.rb +26 -0
- data/app/controllers/iron/account/imports_controller.rb +27 -0
- data/app/helpers/iron/form_builder.rb +7 -0
- data/app/javascript/iron/controllers/local_preference_controller.js +62 -0
- data/app/jobs/iron/export_job.rb +9 -0
- data/app/jobs/iron/import_job.rb +9 -0
- data/app/models/concerns/iron/broadcastable.rb +9 -0
- data/app/models/concerns/iron/processable.rb +34 -0
- data/app/models/iron/account/export.rb +86 -0
- data/app/models/iron/account/import.rb +208 -0
- data/app/models/iron/block_definition/exportable.rb +14 -0
- data/app/models/iron/block_definition/importable.rb +27 -0
- data/app/models/iron/block_definition.rb +1 -1
- data/app/models/iron/content_type/exportable.rb +20 -0
- data/app/models/iron/content_type/importable.rb +32 -0
- data/app/models/iron/content_type.rb +1 -1
- data/app/models/iron/current.rb +6 -3
- data/app/models/iron/entry/exportable.rb +49 -0
- data/app/models/iron/entry/importable.rb +181 -0
- data/app/models/iron/entry.rb +1 -1
- data/app/models/iron/field.rb +9 -1
- data/app/models/iron/field_definition/exportable.rb +23 -0
- data/app/models/iron/field_definition/importable.rb +39 -0
- data/app/models/iron/field_definition.rb +1 -1
- data/app/models/iron/fields/block.rb +34 -0
- data/app/models/iron/fields/block_list.rb +8 -0
- data/app/models/iron/fields/boolean.rb +4 -0
- data/app/models/iron/fields/date.rb +4 -0
- data/app/models/iron/fields/file.rb +16 -0
- data/app/models/iron/fields/number.rb +4 -0
- data/app/models/iron/fields/reference.rb +4 -0
- data/app/models/iron/fields/reference_list.rb +4 -0
- data/app/models/iron/fields/rich_text_area.rb +32 -0
- data/app/models/iron/fields/text_area.rb +4 -0
- data/app/models/iron/fields/text_field.rb +4 -0
- data/app/models/iron/user.rb +2 -0
- data/app/views/iron/account/exports/index.html.erb +43 -0
- data/app/views/iron/account/exports/new.html.erb +39 -0
- data/app/views/iron/account/exports/show.html.erb +40 -0
- data/app/views/iron/account/imports/index.html.erb +43 -0
- data/app/views/iron/account/imports/new.html.erb +52 -0
- data/app/views/iron/account/imports/show.html.erb +37 -0
- data/app/views/iron/content_types/index.html.erb +1 -8
- data/app/views/iron/entries/fields/_block.html.erb +23 -10
- data/app/views/iron/entries/fields/_file.html.erb +3 -3
- data/app/views/iron/settings/show.html.erb +4 -11
- data/app/views/layouts/iron/application.html.erb +14 -0
- data/config/routes.rb +3 -9
- data/db/migrate/20251209103109_create_iron_account_exports.rb +13 -0
- data/db/migrate/20251209103110_create_iron_account_imports.rb +13 -0
- data/lib/iron/version.rb +1 -1
- data/lib/iron.rb +1 -1
- metadata +41 -28
- data/app/controllers/iron/contents_controller.rb +0 -33
- data/app/controllers/iron/schemas_controller.rb +0 -32
- data/app/models/concerns/iron/csv_serializable.rb +0 -28
- data/app/models/iron/archive.rb +0 -69
- data/app/models/iron/block_definition/portable.rb +0 -20
- data/app/models/iron/content_export.rb +0 -73
- data/app/models/iron/content_import/entry_builder.rb +0 -80
- data/app/models/iron/content_import/entry_snapshot.rb +0 -23
- data/app/models/iron/content_import/field_reconstructor.rb +0 -276
- data/app/models/iron/content_import/field_snapshot.rb +0 -33
- data/app/models/iron/content_import/registry.rb +0 -32
- data/app/models/iron/content_import/session.rb +0 -89
- data/app/models/iron/content_import.rb +0 -15
- data/app/models/iron/content_type/portable.rb +0 -30
- data/app/models/iron/entry/portable.rb +0 -35
- data/app/models/iron/field/portable.rb +0 -33
- data/app/models/iron/field_definition/portable.rb +0 -42
- data/app/models/iron/schema_archive.rb +0 -71
- data/app/models/iron/schema_exporter.rb +0 -15
- data/app/models/iron/schema_importer/import_strategy.rb +0 -59
- data/app/models/iron/schema_importer/merge_strategy.rb +0 -52
- data/app/models/iron/schema_importer/replace_strategy.rb +0 -51
- data/app/models/iron/schema_importer/safe_strategy.rb +0 -55
- data/app/models/iron/schema_importer.rb +0 -108
- data/app/views/iron/contents/new.html.erb +0 -34
- data/app/views/iron/schemas/new.html.erb +0 -57
- data/lib/iron/test_fixtures.rb +0 -50
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
module Iron
|
|
2
|
+
class Account::Import < ApplicationRecord
|
|
3
|
+
include Processable, Broadcastable
|
|
4
|
+
|
|
5
|
+
belongs_to :user, class_name: "Iron::User", default: -> { Current.user }
|
|
6
|
+
has_one_attached :file
|
|
7
|
+
|
|
8
|
+
def process_later
|
|
9
|
+
ImportJob.perform_later(self)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def title
|
|
13
|
+
created_at.strftime("%b %-d, %Y at %-l:%M %p")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def perform
|
|
19
|
+
raise ArgumentError, "Nothing selected to import" unless include_schema? || include_content?
|
|
20
|
+
|
|
21
|
+
import_from_zip
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def import_from_zip
|
|
25
|
+
Tempfile.create([ "import", ".zip" ]) do |tempfile|
|
|
26
|
+
tempfile.binmode
|
|
27
|
+
tempfile.write(file.download)
|
|
28
|
+
tempfile.rewind
|
|
29
|
+
|
|
30
|
+
Zip::File.open(tempfile.path) do |zip|
|
|
31
|
+
@zip = zip
|
|
32
|
+
@files_dir = extract_files_to_temp_dir(zip)
|
|
33
|
+
|
|
34
|
+
import_schema_from_zip if include_schema?
|
|
35
|
+
import_content_from_zip if include_content?
|
|
36
|
+
ensure
|
|
37
|
+
FileUtils.rm_rf(@files_dir) if @files_dir
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def extract_files_to_temp_dir(zip)
|
|
43
|
+
dir = Dir.mktmpdir("iron-import")
|
|
44
|
+
zip.glob("files/**/*").each do |entry|
|
|
45
|
+
next if entry.directory?
|
|
46
|
+
|
|
47
|
+
path = File.join(dir, entry.name.sub("files/", ""))
|
|
48
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
49
|
+
entry.extract(path)
|
|
50
|
+
end
|
|
51
|
+
dir
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def import_schema_from_zip
|
|
55
|
+
return unless @zip.find_entry("schema.json")
|
|
56
|
+
|
|
57
|
+
schema_json = @zip.read("schema.json")
|
|
58
|
+
schema = parse_json(schema_json)
|
|
59
|
+
raise ArgumentError, "Invalid schema JSON format" unless schema
|
|
60
|
+
|
|
61
|
+
ActiveRecord::Base.transaction do
|
|
62
|
+
import_block_definitions(schema[:block_definitions] || [])
|
|
63
|
+
import_content_types(schema[:content_types] || [])
|
|
64
|
+
resolve_content_type_references(schema[:content_types] || [])
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def import_content_from_zip
|
|
69
|
+
ActiveRecord::Base.transaction do
|
|
70
|
+
id_mapping = {}
|
|
71
|
+
entries_data = []
|
|
72
|
+
|
|
73
|
+
# First pass: read all entry files and create entries
|
|
74
|
+
@zip.glob("entries/**/*.json").each do |zip_entry|
|
|
75
|
+
attrs = parse_json(zip_entry.get_input_stream.read)
|
|
76
|
+
next unless attrs
|
|
77
|
+
|
|
78
|
+
entries_data << attrs
|
|
79
|
+
entry = Entry.import_from_attributes(attrs, @files_dir)
|
|
80
|
+
id_mapping[attrs[:id]] = entry if entry
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Second pass: resolve references
|
|
84
|
+
resolve_references(entries_data, id_mapping)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def resolve_references(entries_data, id_mapping)
|
|
89
|
+
entries_data.each do |attrs|
|
|
90
|
+
entry = id_mapping[attrs[:id]]
|
|
91
|
+
next unless entry
|
|
92
|
+
|
|
93
|
+
(attrs[:fields] || {}).each do |locale_code, field_data|
|
|
94
|
+
locale = Locale.find_by(code: locale_code.to_s)
|
|
95
|
+
next unless locale
|
|
96
|
+
|
|
97
|
+
resolve_references_in_fields(entry, locale, field_data, id_mapping)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def resolve_references_in_fields(entry, locale, fields_data, id_mapping, parent: nil)
|
|
103
|
+
fields_data.each do |handle, value_data|
|
|
104
|
+
next unless value_data.is_a?(Hash)
|
|
105
|
+
|
|
106
|
+
case value_data[:type]
|
|
107
|
+
when "reference"
|
|
108
|
+
resolve_reference_field(entry, locale, handle.to_s, value_data[:value], id_mapping, parent: parent)
|
|
109
|
+
when "reference_list"
|
|
110
|
+
resolve_reference_list_field(entry, locale, handle.to_s, value_data[:value], id_mapping, parent: parent)
|
|
111
|
+
when "block"
|
|
112
|
+
resolve_references_in_block(entry, locale, handle.to_s, value_data, id_mapping, parent: parent)
|
|
113
|
+
when "block_list"
|
|
114
|
+
resolve_references_in_block_list(entry, locale, handle.to_s, value_data, id_mapping, parent: parent)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def resolve_references_in_block(entry, locale, handle, value_data, id_mapping, parent: nil)
|
|
120
|
+
block_field = find_field(entry, locale, handle, parent)
|
|
121
|
+
return unless block_field
|
|
122
|
+
|
|
123
|
+
resolve_references_in_fields(entry, locale, value_data[:fields] || {}, id_mapping, parent: block_field)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def resolve_references_in_block_list(entry, locale, handle, value_data, id_mapping, parent: nil)
|
|
127
|
+
block_list_field = find_field(entry, locale, handle, parent)
|
|
128
|
+
return unless block_list_field
|
|
129
|
+
|
|
130
|
+
# Reload blocks and get them in rank order (matching import order)
|
|
131
|
+
ordered_blocks = block_list_field.blocks.reload.sort_by(&:rank)
|
|
132
|
+
|
|
133
|
+
(value_data[:value] || []).each_with_index do |block_data, index|
|
|
134
|
+
block = ordered_blocks[index]
|
|
135
|
+
next unless block
|
|
136
|
+
|
|
137
|
+
resolve_references_in_fields(entry, locale, block_data[:fields] || {}, id_mapping, parent: block)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def find_field(entry, locale, handle, parent)
|
|
142
|
+
# Reload associations to get freshly persisted fields from the first pass
|
|
143
|
+
fields = parent&.fields&.reload || entry.fields.reload
|
|
144
|
+
definition_scope = parent.is_a?(Fields::Block) ? parent.block_definition.field_definitions : entry.content_type.field_definitions
|
|
145
|
+
definition = definition_scope.find_by(handle: handle)
|
|
146
|
+
return unless definition
|
|
147
|
+
|
|
148
|
+
fields.find { |f| f.definition_id == definition.id && f.locale_id == locale.id }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def resolve_reference_field(entry, locale, handle, old_entry_id, id_mapping, parent: nil)
|
|
152
|
+
return unless old_entry_id
|
|
153
|
+
|
|
154
|
+
referenced_entry = id_mapping[old_entry_id]
|
|
155
|
+
return unless referenced_entry
|
|
156
|
+
|
|
157
|
+
field = find_field(entry, locale, handle, parent)
|
|
158
|
+
return unless field
|
|
159
|
+
|
|
160
|
+
field.update!(referenced_entry: referenced_entry)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def resolve_reference_list_field(entry, locale, handle, old_entry_ids, id_mapping, parent: nil)
|
|
164
|
+
return unless old_entry_ids.is_a?(Array)
|
|
165
|
+
|
|
166
|
+
field = find_field(entry, locale, handle, parent)
|
|
167
|
+
return unless field
|
|
168
|
+
|
|
169
|
+
old_entry_ids.each_with_index do |old_id, index|
|
|
170
|
+
referenced_entry = id_mapping[old_id]
|
|
171
|
+
next unless referenced_entry
|
|
172
|
+
|
|
173
|
+
field.references.create!(entry: referenced_entry, rank: index)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def parse_json(content)
|
|
178
|
+
JSON.parse(content, symbolize_names: true)
|
|
179
|
+
rescue JSON::ParserError
|
|
180
|
+
nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def import_block_definitions(definitions)
|
|
184
|
+
definitions.each { |attrs| BlockDefinition.import_from_attributes(attrs) }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def import_content_types(definitions)
|
|
188
|
+
definitions.each { |attrs| ContentType.import_from_attributes(attrs) }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def resolve_content_type_references(definitions)
|
|
192
|
+
definitions.each do |attrs|
|
|
193
|
+
content_type = ContentType.find_by(handle: attrs[:handle])
|
|
194
|
+
next unless content_type
|
|
195
|
+
|
|
196
|
+
if attrs[:title_field_handle].present?
|
|
197
|
+
field = content_type.field_definitions.find_by(handle: attrs[:title_field_handle])
|
|
198
|
+
content_type.update!(title_field_definition: field) if field
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
if attrs[:web_page_title_field_handle].present?
|
|
202
|
+
field = content_type.field_definitions.find_by(handle: attrs[:web_page_title_field_handle])
|
|
203
|
+
content_type.update!(web_page_title_field_definition: field) if field
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Iron
|
|
2
|
+
module BlockDefinition::Importable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
class_methods do
|
|
6
|
+
def import_from_attributes(attrs)
|
|
7
|
+
block_def = find_or_initialize_by(handle: attrs[:handle])
|
|
8
|
+
block_def.update!(
|
|
9
|
+
name: attrs[:name],
|
|
10
|
+
description: attrs[:description]
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
import_field_definitions(block_def, attrs[:field_definitions] || [])
|
|
14
|
+
|
|
15
|
+
block_def
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def import_field_definitions(block_def, field_attrs_list)
|
|
21
|
+
field_attrs_list.each do |field_attrs|
|
|
22
|
+
FieldDefinition.import_from_attributes(field_attrs, schemable: block_def)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Iron
|
|
2
|
+
module ContentType::Exportable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
def export_attributes
|
|
6
|
+
{
|
|
7
|
+
handle:,
|
|
8
|
+
name:,
|
|
9
|
+
type: type.demodulize.underscore,
|
|
10
|
+
description:,
|
|
11
|
+
icon:,
|
|
12
|
+
web_publishing_enabled:,
|
|
13
|
+
base_path:,
|
|
14
|
+
title_field_handle: title_field_definition&.handle,
|
|
15
|
+
web_page_title_field_handle: web_page_title_field_definition&.handle,
|
|
16
|
+
field_definitions: field_definitions.ranked.map(&:export_attributes)
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Iron
|
|
2
|
+
module ContentType::Importable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
class_methods do
|
|
6
|
+
def import_from_attributes(attrs)
|
|
7
|
+
content_type = find_or_initialize_by(handle: attrs[:handle])
|
|
8
|
+
content_type = content_type.becomes!(classify_type(attrs[:type].camelize).constantize)
|
|
9
|
+
|
|
10
|
+
content_type.update!(
|
|
11
|
+
name: attrs[:name],
|
|
12
|
+
description: attrs[:description],
|
|
13
|
+
icon: attrs[:icon],
|
|
14
|
+
web_publishing_enabled: attrs[:web_publishing_enabled],
|
|
15
|
+
base_path: attrs[:base_path]
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
import_field_definitions(content_type, attrs[:field_definitions] || [])
|
|
19
|
+
|
|
20
|
+
content_type
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def import_field_definitions(content_type, field_attrs_list)
|
|
26
|
+
field_attrs_list.each do |field_attrs|
|
|
27
|
+
FieldDefinition.import_from_attributes(field_attrs, schemable: content_type)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/app/models/iron/current.rb
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
module Iron
|
|
2
2
|
class Current < ActiveSupport::CurrentAttributes
|
|
3
|
-
attribute :session
|
|
4
|
-
delegate :user, to: :session, allow_nil: true
|
|
5
|
-
|
|
3
|
+
attribute :session, :user
|
|
6
4
|
attribute :locale
|
|
7
5
|
attribute :account
|
|
8
6
|
|
|
7
|
+
def session=(value)
|
|
8
|
+
super(value)
|
|
9
|
+
self.user = value&.user
|
|
10
|
+
end
|
|
11
|
+
|
|
9
12
|
def account
|
|
10
13
|
super || (self.account = Account.first)
|
|
11
14
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Iron
|
|
2
|
+
module Entry::Exportable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
include ActionView::Helpers::TagHelper
|
|
5
|
+
|
|
6
|
+
def export_json
|
|
7
|
+
JSON.pretty_generate(export_attributes)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def export_attributes
|
|
11
|
+
{
|
|
12
|
+
gid: to_gid.to_s,
|
|
13
|
+
id: id,
|
|
14
|
+
content_type_handle: content_type.handle,
|
|
15
|
+
created_at: created_at.iso8601,
|
|
16
|
+
updated_at: updated_at.iso8601,
|
|
17
|
+
route: route,
|
|
18
|
+
creator_gid: creator&.to_gid&.to_s,
|
|
19
|
+
fields: export_fields_by_locale
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def export_attachments
|
|
24
|
+
collect_attachments.map do |blob|
|
|
25
|
+
{ path: export_attachment_path(blob), blob: blob }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def export_attachment_path(blob)
|
|
30
|
+
"#{id}/#{blob.key}_#{blob.filename}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def export_fields_by_locale
|
|
36
|
+
grouped = fields.includes(:locale, :definition).group_by(&:locale)
|
|
37
|
+
|
|
38
|
+
grouped.transform_keys(&:code).transform_values do |locale_fields|
|
|
39
|
+
locale_fields
|
|
40
|
+
.reject { |f| f.parent.present? }
|
|
41
|
+
.to_h { |f| [ f.definition.handle, f.export_value ] }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def collect_attachments
|
|
46
|
+
fields.flat_map(&:export_attachments).compact
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
module Iron
|
|
2
|
+
module Entry::Importable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
class_methods do
|
|
6
|
+
def import_from_attributes(attrs, files_dir)
|
|
7
|
+
content_type = ContentType.find_by(handle: attrs[:content_type_handle])
|
|
8
|
+
return nil unless content_type
|
|
9
|
+
|
|
10
|
+
entry = find_existing_entry(attrs, content_type)
|
|
11
|
+
entry ||= new(content_type: content_type, creator: find_creator(attrs[:creator_gid]))
|
|
12
|
+
entry.route = attrs[:route]
|
|
13
|
+
import_fields(entry, attrs[:fields] || {}, files_dir)
|
|
14
|
+
entry.save!
|
|
15
|
+
entry
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def find_existing_entry(attrs, content_type)
|
|
21
|
+
find_on_same_instance(attrs[:gid]) || locate_by_route(attrs[:route], content_type)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def find_on_same_instance(gid)
|
|
25
|
+
return unless gid.present?
|
|
26
|
+
|
|
27
|
+
::GlobalID::Locator.locate(gid)
|
|
28
|
+
rescue ActiveRecord::RecordNotFound
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def locate_by_route(route, content_type)
|
|
33
|
+
return if route.nil?
|
|
34
|
+
Entry.find_by(content_type: content_type, route: route)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def find_creator(gid)
|
|
38
|
+
return unless gid.present?
|
|
39
|
+
::GlobalID::Locator.locate(gid)
|
|
40
|
+
rescue ActiveRecord::RecordNotFound
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def import_fields(entry, fields_by_locale, files_dir)
|
|
45
|
+
fields_by_locale.each do |locale_code, field_data|
|
|
46
|
+
locale = Locale.find_by(code: locale_code.to_s)
|
|
47
|
+
next unless locale
|
|
48
|
+
|
|
49
|
+
field_data.each do |handle, value_data|
|
|
50
|
+
next if value_data.nil?
|
|
51
|
+
|
|
52
|
+
definition = entry.content_type.field_definitions.find_by(handle: handle.to_s)
|
|
53
|
+
next unless definition
|
|
54
|
+
|
|
55
|
+
import_field(entry, definition, locale, value_data, files_dir)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def import_field(entry, definition, locale, value_data, files_dir)
|
|
61
|
+
field = entry.find_or_build_field(definition, locale)
|
|
62
|
+
import_field_value(field, value_data, entry, files_dir)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def import_rich_text_field(field, value_data, files_dir)
|
|
66
|
+
html = value_data[:value]
|
|
67
|
+
return unless html.present?
|
|
68
|
+
|
|
69
|
+
# Replace local file paths with attached blobs
|
|
70
|
+
html = replace_file_paths_with_blobs(html, files_dir)
|
|
71
|
+
field.rich_text = html
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def import_file_field(field, value_data, files_dir)
|
|
75
|
+
path = value_data[:value]
|
|
76
|
+
return unless path.present? && files_dir
|
|
77
|
+
|
|
78
|
+
file_path = File.join(files_dir, path)
|
|
79
|
+
return unless File.exist?(file_path)
|
|
80
|
+
|
|
81
|
+
field.file.attach(
|
|
82
|
+
io: File.open(file_path),
|
|
83
|
+
filename: File.basename(path).sub(/^[^_]+_/, "")
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def import_block_field(entry, field, value_data, files_dir)
|
|
88
|
+
field.fields.destroy_all if field.persisted?
|
|
89
|
+
import_block_fields(entry, field, field.block_definition, value_data[:fields] || {}, files_dir)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def import_block_list_field(entry, field, value_data, files_dir)
|
|
93
|
+
return unless value_data[:value].is_a?(Array)
|
|
94
|
+
|
|
95
|
+
field.blocks.destroy_all if field.persisted?
|
|
96
|
+
|
|
97
|
+
value_data[:value].each_with_index do |block_data, index|
|
|
98
|
+
import_block(entry, field, block_data, index, files_dir)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def import_block(entry, parent_field, block_data, rank, files_dir)
|
|
103
|
+
block_definition = BlockDefinition.find_by(handle: block_data[:block_handle])
|
|
104
|
+
return unless block_definition
|
|
105
|
+
|
|
106
|
+
block = parent_field.blocks.build(
|
|
107
|
+
entry: entry,
|
|
108
|
+
definition: block_definition,
|
|
109
|
+
locale: parent_field.locale,
|
|
110
|
+
rank: rank
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
import_block_fields(entry, block, block_definition, block_data[:fields] || {}, files_dir)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def import_block_fields(entry, parent, block_definition, fields_data, files_dir)
|
|
117
|
+
fields_data.each do |handle, value_data|
|
|
118
|
+
next if value_data.nil?
|
|
119
|
+
|
|
120
|
+
definition = block_definition.field_definitions.find_by(handle: handle.to_s)
|
|
121
|
+
next unless definition
|
|
122
|
+
|
|
123
|
+
nested_field = parent.fields.build(
|
|
124
|
+
type: definition.field_type,
|
|
125
|
+
entry: entry,
|
|
126
|
+
definition: definition,
|
|
127
|
+
locale: parent.locale
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
import_field_value(nested_field, value_data, entry, files_dir)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def import_field_value(field, value_data, entry, files_dir)
|
|
135
|
+
case value_data[:type]
|
|
136
|
+
when "text_field"
|
|
137
|
+
field.value_string = value_data[:value]
|
|
138
|
+
when "text_area"
|
|
139
|
+
field.value_text = value_data[:value]
|
|
140
|
+
when "number"
|
|
141
|
+
field.value_float = value_data[:value]
|
|
142
|
+
when "boolean"
|
|
143
|
+
field.value_boolean = value_data[:value]
|
|
144
|
+
when "date"
|
|
145
|
+
field.value_datetime = value_data[:value].present? ? Time.iso8601(value_data[:value]) : nil
|
|
146
|
+
when "rich_text_area"
|
|
147
|
+
import_rich_text_field(field, value_data, files_dir)
|
|
148
|
+
when "file"
|
|
149
|
+
import_file_field(field, value_data, files_dir)
|
|
150
|
+
when "reference", "reference_list"
|
|
151
|
+
# Skip - resolved in second pass
|
|
152
|
+
when "block"
|
|
153
|
+
import_block_field(entry, field, value_data, files_dir)
|
|
154
|
+
when "block_list"
|
|
155
|
+
import_block_list_field(entry, field, value_data, files_dir)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def replace_file_paths_with_blobs(html, files_dir)
|
|
160
|
+
return html unless files_dir
|
|
161
|
+
|
|
162
|
+
# Simple regex replacement for src and href attributes pointing to local files
|
|
163
|
+
html.gsub(/(?:src|href)="(\d+\/[^"]+)"/) do |match|
|
|
164
|
+
path = $1
|
|
165
|
+
file_path = File.join(files_dir, path)
|
|
166
|
+
|
|
167
|
+
if File.exist?(file_path)
|
|
168
|
+
filename = File.basename(path).sub(/^[^_]+_/, "")
|
|
169
|
+
blob = ActiveStorage::Blob.create_and_upload!(
|
|
170
|
+
io: File.open(file_path),
|
|
171
|
+
filename: filename
|
|
172
|
+
)
|
|
173
|
+
match.sub(path, Rails.application.routes.url_helpers.rails_blob_path(blob, only_path: true))
|
|
174
|
+
else
|
|
175
|
+
match
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
data/app/models/iron/entry.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Iron
|
|
2
2
|
class Entry < ApplicationRecord
|
|
3
|
-
include Titlable, Schemable, WebPublishable, Presentable, DeepValidation, InstanceScoped,
|
|
3
|
+
include Titlable, Schemable, WebPublishable, Presentable, DeepValidation, InstanceScoped, Exportable, Importable
|
|
4
4
|
|
|
5
5
|
belongs_to :creator, class_name: "Iron::User", default: -> { Current.user }
|
|
6
6
|
has_many :fields, inverse_of: :entry, dependent: :destroy
|
data/app/models/iron/field.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Iron
|
|
2
2
|
class Field < ApplicationRecord
|
|
3
|
-
include BelongsToEntry
|
|
3
|
+
include BelongsToEntry
|
|
4
4
|
|
|
5
5
|
TYPES = %w[text_field text_area rich_text_area number file boolean date block block_list reference_list reference].freeze
|
|
6
6
|
|
|
@@ -16,5 +16,13 @@ module Iron
|
|
|
16
16
|
def value
|
|
17
17
|
raise "Field type '#{type}' value method not supported"
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
def export_value
|
|
21
|
+
raise "Field type '#{type}' export_value method not supported"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def export_attachments
|
|
25
|
+
[]
|
|
26
|
+
end
|
|
19
27
|
end
|
|
20
28
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Iron
|
|
2
|
+
module FieldDefinition::Exportable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
def export_attributes
|
|
6
|
+
{
|
|
7
|
+
handle:,
|
|
8
|
+
name:,
|
|
9
|
+
type: type_handle,
|
|
10
|
+
rank:,
|
|
11
|
+
metadata:
|
|
12
|
+
}.tap do |attrs|
|
|
13
|
+
if respond_to?(:supported_block_definitions)
|
|
14
|
+
attrs[:supported_block_definitions] = supported_block_definitions.pluck(:handle)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
if respond_to?(:supported_content_types)
|
|
18
|
+
attrs[:supported_content_types] = supported_content_types.pluck(:handle)
|
|
19
|
+
end
|
|
20
|
+
end.compact_blank
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Iron
|
|
2
|
+
module FieldDefinition::Importable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
class_methods do
|
|
6
|
+
def import_from_attributes(attrs, schemable:)
|
|
7
|
+
raise ArgumentError, "Field type is required" unless attrs[:type].present?
|
|
8
|
+
|
|
9
|
+
field_def = schemable.field_definitions.find_or_initialize_by(handle: attrs[:handle])
|
|
10
|
+
field_def = field_def.becomes!(classify_type(attrs[:type]).constantize)
|
|
11
|
+
|
|
12
|
+
field_def.assign_attributes(
|
|
13
|
+
name: attrs[:name],
|
|
14
|
+
rank: attrs[:rank],
|
|
15
|
+
metadata: attrs[:metadata] || {}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
assign_supported_definitions(field_def, attrs)
|
|
19
|
+
|
|
20
|
+
field_def.save!
|
|
21
|
+
field_def
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def assign_supported_definitions(field_def, attrs)
|
|
27
|
+
if field_def.respond_to?(:supported_block_definition_ids=) && attrs[:supported_block_definitions].present?
|
|
28
|
+
block_defs = BlockDefinition.where(handle: attrs[:supported_block_definitions])
|
|
29
|
+
field_def.supported_block_definition_ids = block_defs.pluck(:id)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if field_def.respond_to?(:supported_content_type_ids=) && attrs[:supported_content_types].present?
|
|
33
|
+
content_types = ContentType.where(handle: attrs[:supported_content_types])
|
|
34
|
+
field_def.supported_content_type_ids = content_types.pluck(:id)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|