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,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
|
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,9 +1,11 @@
|
|
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
|
7
|
+
has_many :referencing_fields, class_name: "Iron::Field", foreign_key: :referenced_entry_id, dependent: :destroy
|
8
|
+
has_many :referencing_references, class_name: "Iron::Reference", dependent: :destroy
|
7
9
|
|
8
10
|
accepts_nested_attributes_for :fields, allow_destroy: true
|
9
11
|
|
@@ -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
@@ -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,34 @@
|
|
1
|
+
<% content_for :title, "Import Content" %>
|
2
|
+
|
3
|
+
<div class="">
|
4
|
+
<%= back_button_to "Settings", settings_path %>
|
5
|
+
<h1 class="page-title">Import Content</h1>
|
6
|
+
<p class="mt-2 text-sm text-stone-500">Upload a content archive exported from another Iron instance.</p>
|
7
|
+
|
8
|
+
<%= form_with url: import_content_path, multipart: true, class: "mt-8 max-w-2xl", data: { turbo: false } do |form| %>
|
9
|
+
<div class="field-group">
|
10
|
+
<div class="field">
|
11
|
+
<%= form.label :content_file, "Content Archive" %>
|
12
|
+
<div>
|
13
|
+
<%= form.file_field :content_file,
|
14
|
+
accept: ".tar",
|
15
|
+
required: true,
|
16
|
+
class: "input" %>
|
17
|
+
<p class="mt-2 text-sm text-stone-500">Select a .tar file exported using Iron's content export.</p>
|
18
|
+
</div>
|
19
|
+
</div>
|
20
|
+
|
21
|
+
<div class="field">
|
22
|
+
<label class="text-sm font-medium text-stone-700">Import behaviour</label>
|
23
|
+
<p class="text-sm text-stone-500 mt-1">
|
24
|
+
Entries with matching IDs and instance tokens are updated. Other records are created.
|
25
|
+
</p>
|
26
|
+
</div>
|
27
|
+
</div>
|
28
|
+
|
29
|
+
<div class="mt-6 flex items-center gap-x-3">
|
30
|
+
<%= form.submit "Import Content", class: "btn" %>
|
31
|
+
<%= link_to "Cancel", settings_path, class: "btn", data: { variant: "plain" } %>
|
32
|
+
</div>
|
33
|
+
<% end %>
|
34
|
+
</div>
|
@@ -35,4 +35,23 @@
|
|
35
35
|
</div>
|
36
36
|
<% end %>
|
37
37
|
</div>
|
38
|
+
|
39
|
+
<div class="bg-white dark:bg-stone-800/50 rounded-lg shadow-sm p-6 max-w-96">
|
40
|
+
<h2 class="text-lg font-semibold mb-4">Content Management</h2>
|
41
|
+
<p class="text-sm text-stone-600 dark:text-stone-400 mb-4">
|
42
|
+
Export content entries and their files or import an archive from another instance.
|
43
|
+
</p>
|
44
|
+
<div class="flex flex-col gap-2">
|
45
|
+
<%= link_to "Export Content",
|
46
|
+
export_content_path,
|
47
|
+
class: "btn",
|
48
|
+
data: {
|
49
|
+
turbo: false,
|
50
|
+
} %>
|
51
|
+
<%= link_to "Import Content",
|
52
|
+
new_content_path,
|
53
|
+
class: "btn",
|
54
|
+
data: { variant: "secondary" } %>
|
55
|
+
</div>
|
56
|
+
</div>
|
38
57
|
</div>
|
data/config/routes.rb
CHANGED
@@ -22,6 +22,11 @@ Iron::Engine.routes.draw do
|
|
22
22
|
post :import, on: :member
|
23
23
|
end
|
24
24
|
|
25
|
+
resource :content, only: [ :new ] do
|
26
|
+
get :export, on: :collection
|
27
|
+
post :import, on: :collection
|
28
|
+
end
|
29
|
+
|
25
30
|
resources :block_definitions do
|
26
31
|
resources :field_definitions, shallow: true, module: :block_definitions
|
27
32
|
end
|
data/lib/iron/engine.rb
CHANGED
@@ -31,6 +31,15 @@ module Iron
|
|
31
31
|
app.config.active_storage.analyzers.prepend Iron::ImageAnalyzer
|
32
32
|
end
|
33
33
|
|
34
|
+
initializer "iron.global_id_locator" do |app|
|
35
|
+
app.config.after_initialize do
|
36
|
+
global_id_app = ::GlobalID.app || ::Rails.application.config.try(:global_id)&.app
|
37
|
+
next unless global_id_app
|
38
|
+
|
39
|
+
::GlobalID::Locator.use(global_id_app, Iron::GlobalID::InstanceScopedLocator.new)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
34
43
|
config.to_prepare do
|
35
44
|
ActionView::Base.include(Module.new do
|
36
45
|
def attachment_url(attachment, **options)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "globalid"
|
2
|
+
|
3
|
+
module Iron
|
4
|
+
module GlobalID
|
5
|
+
class InstanceScopedLocator < ::GlobalID::Locator::UnscopedLocator
|
6
|
+
def locate(gid, options = {})
|
7
|
+
return unless instance_matches?(gid)
|
8
|
+
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def locate_many(gids, options = {})
|
13
|
+
filtered_gids = gids.select { |candidate| instance_matches?(candidate) }
|
14
|
+
return [] if filtered_gids.empty?
|
15
|
+
|
16
|
+
super(filtered_gids, options)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def instance_matches?(gid)
|
22
|
+
return true if gid.params.nil?
|
23
|
+
|
24
|
+
instance_token = gid.params[:instance]
|
25
|
+
return true if instance_token.blank?
|
26
|
+
|
27
|
+
Current.account&.instance_token == instance_token
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/iron/version.rb
CHANGED
data/lib/iron.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: iron-cms
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Massimo De Marchi
|
@@ -198,6 +198,7 @@ files:
|
|
198
198
|
- app/controllers/iron/blocks_controller.rb
|
199
199
|
- app/controllers/iron/content_types/field_definitions_controller.rb
|
200
200
|
- app/controllers/iron/content_types_controller.rb
|
201
|
+
- app/controllers/iron/contents_controller.rb
|
201
202
|
- app/controllers/iron/entries_controller.rb
|
202
203
|
- app/controllers/iron/field_definitions_controller.rb
|
203
204
|
- app/controllers/iron/first_runs_controller.rb
|
@@ -239,11 +240,21 @@ files:
|
|
239
240
|
- app/mailers/iron/application_mailer.rb
|
240
241
|
- app/mailers/passwords_mailer.rb
|
241
242
|
- app/models/concerns/iron/csv_serializable.rb
|
243
|
+
- app/models/concerns/iron/instance_scoped.rb
|
242
244
|
- app/models/iron/account.rb
|
243
245
|
- app/models/iron/account/joinable.rb
|
244
246
|
- app/models/iron/application_record.rb
|
247
|
+
- app/models/iron/archive.rb
|
245
248
|
- app/models/iron/block_definition.rb
|
246
249
|
- app/models/iron/block_definition/portable.rb
|
250
|
+
- app/models/iron/content_export.rb
|
251
|
+
- app/models/iron/content_import.rb
|
252
|
+
- app/models/iron/content_import/entry_builder.rb
|
253
|
+
- app/models/iron/content_import/entry_snapshot.rb
|
254
|
+
- app/models/iron/content_import/field_reconstructor.rb
|
255
|
+
- app/models/iron/content_import/field_snapshot.rb
|
256
|
+
- app/models/iron/content_import/registry.rb
|
257
|
+
- app/models/iron/content_import/session.rb
|
247
258
|
- app/models/iron/content_type.rb
|
248
259
|
- app/models/iron/content_type/field_queryable.rb
|
249
260
|
- app/models/iron/content_type/portable.rb
|
@@ -254,12 +265,14 @@ files:
|
|
254
265
|
- app/models/iron/current.rb
|
255
266
|
- app/models/iron/entry.rb
|
256
267
|
- app/models/iron/entry/deep_validation.rb
|
268
|
+
- app/models/iron/entry/portable.rb
|
257
269
|
- app/models/iron/entry/presentable.rb
|
258
270
|
- app/models/iron/entry/schemable.rb
|
259
271
|
- app/models/iron/entry/titlable.rb
|
260
272
|
- app/models/iron/entry/web_publishable.rb
|
261
273
|
- app/models/iron/field.rb
|
262
274
|
- app/models/iron/field/belongs_to_entry.rb
|
275
|
+
- app/models/iron/field/portable.rb
|
263
276
|
- app/models/iron/field_definition.rb
|
264
277
|
- app/models/iron/field_definition/portable.rb
|
265
278
|
- app/models/iron/field_definitions/block.rb
|
@@ -312,6 +325,7 @@ files:
|
|
312
325
|
- app/views/iron/content_types/index.html.erb
|
313
326
|
- app/views/iron/content_types/new.html.erb
|
314
327
|
- app/views/iron/content_types/show.html.erb
|
328
|
+
- app/views/iron/contents/new.html.erb
|
315
329
|
- app/views/iron/entries/_entry.html.erb
|
316
330
|
- app/views/iron/entries/_entry_option.html.erb
|
317
331
|
- app/views/iron/entries/_form.html.erb
|
@@ -394,6 +408,7 @@ files:
|
|
394
408
|
- db/migrate/20250601080146_add_icon_to_iron_content_types.rb
|
395
409
|
- db/migrate/20250609091605_add_web_publishing_to_iron_content_types.rb
|
396
410
|
- db/migrate/20250609091813_add_route_to_iron_entries.rb
|
411
|
+
- db/migrate/20250908203158_add_instance_token_to_iron_accounts.rb
|
397
412
|
- lib/generators/iron/pages/pages_generator.rb
|
398
413
|
- lib/generators/iron/pages/templates/pages_controller.rb
|
399
414
|
- lib/generators/iron/pages/templates/show.html.erb
|
@@ -403,6 +418,7 @@ files:
|
|
403
418
|
- lib/iron.rb
|
404
419
|
- lib/iron/cva.rb
|
405
420
|
- lib/iron/engine.rb
|
421
|
+
- lib/iron/global_id/instance_scoped_locator.rb
|
406
422
|
- lib/iron/image_analyzer.rb
|
407
423
|
- lib/iron/lexorank.rb
|
408
424
|
- lib/iron/lexorank/exceptions.rb
|