iron-cms 0.11.0 → 0.13.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 +19 -20
- data/app/assets/builds/iron.css +3 -0
- data/app/controllers/concerns/iron/admin_locale.rb +16 -1
- data/app/controllers/concerns/iron/authentication.rb +2 -1
- data/app/controllers/iron/passwords_controller.rb +2 -1
- data/app/controllers/iron/sessions/transfers_controller.rb +1 -1
- data/app/controllers/iron/sessions_controller.rb +1 -1
- data/app/controllers/iron/users/reactivations_controller.rb +18 -0
- data/app/controllers/iron/users/roles_controller.rb +1 -1
- data/app/controllers/iron/users_controller.rb +6 -5
- data/app/mailers/iron/application_mailer.rb +0 -1
- data/app/models/iron/account/export.rb +3 -61
- data/app/models/iron/account/import.rb +1 -179
- data/app/models/iron/block_definition/importable.rb +6 -12
- data/app/models/iron/exporter.rb +75 -0
- data/app/models/iron/importer.rb +211 -0
- data/app/models/iron/seed.rb +77 -0
- data/app/models/iron/user/deactivatable.rb +23 -0
- data/app/models/iron/user.rb +1 -9
- data/app/views/iron/block_definitions/_empty_state.html.erb +3 -3
- data/app/views/iron/block_definitions/edit.html.erb +5 -5
- data/app/views/iron/block_definitions/index.html.erb +4 -4
- data/app/views/iron/block_definitions/new.html.erb +3 -3
- data/app/views/iron/block_definitions/show.html.erb +8 -8
- data/app/views/iron/entries/fields/_block.html.erb +3 -3
- data/app/views/iron/entries/fields/_file.html.erb +3 -3
- data/app/views/iron/field_definitions/block/_form.html.erb +2 -2
- data/app/views/iron/field_definitions/block_list/_form.html.erb +1 -1
- data/app/views/iron/field_definitions/edit.html.erb +3 -3
- data/app/views/iron/field_definitions/file/_form.html.erb +4 -4
- data/app/views/iron/field_definitions/layouts/_form.html.erb +1 -1
- data/app/views/iron/field_definitions/new.html.erb +3 -3
- data/app/views/iron/field_definitions/reference/_form.html.erb +1 -1
- data/app/views/iron/field_definitions/reference_list/_form.html.erb +1 -1
- data/app/views/iron/field_definitions/text_field/_form.html.erb +3 -3
- data/app/views/iron/first_runs/show.html.erb +4 -4
- data/app/views/iron/locales/_form.html.erb +1 -1
- data/app/views/iron/published_pages/show.html.erb +2 -2
- data/app/views/iron/users/index.html.erb +25 -0
- data/config/locales/en.yml +101 -1
- data/config/locales/it.yml +101 -1
- data/config/routes.rb +1 -0
- data/db/migrate/20260209215027_add_active_to_iron_users.rb +5 -0
- data/lib/iron/version.rb +1 -1
- data/lib/iron.rb +0 -1
- data/lib/tasks/iron_tasks.rake +10 -19
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6f8165107a118a6f3a8dcefe95984760653d8baf16f96653642382fc73ce8aca
|
|
4
|
+
data.tar.gz: 7143fd0c4f78a53e4b945f52005cc1f68e225674667f0f6a8bce8dcb8b77a02d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e09696ccbfb17bf2a82d515e7297f097a4a6fbdeb8a30626bf1cb1e44818ae9f55f260fa6b7e04acce6707d1d212305318cddf6db545f62d04b37d919191c73f
|
|
7
|
+
data.tar.gz: be54e07e53ca096cb56e66887eab33085f76545716a61dfa7976866303f224867d9766eceaa338a65ecd3182d5b2b7af2003226d98c52d64dd17a68b912cc7e3
|
data/README.md
CHANGED
|
@@ -96,37 +96,36 @@ For cases where you need a basic responsive image without format optimization:
|
|
|
96
96
|
|
|
97
97
|
This generates a standard `<img>` with srcset but without modern format variants.
|
|
98
98
|
|
|
99
|
-
###
|
|
99
|
+
### Development Seeding
|
|
100
100
|
|
|
101
|
-
Iron provides
|
|
101
|
+
Iron provides a seeding mechanism to snapshot and restore CMS schema and content, making it easy for teammates to get a consistent development database.
|
|
102
102
|
|
|
103
|
-
####
|
|
103
|
+
#### Creating a seed
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
After setting up your content types and sample content via the admin UI:
|
|
106
106
|
|
|
107
107
|
```bash
|
|
108
|
-
bin/rails iron:
|
|
108
|
+
bin/rails iron:seed:dump
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
This
|
|
111
|
+
This exports the full CMS state to `db/seeds/iron.zip`. Commit this file to your repository.
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
- `content.tar.gz` - All entries and their field data
|
|
113
|
+
#### Loading a seed
|
|
115
114
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
#### Using Fixtures in Tests
|
|
119
|
-
|
|
120
|
-
Load the fixtures in your `test/test_helper.rb`:
|
|
115
|
+
Add this to your `db/seeds.rb`:
|
|
121
116
|
|
|
122
117
|
```ruby
|
|
123
|
-
|
|
124
|
-
fixtures :all
|
|
125
|
-
Iron::TestFixtures.load
|
|
126
|
-
end
|
|
118
|
+
Iron::Seed.load
|
|
127
119
|
```
|
|
128
120
|
|
|
129
|
-
|
|
121
|
+
Then `rails db:prepare` (or `rails db:seed`) will automatically bootstrap the CMS when the database is empty. Loading is skipped if content types already exist.
|
|
122
|
+
|
|
123
|
+
#### Admin credentials
|
|
124
|
+
|
|
125
|
+
The seed creates a default admin user during loading. Configure via environment variables:
|
|
126
|
+
|
|
127
|
+
- `IRON_SEED_EMAIL` (default: `admin@example.com`)
|
|
128
|
+
- `IRON_SEED_PASSWORD` (default: `password`)
|
|
130
129
|
|
|
131
130
|
## System Requirements
|
|
132
131
|
|
|
@@ -178,8 +177,8 @@ bin/rails iron:install:migrations
|
|
|
178
177
|
Configure the mailer sender address used for system emails (e.g. password resets):
|
|
179
178
|
|
|
180
179
|
```ruby
|
|
181
|
-
# config/
|
|
182
|
-
|
|
180
|
+
# config/environments/production.rb
|
|
181
|
+
config.action_mailer.default_options = { from: "hello@mysite.com" }
|
|
183
182
|
```
|
|
184
183
|
|
|
185
184
|
## Architecture
|
data/app/assets/builds/iron.css
CHANGED
|
@@ -3429,6 +3429,9 @@
|
|
|
3429
3429
|
.opacity-0 {
|
|
3430
3430
|
opacity: 0%;
|
|
3431
3431
|
}
|
|
3432
|
+
.opacity-50 {
|
|
3433
|
+
opacity: 50%;
|
|
3434
|
+
}
|
|
3432
3435
|
.shadow {
|
|
3433
3436
|
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
|
3434
3437
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
@@ -8,7 +8,22 @@ module Iron
|
|
|
8
8
|
|
|
9
9
|
private
|
|
10
10
|
def set_admin_locale(&action)
|
|
11
|
-
I18n.with_locale(
|
|
11
|
+
I18n.with_locale(preferred_admin_language, &action)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def preferred_admin_language
|
|
15
|
+
Current.user&.language || browser_admin_language || "en"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def browser_admin_language
|
|
19
|
+
browser_language_tags.find { |tag| User::ADMIN_LANGUAGES.include?(tag) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def browser_language_tags
|
|
23
|
+
request.accept_language.to_s.split(",").flat_map { |entry|
|
|
24
|
+
tag = entry.split(";").first.strip.downcase
|
|
25
|
+
[ tag, tag.split("-").first ]
|
|
26
|
+
}.reject(&:blank?).uniq
|
|
12
27
|
end
|
|
13
28
|
end
|
|
14
29
|
end
|
|
@@ -9,7 +9,7 @@ module Iron
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def create
|
|
12
|
-
if user = User.find_by(email_address: params[:email_address])
|
|
12
|
+
if user = User.active.find_by(email_address: params[:email_address])
|
|
13
13
|
Iron::PasswordsMailer.reset(user).deliver_later
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -36,6 +36,7 @@ module Iron
|
|
|
36
36
|
|
|
37
37
|
def set_user_by_token
|
|
38
38
|
@user = User.find_by_password_reset_token!(params[:token])
|
|
39
|
+
redirect_to new_password_url, alert: t("iron.passwords.alerts.reset_link_invalid") unless @user.active?
|
|
39
40
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
|
40
41
|
redirect_to new_password_url, alert: t("iron.passwords.alerts.reset_link_invalid")
|
|
41
42
|
end
|
|
@@ -10,7 +10,7 @@ module Iron
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def create
|
|
13
|
-
if user = User.authenticate_by(params.permit(:email_address, :password))
|
|
13
|
+
if user = User.active.authenticate_by(params.permit(:email_address, :password))
|
|
14
14
|
start_new_session_for user
|
|
15
15
|
redirect_to after_authentication_url
|
|
16
16
|
else
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Iron
|
|
2
|
+
module Users
|
|
3
|
+
class ReactivationsController < ApplicationController
|
|
4
|
+
before_action :set_user
|
|
5
|
+
before_action :ensure_can_administer
|
|
6
|
+
|
|
7
|
+
def update
|
|
8
|
+
@user.reactivate
|
|
9
|
+
redirect_to @user, notice: t("iron.users.notifications.reactivated")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
def set_user
|
|
14
|
+
@user = User.find(params.expect(:user_id))
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -8,7 +8,8 @@ module Iron
|
|
|
8
8
|
before_action :ensure_can_administer, only: %i[ index destroy ]
|
|
9
9
|
|
|
10
10
|
def index
|
|
11
|
-
@users = User.
|
|
11
|
+
@users = User.active
|
|
12
|
+
@deactivated_users = User.where(active: false)
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def new
|
|
@@ -19,7 +20,7 @@ module Iron
|
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def create
|
|
22
|
-
@user = User.new(join_params)
|
|
23
|
+
@user = User.new(join_params.merge(language: preferred_admin_language))
|
|
23
24
|
if @user.save
|
|
24
25
|
start_new_session_for @user
|
|
25
26
|
redirect_to root_url
|
|
@@ -31,8 +32,8 @@ module Iron
|
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def destroy
|
|
34
|
-
if @user.
|
|
35
|
-
|
|
35
|
+
if @user.deactivate
|
|
36
|
+
redirect_to users_path, notice: t("iron.users.notifications.removed"), status: :see_other
|
|
36
37
|
else
|
|
37
38
|
redirect_back fallback_location: users_path, alert: @user.errors.full_messages.to_sentence, status: :see_other
|
|
38
39
|
end
|
|
@@ -40,7 +41,7 @@ module Iron
|
|
|
40
41
|
|
|
41
42
|
private
|
|
42
43
|
def set_user
|
|
43
|
-
@user = User.find(params.expect(:id))
|
|
44
|
+
@user = User.active.find(params.expect(:id))
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
def join_params
|
|
@@ -17,70 +17,12 @@ module Iron
|
|
|
17
17
|
def perform
|
|
18
18
|
raise ArgumentError, "Nothing selected to export" unless include_schema? || include_content?
|
|
19
19
|
|
|
20
|
-
zipfile =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
filename: "iron-export-#{id}.zip",
|
|
24
|
-
content_type: "application/zip"
|
|
25
|
-
)
|
|
20
|
+
zipfile = Tempfile.new([ "export", ".zip" ])
|
|
21
|
+
Exporter.new(zipfile.path, include_schema: include_schema?, include_content: include_content?).export
|
|
22
|
+
file.attach(io: File.open(zipfile.path), filename: "iron-export-#{id}.zip", content_type: "application/zip")
|
|
26
23
|
ensure
|
|
27
24
|
zipfile&.close
|
|
28
25
|
zipfile&.unlink
|
|
29
26
|
end
|
|
30
|
-
|
|
31
|
-
def generate_zip
|
|
32
|
-
Tempfile.new([ "export", ".zip" ]).tap do |tempfile|
|
|
33
|
-
Zip::File.open(tempfile.path, create: true) do |zip|
|
|
34
|
-
add_schema_to_zip(zip) if include_schema?
|
|
35
|
-
add_content_to_zip(zip) if include_content?
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def add_schema_to_zip(zip)
|
|
41
|
-
zip.get_output_stream("schema.json") { |f| f.write(export_schema_json) }
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def add_content_to_zip(zip)
|
|
45
|
-
exportable_entries.find_each do |entry|
|
|
46
|
-
path = "entries/#{entry.content_type.handle}/#{entry.id}.json"
|
|
47
|
-
zip.get_output_stream(path) { |f| f.write(entry.export_json) }
|
|
48
|
-
|
|
49
|
-
entry.export_attachments.each do |attachment|
|
|
50
|
-
zip.get_output_stream("files/#{attachment[:path]}", compression_method: Zip::Entry::STORED) do |f|
|
|
51
|
-
attachment[:blob].download { |chunk| f.write(chunk) }
|
|
52
|
-
end
|
|
53
|
-
rescue ActiveStorage::FileNotFoundError
|
|
54
|
-
# Skip missing files
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def export_schema_json
|
|
60
|
-
JSON.pretty_generate(
|
|
61
|
-
version: "1.0",
|
|
62
|
-
exported_at: Time.current.iso8601,
|
|
63
|
-
block_definitions: export_block_definitions,
|
|
64
|
-
content_types: export_content_types
|
|
65
|
-
)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def export_block_definitions
|
|
69
|
-
BlockDefinition
|
|
70
|
-
.includes(:field_definitions)
|
|
71
|
-
.order(:handle)
|
|
72
|
-
.map(&:export_attributes)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def export_content_types
|
|
76
|
-
ContentType
|
|
77
|
-
.includes(:title_field_definition, :web_page_title_field_definition, :field_definitions)
|
|
78
|
-
.order(:handle)
|
|
79
|
-
.map(&:export_attributes)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def exportable_entries
|
|
83
|
-
Entry.includes(:content_type, :creator, fields: [ :locale, :definition ])
|
|
84
|
-
end
|
|
85
27
|
end
|
|
86
28
|
end
|
|
@@ -18,190 +18,12 @@ module Iron
|
|
|
18
18
|
def perform
|
|
19
19
|
raise ArgumentError, "Nothing selected to import" unless include_schema? || include_content?
|
|
20
20
|
|
|
21
|
-
import_from_zip
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def import_from_zip
|
|
25
21
|
Tempfile.create([ "import", ".zip" ]) do |tempfile|
|
|
26
22
|
tempfile.binmode
|
|
27
23
|
tempfile.write(file.download)
|
|
28
24
|
tempfile.rewind
|
|
29
25
|
|
|
30
|
-
|
|
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
|
|
26
|
+
Importer.new(tempfile.path, include_schema: include_schema?, include_content: include_content?).import
|
|
205
27
|
end
|
|
206
28
|
end
|
|
207
29
|
end
|
|
@@ -4,21 +4,15 @@ module Iron
|
|
|
4
4
|
|
|
5
5
|
class_methods do
|
|
6
6
|
def import_from_attributes(attrs)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
description: attrs[:description]
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
import_field_definitions(block_def, attrs[:field_definitions] || [])
|
|
14
|
-
|
|
15
|
-
block_def
|
|
7
|
+
find_or_initialize_by(handle: attrs[:handle]).tap do |block_def|
|
|
8
|
+
block_def.update!(name: attrs[:name], description: attrs[:description])
|
|
9
|
+
end
|
|
16
10
|
end
|
|
17
11
|
|
|
18
|
-
|
|
12
|
+
def populate_field_definitions(attrs)
|
|
13
|
+
block_def = find_by!(handle: attrs[:handle])
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
field_attrs_list.each do |field_attrs|
|
|
15
|
+
(attrs[:field_definitions] || []).each do |field_attrs|
|
|
22
16
|
FieldDefinition.import_from_attributes(field_attrs, schemable: block_def)
|
|
23
17
|
end
|
|
24
18
|
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Iron
|
|
2
|
+
class Exporter
|
|
3
|
+
def initialize(path, include_schema: true, include_content: true)
|
|
4
|
+
@path = path.to_s
|
|
5
|
+
@include_schema = include_schema
|
|
6
|
+
@include_content = include_content
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def export
|
|
10
|
+
Zip::File.open(path, create: true) do |zip|
|
|
11
|
+
write_schema(zip) if schema?
|
|
12
|
+
write_entries(zip) if content?
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
attr_reader :path
|
|
19
|
+
|
|
20
|
+
def schema? = @include_schema
|
|
21
|
+
def content? = @include_content
|
|
22
|
+
|
|
23
|
+
def write_schema(zip)
|
|
24
|
+
zip.get_output_stream("schema.json") { |f| f.write(schema_json) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def write_entries(zip)
|
|
28
|
+
exportable_entries.find_each do |entry|
|
|
29
|
+
path = "entries/#{entry.content_type.handle}/#{entry.id}.json"
|
|
30
|
+
zip.get_output_stream(path) { |f| f.write(entry.export_json) }
|
|
31
|
+
|
|
32
|
+
entry.export_attachments.each do |attachment|
|
|
33
|
+
zip.get_output_stream("files/#{attachment[:path]}", compression_method: Zip::Entry::STORED) do |f|
|
|
34
|
+
attachment[:blob].download { |chunk| f.write(chunk) }
|
|
35
|
+
end
|
|
36
|
+
rescue ActiveStorage::FileNotFoundError
|
|
37
|
+
# Skip missing files
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def schema_json
|
|
43
|
+
JSON.pretty_generate(
|
|
44
|
+
version: "1.0",
|
|
45
|
+
exported_at: Time.current.iso8601,
|
|
46
|
+
default_locale: Current.account&.default_locale&.code,
|
|
47
|
+
locales: export_locales,
|
|
48
|
+
block_definitions: export_block_definitions,
|
|
49
|
+
content_types: export_content_types
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def export_locales
|
|
54
|
+
Locale.order(:code).map { |l| { code: l.code, name: l.name } }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def export_block_definitions
|
|
58
|
+
BlockDefinition
|
|
59
|
+
.includes(:field_definitions)
|
|
60
|
+
.order(:handle)
|
|
61
|
+
.map(&:export_attributes)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def export_content_types
|
|
65
|
+
ContentType
|
|
66
|
+
.includes(:title_field_definition, :web_page_title_field_definition, :field_definitions)
|
|
67
|
+
.order(:handle)
|
|
68
|
+
.map(&:export_attributes)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def exportable_entries
|
|
72
|
+
Entry.includes(:content_type, :creator, fields: [ :locale, :definition ])
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|