iron-cms 0.1.3 → 0.2.1
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/README.md +26 -0
- data/app/assets/builds/iron.css +5538 -1
- data/app/controllers/iron/contents_controller.rb +33 -0
- data/app/models/concerns/iron/instance_scoped.rb +25 -0
- data/app/models/iron/account.rb +2 -0
- data/app/models/iron/archive.rb +69 -0
- data/app/models/iron/content_export.rb +73 -0
- data/app/models/iron/content_import/entry_builder.rb +80 -0
- data/app/models/iron/content_import/entry_snapshot.rb +23 -0
- data/app/models/iron/content_import/field_reconstructor.rb +276 -0
- data/app/models/iron/content_import/field_snapshot.rb +33 -0
- data/app/models/iron/content_import/registry.rb +32 -0
- data/app/models/iron/content_import/session.rb +89 -0
- data/app/models/iron/content_import.rb +15 -0
- data/app/models/iron/current.rb +2 -1
- data/app/models/iron/entry/portable.rb +35 -0
- data/app/models/iron/entry.rb +3 -1
- data/app/models/iron/field/portable.rb +33 -0
- data/app/models/iron/field.rb +1 -1
- data/app/models/iron/fields/block.rb +22 -8
- data/app/models/iron/fields/block_list.rb +10 -0
- data/app/models/iron/fields/boolean.rb +10 -0
- data/app/models/iron/fields/date.rb +10 -0
- data/app/models/iron/fields/file.rb +8 -0
- data/app/models/iron/fields/number.rb +10 -0
- data/app/models/iron/fields/reference.rb +10 -0
- data/app/models/iron/fields/reference_list.rb +14 -0
- data/app/models/iron/fields/rich_text_area.rb +26 -0
- data/app/models/iron/fields/text_area.rb +10 -0
- data/app/models/iron/fields/text_field.rb +8 -0
- data/app/models/iron/locale.rb +1 -1
- data/app/views/iron/contents/new.html.erb +34 -0
- data/app/views/iron/settings/show.html.erb +19 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20250908203158_add_instance_token_to_iron_accounts.rb +6 -0
- data/lib/iron/engine.rb +9 -0
- data/lib/iron/global_id/instance_scoped_locator.rb +31 -0
- data/lib/iron/version.rb +1 -1
- data/lib/iron.rb +1 -0
- metadata +17 -1
@@ -0,0 +1,33 @@
|
|
1
|
+
module Iron
|
2
|
+
class ContentsController < ApplicationController
|
3
|
+
before_action :ensure_can_administer
|
4
|
+
|
5
|
+
def export
|
6
|
+
send_data ContentExport.export.to_tar,
|
7
|
+
filename: "iron-content-#{Date.current}.tar",
|
8
|
+
type: "application/x-tar",
|
9
|
+
disposition: "attachment"
|
10
|
+
end
|
11
|
+
|
12
|
+
def new
|
13
|
+
end
|
14
|
+
|
15
|
+
def import
|
16
|
+
file = params[:content_file]
|
17
|
+
|
18
|
+
unless file.present?
|
19
|
+
redirect_to new_content_path, alert: "Please select a file to import"
|
20
|
+
return
|
21
|
+
end
|
22
|
+
|
23
|
+
archive = Archive.from_file(file)
|
24
|
+
result = ContentImport.import(archive)
|
25
|
+
|
26
|
+
if result.success?
|
27
|
+
redirect_to settings_path, notice: "Content imported successfully"
|
28
|
+
else
|
29
|
+
redirect_to new_content_path, alert: result.errors.join(". ")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Adds instance token to GlobalIDs to disambiguate between create and update operations
|
2
|
+
# during import. When importing data:
|
3
|
+
# - Same instance token + existing ID = UPDATE the existing record
|
4
|
+
# - Different instance token + any ID = CREATE a new record
|
5
|
+
# This allows safe data exchange between Iron instances without ID conflicts.
|
6
|
+
module Iron
|
7
|
+
module InstanceScoped
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
def to_global_id(options = {})
|
11
|
+
super(options.merge(instance: instance))
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_signed_global_id(options = {})
|
15
|
+
super(options.merge(instance: instance))
|
16
|
+
end
|
17
|
+
|
18
|
+
alias to_gid to_global_id
|
19
|
+
alias to_sgid to_signed_global_id
|
20
|
+
|
21
|
+
def instance
|
22
|
+
@instance ||= Current.account.instance_token
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/app/models/iron/account.rb
CHANGED
@@ -0,0 +1,69 @@
|
|
1
|
+
require "rubygems/package"
|
2
|
+
require "stringio"
|
3
|
+
|
4
|
+
module Iron
|
5
|
+
class Archive
|
6
|
+
attr_reader :files
|
7
|
+
|
8
|
+
def initialize(tar_content = nil)
|
9
|
+
@files = {}
|
10
|
+
extract_from_tar(tar_content) if tar_content
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.from_file(file)
|
14
|
+
new(file.read)
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_file(path, content)
|
18
|
+
@files[path] = content
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def [](path)
|
23
|
+
@files[path]
|
24
|
+
end
|
25
|
+
|
26
|
+
def file_exists?(path)
|
27
|
+
@files.key?(path)
|
28
|
+
end
|
29
|
+
|
30
|
+
def file_paths
|
31
|
+
@files.keys
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_tar
|
35
|
+
tarfile = StringIO.new
|
36
|
+
|
37
|
+
Gem::Package::TarWriter.new(tarfile) do |tar|
|
38
|
+
@files.each do |path, content|
|
39
|
+
content_string = content.is_a?(StringIO) ? content.string : content.to_s
|
40
|
+
tar.add_file_simple(path, 0644, content_string.bytesize) do |io|
|
41
|
+
io.write(content_string)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
tarfile.string
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_file(path)
|
50
|
+
File.open(path, "wb") { |f| f.write(to_tar) }
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def extract_from_tar(tar_content)
|
56
|
+
tar_io = StringIO.new(tar_content)
|
57
|
+
|
58
|
+
Gem::Package::TarReader.new(tar_io) do |tar|
|
59
|
+
tar.each do |entry|
|
60
|
+
if entry.file?
|
61
|
+
@files[entry.full_name] = entry.read.force_encoding("UTF-8")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
rescue => e
|
66
|
+
raise "Failed to extract tar archive: #{e.message}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "csv"
|
2
|
+
|
3
|
+
module Iron
|
4
|
+
class ContentExport
|
5
|
+
def self.export
|
6
|
+
new.export
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@archive = Archive.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def export
|
14
|
+
export_entries_by_content_type
|
15
|
+
export_files
|
16
|
+
|
17
|
+
@archive
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def export_entries_by_content_type
|
23
|
+
ContentType.find_each do |content_type|
|
24
|
+
next if content_type.entries.empty?
|
25
|
+
|
26
|
+
@archive.add_file("content/#{content_type.handle}.csv", csv_for(content_type))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def csv_for(content_type)
|
31
|
+
CSV.generate do |csv|
|
32
|
+
csv << %w[entry_key locale path type value]
|
33
|
+
|
34
|
+
content_type.entries.find_each do |entry|
|
35
|
+
entry.to_csv_rows.each do |row|
|
36
|
+
csv << row
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def export_files
|
43
|
+
collect_blob_ids.each do |blob_id|
|
44
|
+
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
45
|
+
next unless blob
|
46
|
+
|
47
|
+
begin
|
48
|
+
blob.open do |file|
|
49
|
+
@archive.add_file("uploads/#{blob_id}", file.read)
|
50
|
+
end
|
51
|
+
rescue => e
|
52
|
+
Rails.logger.warn "Failed to export blob #{blob_id}: #{e.message}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def collect_blob_ids
|
58
|
+
ids = Set.new
|
59
|
+
|
60
|
+
Fields::File.joins(file_attachment: :blob).pluck("active_storage_blobs.id").each do |id|
|
61
|
+
ids << id
|
62
|
+
end
|
63
|
+
|
64
|
+
ActionText::RichText.includes(:embeds_attachments).find_each do |rich_text|
|
65
|
+
rich_text.embeds_attachments.each do |attachment|
|
66
|
+
ids << attachment.blob.id if attachment.blob
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
ids
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require "csv"
|
2
|
+
|
3
|
+
module Iron
|
4
|
+
class ContentImport::EntryBuilder
|
5
|
+
def initialize(content_type, registry)
|
6
|
+
@content_type = content_type
|
7
|
+
@registry = registry
|
8
|
+
end
|
9
|
+
|
10
|
+
def ensure_entries_exist(csv_data)
|
11
|
+
entry_snapshots = parse_entry_snapshots(csv_data)
|
12
|
+
|
13
|
+
entry_snapshots.each do |_entry_gid, snapshot|
|
14
|
+
ensure_entry(snapshot)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def parse_entry_snapshots(csv_data)
|
21
|
+
snapshots = {}
|
22
|
+
return snapshots unless csv_data.present?
|
23
|
+
|
24
|
+
CSV.parse(csv_data, headers: true) do |row|
|
25
|
+
entry_key = row["entry_key"]
|
26
|
+
next if entry_key.blank?
|
27
|
+
|
28
|
+
snapshot = (snapshots[entry_key] ||= ContentImport::EntrySnapshot.new(entry_key))
|
29
|
+
add_row_to_snapshot(snapshot, row)
|
30
|
+
end
|
31
|
+
|
32
|
+
snapshots
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_row_to_snapshot(snapshot, row)
|
36
|
+
path = row["path"]
|
37
|
+
return if path.blank?
|
38
|
+
|
39
|
+
if path == "/route"
|
40
|
+
snapshot.add_route(row["value"])
|
41
|
+
elsif path.start_with?("/fields/")
|
42
|
+
locale_code = row["locale"]
|
43
|
+
return unless locale_code.present?
|
44
|
+
|
45
|
+
field_snapshot = ContentImport::FieldSnapshot.new(
|
46
|
+
locale_code: locale_code,
|
47
|
+
path: path,
|
48
|
+
value: row["value"]
|
49
|
+
)
|
50
|
+
snapshot.add_field_snapshot(field_snapshot)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def ensure_entry(snapshot)
|
55
|
+
if snapshot.route.present?
|
56
|
+
existing_by_route = @content_type.entries.find_by(route: snapshot.route)
|
57
|
+
if existing_by_route
|
58
|
+
@registry.register_entry(snapshot.gid, existing_by_route)
|
59
|
+
return existing_by_route
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
existing_entry = @registry.find_entry(snapshot.gid)
|
64
|
+
# Only use the existing entry if it belongs to the correct content type
|
65
|
+
if existing_entry && existing_entry.content_type_id == @content_type.id
|
66
|
+
return existing_entry
|
67
|
+
end
|
68
|
+
|
69
|
+
new_entry = create_entry
|
70
|
+
@registry.register_entry(snapshot.gid, new_entry)
|
71
|
+
new_entry
|
72
|
+
end
|
73
|
+
|
74
|
+
def create_entry
|
75
|
+
entry = @content_type.entries.build
|
76
|
+
entry.save(validate: false)
|
77
|
+
entry
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Iron
|
2
|
+
class ContentImport::EntrySnapshot
|
3
|
+
attr_reader :gid, :route, :field_snapshots
|
4
|
+
|
5
|
+
def initialize(gid)
|
6
|
+
@gid = gid
|
7
|
+
@route = nil
|
8
|
+
@field_snapshots = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_route(route)
|
12
|
+
@route = route
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_field_snapshot(snapshot)
|
16
|
+
@field_snapshots << snapshot
|
17
|
+
end
|
18
|
+
|
19
|
+
def group_field_snapshots_by_locale
|
20
|
+
@field_snapshots.group_by(&:locale_code)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,276 @@
|
|
1
|
+
require "csv"
|
2
|
+
|
3
|
+
module Iron
|
4
|
+
class ContentImport::FieldReconstructor
|
5
|
+
def initialize(content_type, registry, archive)
|
6
|
+
@content_type = content_type
|
7
|
+
@registry = registry
|
8
|
+
@archive = archive
|
9
|
+
end
|
10
|
+
|
11
|
+
def reconstruct_all(csv_data)
|
12
|
+
entry_snapshots = build_entry_snapshots(csv_data)
|
13
|
+
|
14
|
+
entry_snapshots.each do |entry_gid, snapshot|
|
15
|
+
reconstruct_entry(entry_gid, snapshot)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def build_entry_snapshots(csv_data)
|
22
|
+
snapshots = {}
|
23
|
+
return snapshots unless csv_data.present?
|
24
|
+
|
25
|
+
CSV.parse(csv_data, headers: true) do |row|
|
26
|
+
entry_key = row["entry_key"]
|
27
|
+
next if entry_key.blank?
|
28
|
+
|
29
|
+
snapshot = (snapshots[entry_key] ||= ContentImport::EntrySnapshot.new(entry_key))
|
30
|
+
populate_snapshot(snapshot, row)
|
31
|
+
end
|
32
|
+
|
33
|
+
snapshots
|
34
|
+
end
|
35
|
+
|
36
|
+
def populate_snapshot(snapshot, row)
|
37
|
+
path = row["path"]
|
38
|
+
return if path.blank?
|
39
|
+
|
40
|
+
if path == "/route"
|
41
|
+
snapshot.add_route(row["value"])
|
42
|
+
elsif path.start_with?("/fields/")
|
43
|
+
add_field_snapshot(snapshot, row)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_field_snapshot(snapshot, row)
|
48
|
+
locale_code = row["locale"]
|
49
|
+
return unless locale_code.present?
|
50
|
+
|
51
|
+
field_snapshot = ContentImport::FieldSnapshot.new(
|
52
|
+
locale_code: locale_code,
|
53
|
+
path: row["path"],
|
54
|
+
value: row["value"]
|
55
|
+
)
|
56
|
+
snapshot.add_field_snapshot(field_snapshot)
|
57
|
+
end
|
58
|
+
|
59
|
+
def reconstruct_entry(entry_gid, snapshot)
|
60
|
+
entry = @registry.find_entry(entry_gid)
|
61
|
+
return unless entry
|
62
|
+
|
63
|
+
update_entry_route(entry, snapshot.route) unless snapshot.route.nil?
|
64
|
+
reconstruct_entry_fields(entry, snapshot)
|
65
|
+
end
|
66
|
+
|
67
|
+
def update_entry_route(entry, route)
|
68
|
+
entry.update_columns(route: route)
|
69
|
+
end
|
70
|
+
|
71
|
+
def reconstruct_entry_fields(entry, snapshot)
|
72
|
+
snapshot.group_field_snapshots_by_locale.each do |locale_code, field_snapshots|
|
73
|
+
locale = Iron::Locale.find_by(code: locale_code)
|
74
|
+
next unless locale
|
75
|
+
|
76
|
+
reconstruct_fields_for_locale(entry, field_snapshots, locale)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def reconstruct_fields_for_locale(entry, field_snapshots, locale)
|
81
|
+
field_groups = field_snapshots
|
82
|
+
.select(&:field_path?)
|
83
|
+
.group_by(&:field_handle)
|
84
|
+
|
85
|
+
field_groups.each do |handle, snapshots|
|
86
|
+
definition = @content_type.field_definitions.find_by(handle: handle)
|
87
|
+
next unless definition
|
88
|
+
|
89
|
+
field = entry.find_or_build_field(definition, locale)
|
90
|
+
reconstruct_field(field, snapshots)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def reconstruct_field(field, snapshots)
|
95
|
+
case field
|
96
|
+
when Iron::Fields::TextField
|
97
|
+
field.value_string = snapshots.first.value
|
98
|
+
when Iron::Fields::TextArea
|
99
|
+
field.value_text = snapshots.first.value
|
100
|
+
when Iron::Fields::Number
|
101
|
+
field.value_decimal = snapshots.first.value.to_f if snapshots.first.value.present?
|
102
|
+
when Iron::Fields::Boolean
|
103
|
+
field.value_boolean = ActiveModel::Type::Boolean.new.cast(snapshots.first.value)
|
104
|
+
when Iron::Fields::Date
|
105
|
+
field.value_datetime = Time.zone.parse(snapshots.first.value) if snapshots.first.value.present?
|
106
|
+
when Iron::Fields::File
|
107
|
+
attach_file(field, snapshots.first.value)
|
108
|
+
when Iron::Fields::Reference
|
109
|
+
field.referenced_entry = @registry.find_entry(snapshots.first.value)
|
110
|
+
when Iron::Fields::RichTextArea
|
111
|
+
reconstruct_rich_text(field, snapshots)
|
112
|
+
when Iron::Fields::ReferenceList
|
113
|
+
reconstruct_reference_list(field, snapshots)
|
114
|
+
when Iron::Fields::Block
|
115
|
+
reconstruct_block(field, snapshots)
|
116
|
+
when Iron::Fields::BlockList
|
117
|
+
reconstruct_block_list(field, snapshots)
|
118
|
+
end
|
119
|
+
|
120
|
+
field.save!(validate: false)
|
121
|
+
rescue ArgumentError
|
122
|
+
# Handle date parsing errors
|
123
|
+
field.save!(validate: false)
|
124
|
+
end
|
125
|
+
|
126
|
+
def attach_file(field, blob_id)
|
127
|
+
return unless blob_id.present?
|
128
|
+
|
129
|
+
data = @archive["uploads/#{blob_id}"]
|
130
|
+
return unless data
|
131
|
+
|
132
|
+
field.file.attach(
|
133
|
+
io: StringIO.new(data),
|
134
|
+
filename: "imported-#{blob_id}",
|
135
|
+
content_type: "application/octet-stream"
|
136
|
+
)
|
137
|
+
end
|
138
|
+
|
139
|
+
def reconstruct_rich_text(field, snapshots)
|
140
|
+
html_snapshot = snapshots.find { |s| s.path.end_with?("/html") }
|
141
|
+
|
142
|
+
if html_snapshot&.value.present?
|
143
|
+
field.update!(rich_text: html_snapshot.value)
|
144
|
+
else
|
145
|
+
field.rich_text&.destroy
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def reconstruct_reference_list(field, snapshots)
|
150
|
+
# Group snapshots by rank (the first segment after the field handle)
|
151
|
+
field.save!(validate: false) if field.new_record?
|
152
|
+
|
153
|
+
items = snapshots.filter_map do |snapshot|
|
154
|
+
segments = snapshot.field_segments
|
155
|
+
next if segments.length < 2
|
156
|
+
|
157
|
+
rank = segments.last
|
158
|
+
entry = @registry.find_entry(snapshot.value)
|
159
|
+
next unless entry
|
160
|
+
|
161
|
+
{ rank:, entry: }
|
162
|
+
end
|
163
|
+
|
164
|
+
existing = field.references.index_by(&:rank)
|
165
|
+
kept_ids = []
|
166
|
+
|
167
|
+
items.each do |item|
|
168
|
+
reference = existing[item[:rank]] || field.references.build(rank: item[:rank])
|
169
|
+
reference.entry = item[:entry]
|
170
|
+
reference.save!
|
171
|
+
kept_ids << reference.id
|
172
|
+
end
|
173
|
+
|
174
|
+
field.references.where.not(id: kept_ids).destroy_all
|
175
|
+
end
|
176
|
+
|
177
|
+
def reconstruct_block(field, snapshots)
|
178
|
+
# Fields::Block IS the block - it doesn't have a separate block relation
|
179
|
+
# The field already has the block definition through field.definition
|
180
|
+
|
181
|
+
# Group snapshots by field handle
|
182
|
+
field_snapshots = snapshots.reject { |s| s.path.include?("/_block") }
|
183
|
+
reconstruct_block_fields(field, field_snapshots, field.locale)
|
184
|
+
end
|
185
|
+
|
186
|
+
def reconstruct_block_list(field, snapshots)
|
187
|
+
# Group snapshots by block rank
|
188
|
+
blocks_by_rank = {}
|
189
|
+
|
190
|
+
snapshots.each do |snapshot|
|
191
|
+
segments = snapshot.field_segments
|
192
|
+
next if segments.size < 2
|
193
|
+
|
194
|
+
rank = segments[1]
|
195
|
+
blocks_by_rank[rank] ||= []
|
196
|
+
blocks_by_rank[rank] << snapshot
|
197
|
+
end
|
198
|
+
|
199
|
+
# Clear all existing blocks first to avoid rank conflicts
|
200
|
+
field.blocks.destroy_all
|
201
|
+
|
202
|
+
blocks_by_rank.each do |rank, block_snapshots|
|
203
|
+
# Find block handle
|
204
|
+
block_snapshot = block_snapshots.find { |s| s.path.include?("/_block") }
|
205
|
+
block_handle = block_snapshot&.value
|
206
|
+
next unless block_handle
|
207
|
+
|
208
|
+
block_definition = Iron::BlockDefinition.find_by(handle: block_handle)
|
209
|
+
next unless block_definition
|
210
|
+
|
211
|
+
block = field.blocks.build(
|
212
|
+
rank: rank,
|
213
|
+
entry: field.entry,
|
214
|
+
locale: field.locale,
|
215
|
+
definition: block_definition
|
216
|
+
)
|
217
|
+
|
218
|
+
# Save the block first to establish it in the database
|
219
|
+
block.save!(validate: false)
|
220
|
+
|
221
|
+
# Then reconstruct its fields
|
222
|
+
field_snapshots = block_snapshots.reject { |s| s.path.include?("/_block") }
|
223
|
+
reconstruct_block_fields(block, field_snapshots, field.locale)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def reconstruct_block_fields(block, snapshots, locale)
|
228
|
+
# Group snapshots by field handle
|
229
|
+
field_groups = {}
|
230
|
+
|
231
|
+
snapshots.each do |snapshot|
|
232
|
+
field_handle = block_child_handle(block, snapshot)
|
233
|
+
next unless field_handle
|
234
|
+
|
235
|
+
field_groups[field_handle] ||= []
|
236
|
+
field_groups[field_handle] << snapshot
|
237
|
+
end
|
238
|
+
|
239
|
+
# Get the actual block definition
|
240
|
+
block_def = if block.respond_to?(:block_definition)
|
241
|
+
block.block_definition
|
242
|
+
else
|
243
|
+
block.definition
|
244
|
+
end
|
245
|
+
|
246
|
+
# Clear existing fields to avoid rank conflicts
|
247
|
+
block.fields.where(locale: locale).destroy_all
|
248
|
+
|
249
|
+
field_groups.each do |handle, field_snapshots|
|
250
|
+
definition = block_def.field_definitions.find_by(handle: handle)
|
251
|
+
next unless definition
|
252
|
+
|
253
|
+
field = block.fields.build(
|
254
|
+
type: definition.field_type,
|
255
|
+
entry: block.entry,
|
256
|
+
definition: definition,
|
257
|
+
locale: locale
|
258
|
+
)
|
259
|
+
|
260
|
+
reconstruct_field(field, field_snapshots)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def block_child_handle(block, snapshot)
|
265
|
+
relative_segments = snapshot.field_segments
|
266
|
+
block_path = block.send(:field_path)
|
267
|
+
|
268
|
+
if block_path.present?
|
269
|
+
block_segments = block_path.sub(%r{^/fields/}, "").split("/")
|
270
|
+
relative_segments = relative_segments.drop(block_segments.length)
|
271
|
+
end
|
272
|
+
|
273
|
+
relative_segments.first
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Iron
|
2
|
+
class ContentImport::FieldSnapshot
|
3
|
+
attr_reader :locale_code, :path, :value
|
4
|
+
|
5
|
+
def initialize(locale_code:, path:, value:)
|
6
|
+
@locale_code = locale_code
|
7
|
+
@path = path
|
8
|
+
@value = value
|
9
|
+
end
|
10
|
+
|
11
|
+
def field_handle
|
12
|
+
return nil unless field_path?
|
13
|
+
|
14
|
+
segments[0]
|
15
|
+
end
|
16
|
+
|
17
|
+
def field_segments
|
18
|
+
return [] unless field_path?
|
19
|
+
|
20
|
+
segments
|
21
|
+
end
|
22
|
+
|
23
|
+
def field_path?
|
24
|
+
@path&.start_with?("/fields/")
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def segments
|
30
|
+
@segments ||= @path.sub(%r{^/fields/}, "").split("/")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Iron
|
2
|
+
class ContentImport::Registry
|
3
|
+
def initialize
|
4
|
+
@entries_by_original_gid = {}
|
5
|
+
end
|
6
|
+
|
7
|
+
def register_entry(original_gid, entry)
|
8
|
+
@entries_by_original_gid[original_gid] = entry
|
9
|
+
@entries_by_original_gid[entry.to_gid.to_s] = entry
|
10
|
+
end
|
11
|
+
|
12
|
+
def find_entry(gid_string)
|
13
|
+
return nil if gid_string.blank?
|
14
|
+
|
15
|
+
cached_entry = @entries_by_original_gid[gid_string]
|
16
|
+
return cached_entry if cached_entry
|
17
|
+
|
18
|
+
locate_existing_entry(gid_string)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def locate_existing_entry(gid_string)
|
24
|
+
entry = ::GlobalID::Locator.locate(gid_string)
|
25
|
+
return nil unless entry.is_a?(Iron::Entry)
|
26
|
+
|
27
|
+
@entries_by_original_gid[gid_string] = entry
|
28
|
+
rescue ActiveRecord::RecordNotFound
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|