iron-cms 0.1.2 → 0.2.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/README.md +26 -0
- data/app/assets/builds/iron.css +9 -0
- 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/block_definition.rb +8 -0
- data/app/models/iron/content_export.rb +73 -0
- data/app/models/iron/content_import/entry_builder.rb +77 -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/content_type.rb +8 -0
- data/app/models/iron/current.rb +2 -1
- data/app/models/iron/entry/portable.rb +35 -0
- data/app/models/iron/entry.rb +1 -1
- data/app/models/iron/field/portable.rb +33 -0
- data/app/models/iron/field.rb +1 -1
- data/app/models/iron/field_definition/portable.rb +12 -6
- 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/models/iron/schema_importer/import_strategy.rb +59 -0
- data/app/models/iron/schema_importer/merge_strategy.rb +52 -0
- data/app/models/iron/schema_importer/replace_strategy.rb +51 -0
- data/app/models/iron/schema_importer/safe_strategy.rb +55 -0
- data/app/models/iron/schema_importer.rb +29 -199
- 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 +29 -0
- data/lib/iron/version.rb +1 -1
- data/lib/iron-cms.rb +1 -1
- data/lib/iron.rb +1 -0
- metadata +21 -1
@@ -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
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "csv"
|
2
|
+
|
3
|
+
module Iron
|
4
|
+
class ContentImport::Session
|
5
|
+
attr_reader :errors
|
6
|
+
|
7
|
+
def initialize(archive)
|
8
|
+
raise ArgumentError, "Missing import archive" unless archive.present?
|
9
|
+
|
10
|
+
@archive = archive
|
11
|
+
@errors = []
|
12
|
+
@registry = ContentImport::Registry.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
ActiveRecord::Base.transaction do
|
17
|
+
prepare_entries
|
18
|
+
reconstruct_entries
|
19
|
+
|
20
|
+
Result.new(success: true)
|
21
|
+
end
|
22
|
+
rescue StandardError => e
|
23
|
+
warn e.full_message
|
24
|
+
@errors << "Import failed: #{e.message}"
|
25
|
+
Result.new(success: false, errors: @errors)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def prepare_entries
|
31
|
+
content_manifests.each do |content_type, csv_data|
|
32
|
+
entry_builder = ContentImport::EntryBuilder.new(content_type, @registry)
|
33
|
+
entry_builder.ensure_entries_exist(csv_data)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def reconstruct_entries
|
38
|
+
content_manifests.each do |content_type, csv_data|
|
39
|
+
reconstructor = ContentImport::FieldReconstructor.new(content_type, @registry, @archive)
|
40
|
+
reconstructor.reconstruct_all(csv_data)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def content_manifests
|
45
|
+
@content_manifests ||= build_content_manifests
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_content_manifests
|
49
|
+
@archive.file_paths
|
50
|
+
.select { |path| content_manifest?(path) }
|
51
|
+
.sort
|
52
|
+
.filter_map do |path|
|
53
|
+
content_type = find_content_type(path)
|
54
|
+
|
55
|
+
unless content_type
|
56
|
+
@errors << "Unknown content type '#{extract_handle(path)}' in archive"
|
57
|
+
next
|
58
|
+
end
|
59
|
+
|
60
|
+
[ content_type, @archive[path] ]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def content_manifest?(path)
|
65
|
+
path.start_with?("content/") && path.end_with?(".csv")
|
66
|
+
end
|
67
|
+
|
68
|
+
def extract_handle(path)
|
69
|
+
::File.basename(path, ".csv")
|
70
|
+
end
|
71
|
+
|
72
|
+
def find_content_type(path)
|
73
|
+
Iron::ContentType.find_by(handle: extract_handle(path))
|
74
|
+
end
|
75
|
+
|
76
|
+
class Result
|
77
|
+
attr_reader :errors
|
78
|
+
|
79
|
+
def initialize(success:, errors: [])
|
80
|
+
@success = success
|
81
|
+
@errors = errors
|
82
|
+
end
|
83
|
+
|
84
|
+
def success?
|
85
|
+
@success
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -11,6 +11,14 @@ module Iron
|
|
11
11
|
|
12
12
|
has_many :entries, -> { extending FieldQueryable }, dependent: :destroy
|
13
13
|
|
14
|
+
has_and_belongs_to_many :referencing_field_definitions,
|
15
|
+
class_name: "::Iron::FieldDefinition",
|
16
|
+
foreign_key: "content_type_id",
|
17
|
+
association_foreign_key: "field_definition_id",
|
18
|
+
join_table: "iron_content_types_field_definitions"
|
19
|
+
|
20
|
+
before_destroy { referencing_field_definitions.clear }
|
21
|
+
|
14
22
|
class << self
|
15
23
|
def classify_type(type)
|
16
24
|
"Iron::ContentTypes::#{type}"
|
data/app/models/iron/current.rb
CHANGED
@@ -0,0 +1,35 @@
|
|
1
|
+
module Iron
|
2
|
+
class Entry
|
3
|
+
module Portable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def to_csv_rows
|
7
|
+
rows = []
|
8
|
+
|
9
|
+
rows.concat(entry_attribute_rows)
|
10
|
+
|
11
|
+
fields.where(parent_type: nil).includes(:locale, :definition).each do |field|
|
12
|
+
rows.concat(field.to_csv_rows)
|
13
|
+
end
|
14
|
+
|
15
|
+
rows
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def entry_attribute_rows
|
21
|
+
rows = []
|
22
|
+
|
23
|
+
rows << csv_row("/created_at", "date", created_at.iso8601)
|
24
|
+
rows << csv_row("/updated_at", "date", updated_at.iso8601)
|
25
|
+
rows << csv_row("/route", "text", route) unless route.nil?
|
26
|
+
|
27
|
+
rows
|
28
|
+
end
|
29
|
+
|
30
|
+
def csv_row(path, type, value)
|
31
|
+
[ to_gid, nil, path, type, value ]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
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
|
3
|
+
include Titlable, Schemable, WebPublishable, Presentable, DeepValidation, InstanceScoped, Portable
|
4
4
|
|
5
5
|
belongs_to :creator, class_name: "Iron::User", default: -> { Current.user }
|
6
6
|
has_many :fields, inverse_of: :entry, dependent: :destroy
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Iron
|
2
|
+
class Field
|
3
|
+
module Portable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def to_csv_rows
|
7
|
+
[ csv_row(field_path, csv_type, csv_value) ]
|
8
|
+
end
|
9
|
+
|
10
|
+
def field_path
|
11
|
+
if parent.is_a?(Fields::Block)
|
12
|
+
"#{parent.field_path}/#{definition.handle}"
|
13
|
+
elsif parent.is_a?(Fields::BlockList)
|
14
|
+
"#{parent.field_path}/#{rank}"
|
15
|
+
else
|
16
|
+
"/fields/#{definition.handle}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def csv_row(path, type, value = nil)
|
21
|
+
[ entry.to_gid, locale&.code, path, type, value ]
|
22
|
+
end
|
23
|
+
|
24
|
+
def csv_type
|
25
|
+
raise NotImplementedError, "#{self.class} must implement csv_type"
|
26
|
+
end
|
27
|
+
|
28
|
+
def csv_value
|
29
|
+
raise NotImplementedError, "#{self.class} must implement csv_value"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/app/models/iron/field.rb
CHANGED
@@ -5,7 +5,7 @@ module Iron
|
|
5
5
|
|
6
6
|
class_methods do
|
7
7
|
def csv_headers
|
8
|
-
%w[parent_type parent_handle handle name type rank metadata supported_block_definitions]
|
8
|
+
%w[parent_type parent_handle handle name type rank metadata supported_block_definitions supported_content_types]
|
9
9
|
end
|
10
10
|
|
11
11
|
def csv_scope
|
@@ -22,15 +22,21 @@ module Iron
|
|
22
22
|
type.demodulize.underscore,
|
23
23
|
rank,
|
24
24
|
metadata.to_json,
|
25
|
-
export_supported_block_definitions
|
25
|
+
export_supported_block_definitions,
|
26
|
+
export_supported_content_types
|
26
27
|
]
|
27
28
|
end
|
28
29
|
|
29
30
|
private
|
30
31
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
def export_supported_block_definitions
|
33
|
+
return nil unless respond_to?(:supported_block_definitions)
|
34
|
+
supported_block_definitions.pluck(:handle).join("|")
|
35
|
+
end
|
36
|
+
|
37
|
+
def export_supported_content_types
|
38
|
+
return nil unless respond_to?(:supported_content_types)
|
39
|
+
supported_content_types.pluck(:handle).join("|")
|
40
|
+
end
|
35
41
|
end
|
36
42
|
end
|
@@ -6,7 +6,15 @@ module Iron
|
|
6
6
|
has_rank scoped_by: :parent
|
7
7
|
|
8
8
|
def block_list
|
9
|
-
parent.is_a?(
|
9
|
+
parent.is_a?(Fields::BlockList) ? parent : nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def block_definition
|
13
|
+
if definition.is_a?(FieldDefinitions::Block)
|
14
|
+
definition.supported_block_definition
|
15
|
+
else
|
16
|
+
definition
|
17
|
+
end
|
10
18
|
end
|
11
19
|
|
12
20
|
def find_or_build_field(definition)
|
@@ -14,13 +22,7 @@ module Iron
|
|
14
22
|
end
|
15
23
|
|
16
24
|
def value
|
17
|
-
locale =
|
18
|
-
|
19
|
-
block_definition = if definition.is_a?(Iron::FieldDefinitions::Block)
|
20
|
-
definition.supported_block_definition
|
21
|
-
else
|
22
|
-
definition
|
23
|
-
end
|
25
|
+
locale = Current.locale || Locale.default
|
24
26
|
|
25
27
|
result = OpenStruct.new(id: id, _type: block_definition.handle)
|
26
28
|
|
@@ -36,5 +38,17 @@ module Iron
|
|
36
38
|
|
37
39
|
result
|
38
40
|
end
|
41
|
+
|
42
|
+
def to_csv_rows
|
43
|
+
rows = []
|
44
|
+
|
45
|
+
rows << csv_row("#{field_path}/_block", "text", block_definition.handle)
|
46
|
+
|
47
|
+
fields.includes(:locale, :definition).each do |nested_field|
|
48
|
+
rows.concat(nested_field.to_csv_rows)
|
49
|
+
end
|
50
|
+
|
51
|
+
rows
|
52
|
+
end
|
39
53
|
end
|
40
54
|
end
|
@@ -13,5 +13,19 @@ module Iron
|
|
13
13
|
)
|
14
14
|
end
|
15
15
|
end
|
16
|
+
|
17
|
+
def to_csv_rows
|
18
|
+
rows = []
|
19
|
+
|
20
|
+
references.includes(:entry).each do |reference|
|
21
|
+
rows << csv_row(
|
22
|
+
"#{field_path}/#{reference.rank}",
|
23
|
+
"reference",
|
24
|
+
reference.entry.to_gid
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
rows
|
29
|
+
end
|
16
30
|
end
|
17
31
|
end
|
@@ -5,5 +5,31 @@ module Iron
|
|
5
5
|
def value
|
6
6
|
rich_text&.to_s
|
7
7
|
end
|
8
|
+
|
9
|
+
def to_csv_rows
|
10
|
+
rows = []
|
11
|
+
|
12
|
+
if rich_text.present?
|
13
|
+
rows << csv_row("#{field_path}/html", "rich_text", rich_text.body&.to_html)
|
14
|
+
|
15
|
+
rich_text.body.attachables.each do |attachable|
|
16
|
+
next unless attachable.respond_to?(:blob)
|
17
|
+
|
18
|
+
rows << csv_row("#{field_path}/attachments", "reference", attachable.blob.id)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
rows
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def csv_type
|
28
|
+
"rich_text"
|
29
|
+
end
|
30
|
+
|
31
|
+
def csv_value
|
32
|
+
rich_text.body&.to_html
|
33
|
+
end
|
8
34
|
end
|
9
35
|
end
|
data/app/models/iron/locale.rb
CHANGED
@@ -0,0 +1,59 @@
|
|
1
|
+
module Iron
|
2
|
+
class SchemaImporter::ImportStrategy
|
3
|
+
def import_block_definition(row)
|
4
|
+
raise NotImplementedError
|
5
|
+
end
|
6
|
+
|
7
|
+
def import_content_type(row)
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
10
|
+
|
11
|
+
def import_field_definition(row)
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def find_parent(parent_type, parent_handle)
|
18
|
+
case parent_type
|
19
|
+
when "content_type"
|
20
|
+
ContentType.find_by(handle: parent_handle)
|
21
|
+
when "block"
|
22
|
+
BlockDefinition.find_by(handle: parent_handle)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def content_type_class(type)
|
27
|
+
case type
|
28
|
+
when "single" then ContentTypes::Single
|
29
|
+
when "collection" then ContentTypes::Collection
|
30
|
+
else
|
31
|
+
raise "Invalid content type: #{type}. Must be 'single' or 'collection'"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def field_definition_class(type)
|
36
|
+
"Iron::FieldDefinitions::#{type.camelize}".constantize
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse_metadata(metadata_string)
|
40
|
+
return {} if metadata_string.blank?
|
41
|
+
|
42
|
+
JSON.parse(metadata_string)
|
43
|
+
rescue JSON::ParserError
|
44
|
+
{}
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse_supported_block_definitions(supported_handles_string)
|
48
|
+
return BlockDefinition.none unless supported_handles_string.present?
|
49
|
+
|
50
|
+
BlockDefinition.where(handle: supported_handles_string.split("|"))
|
51
|
+
end
|
52
|
+
|
53
|
+
def parse_supported_content_types(supported_handles_string)
|
54
|
+
return ContentType.none unless supported_handles_string.present?
|
55
|
+
|
56
|
+
ContentType.where(handle: supported_handles_string.split("|"))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Iron
|
2
|
+
class SchemaImporter::MergeStrategy < SchemaImporter::ImportStrategy
|
3
|
+
def import_block_definition(row)
|
4
|
+
definition = BlockDefinition.find_or_initialize_by(handle: row["handle"])
|
5
|
+
definition.update!(
|
6
|
+
name: row["name"],
|
7
|
+
description: row["description"]
|
8
|
+
)
|
9
|
+
end
|
10
|
+
|
11
|
+
def import_content_type(row)
|
12
|
+
content_type = ContentType.find_or_initialize_by(handle: row["handle"])
|
13
|
+
|
14
|
+
content_type = content_type.becomes!(content_type_class(row["type"]))
|
15
|
+
content_type.update!(
|
16
|
+
name: row["name"],
|
17
|
+
description: row["description"],
|
18
|
+
icon: row["icon"],
|
19
|
+
web_publishing_enabled: row["web_publishing_enabled"] == "true",
|
20
|
+
base_path: row["base_path"]
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def import_field_definition(row)
|
25
|
+
raise "Field type is required" unless row["type"].present?
|
26
|
+
return unless row["parent_type"].present? && row["parent_handle"].present? && row["handle"].present?
|
27
|
+
|
28
|
+
parent = find_parent(row["parent_type"], row["parent_handle"])
|
29
|
+
return unless parent
|
30
|
+
|
31
|
+
field_def = parent.field_definitions.find_or_initialize_by(handle: row["handle"])
|
32
|
+
field_def = field_def.becomes!(field_definition_class(row["type"]))
|
33
|
+
|
34
|
+
attributes = {
|
35
|
+
name: row["name"],
|
36
|
+
rank: row["rank"],
|
37
|
+
metadata: parse_metadata(row["metadata"])
|
38
|
+
}
|
39
|
+
|
40
|
+
if field_def.is_a?(FieldDefinitions::Block) || field_def.is_a?(FieldDefinitions::BlockList)
|
41
|
+
attributes[:supported_block_definition_ids] = parse_supported_block_definitions(row["supported_block_definitions"]).pluck(:id)
|
42
|
+
end
|
43
|
+
|
44
|
+
if field_def.is_a?(FieldDefinitions::Reference) || field_def.is_a?(FieldDefinitions::ReferenceList)
|
45
|
+
attributes[:supported_content_type_ids] = parse_supported_content_types(row["supported_content_types"]).pluck(:id)
|
46
|
+
end
|
47
|
+
|
48
|
+
field_def.assign_attributes(attributes)
|
49
|
+
field_def.save!
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|