iron-cms 0.6.0 → 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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/iron.css +668 -388
  3. data/app/assets/tailwind/iron/application.css +1 -0
  4. data/app/assets/tailwind/iron/components/button.css +0 -7
  5. data/app/assets/tailwind/iron/components/checkbox.css +21 -0
  6. data/app/assets/tailwind/iron/components/form.css +1 -1
  7. data/app/assets/tailwind/iron/lexxy.css +165 -51
  8. data/app/controllers/iron/account/exports_controller.rb +26 -0
  9. data/app/controllers/iron/account/imports_controller.rb +27 -0
  10. data/app/helpers/iron/form_builder.rb +7 -0
  11. data/app/jobs/iron/export_job.rb +9 -0
  12. data/app/jobs/iron/import_job.rb +9 -0
  13. data/app/models/concerns/iron/broadcastable.rb +9 -0
  14. data/app/models/concerns/iron/processable.rb +34 -0
  15. data/app/models/iron/account/export.rb +86 -0
  16. data/app/models/iron/account/import.rb +208 -0
  17. data/app/models/iron/block_definition/exportable.rb +14 -0
  18. data/app/models/iron/block_definition/importable.rb +27 -0
  19. data/app/models/iron/block_definition.rb +1 -1
  20. data/app/models/iron/content_type/exportable.rb +20 -0
  21. data/app/models/iron/content_type/importable.rb +32 -0
  22. data/app/models/iron/content_type.rb +1 -1
  23. data/app/models/iron/current.rb +6 -3
  24. data/app/models/iron/entry/exportable.rb +49 -0
  25. data/app/models/iron/entry/importable.rb +181 -0
  26. data/app/models/iron/entry.rb +1 -1
  27. data/app/models/iron/field.rb +9 -1
  28. data/app/models/iron/field_definition/exportable.rb +23 -0
  29. data/app/models/iron/field_definition/importable.rb +39 -0
  30. data/app/models/iron/field_definition.rb +1 -1
  31. data/app/models/iron/fields/block.rb +18 -0
  32. data/app/models/iron/fields/block_list.rb +8 -0
  33. data/app/models/iron/fields/boolean.rb +4 -0
  34. data/app/models/iron/fields/date.rb +4 -0
  35. data/app/models/iron/fields/file.rb +16 -0
  36. data/app/models/iron/fields/number.rb +4 -0
  37. data/app/models/iron/fields/reference.rb +4 -0
  38. data/app/models/iron/fields/reference_list.rb +4 -0
  39. data/app/models/iron/fields/rich_text_area.rb +32 -0
  40. data/app/models/iron/fields/text_area.rb +4 -0
  41. data/app/models/iron/fields/text_field.rb +4 -0
  42. data/app/models/iron/user.rb +2 -0
  43. data/app/views/iron/account/exports/index.html.erb +43 -0
  44. data/app/views/iron/account/exports/new.html.erb +39 -0
  45. data/app/views/iron/account/exports/show.html.erb +40 -0
  46. data/app/views/iron/account/imports/index.html.erb +43 -0
  47. data/app/views/iron/account/imports/new.html.erb +52 -0
  48. data/app/views/iron/account/imports/show.html.erb +37 -0
  49. data/app/views/iron/content_types/index.html.erb +1 -8
  50. data/app/views/iron/entries/fields/_file.html.erb +3 -3
  51. data/app/views/iron/settings/show.html.erb +4 -11
  52. data/config/routes.rb +3 -9
  53. data/db/migrate/20251209103109_create_iron_account_exports.rb +13 -0
  54. data/db/migrate/20251209103110_create_iron_account_imports.rb +13 -0
  55. data/lib/iron/version.rb +1 -1
  56. data/lib/iron.rb +1 -1
  57. metadata +40 -28
  58. data/app/controllers/iron/contents_controller.rb +0 -33
  59. data/app/controllers/iron/schemas_controller.rb +0 -32
  60. data/app/models/concerns/iron/csv_serializable.rb +0 -28
  61. data/app/models/iron/archive.rb +0 -69
  62. data/app/models/iron/block_definition/portable.rb +0 -20
  63. data/app/models/iron/content_export.rb +0 -73
  64. data/app/models/iron/content_import/entry_builder.rb +0 -80
  65. data/app/models/iron/content_import/entry_snapshot.rb +0 -23
  66. data/app/models/iron/content_import/field_reconstructor.rb +0 -276
  67. data/app/models/iron/content_import/field_snapshot.rb +0 -33
  68. data/app/models/iron/content_import/registry.rb +0 -32
  69. data/app/models/iron/content_import/session.rb +0 -89
  70. data/app/models/iron/content_import.rb +0 -15
  71. data/app/models/iron/content_type/portable.rb +0 -30
  72. data/app/models/iron/entry/portable.rb +0 -35
  73. data/app/models/iron/field/portable.rb +0 -33
  74. data/app/models/iron/field_definition/portable.rb +0 -42
  75. data/app/models/iron/schema_archive.rb +0 -71
  76. data/app/models/iron/schema_exporter.rb +0 -15
  77. data/app/models/iron/schema_importer/import_strategy.rb +0 -59
  78. data/app/models/iron/schema_importer/merge_strategy.rb +0 -52
  79. data/app/models/iron/schema_importer/replace_strategy.rb +0 -51
  80. data/app/models/iron/schema_importer/safe_strategy.rb +0 -55
  81. data/app/models/iron/schema_importer.rb +0 -108
  82. data/app/views/iron/contents/new.html.erb +0 -34
  83. data/app/views/iron/schemas/new.html.erb +0 -57
  84. data/lib/iron/test_fixtures.rb +0 -50
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.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Massimo De Marchi
@@ -177,6 +177,20 @@ dependencies:
177
177
  - - "~>"
178
178
  - !ruby/object:Gem::Version
179
179
  version: '3.3'
180
+ - !ruby/object:Gem::Dependency
181
+ name: rubyzip
182
+ requirement: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - "~>"
185
+ - !ruby/object:Gem::Version
186
+ version: '2.3'
187
+ type: :runtime
188
+ prerelease: false
189
+ version_requirements: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - "~>"
192
+ - !ruby/object:Gem::Version
193
+ version: '2.3'
180
194
  description: A Rails engine for managing and publishing content with a flexible schema.
181
195
  email:
182
196
  - massimo@inkofpixel.com
@@ -193,6 +207,7 @@ files:
193
207
  - app/assets/tailwind/iron/application.css
194
208
  - app/assets/tailwind/iron/components/badge.css
195
209
  - app/assets/tailwind/iron/components/button.css
210
+ - app/assets/tailwind/iron/components/checkbox.css
196
211
  - app/assets/tailwind/iron/components/dropdown.css
197
212
  - app/assets/tailwind/iron/components/fieldset.css
198
213
  - app/assets/tailwind/iron/components/form.css
@@ -206,13 +221,14 @@ files:
206
221
  - app/controllers/concerns/iron/authorization.rb
207
222
  - app/controllers/concerns/iron/locale_aware.rb
208
223
  - app/controllers/concerns/iron/web_page.rb
224
+ - app/controllers/iron/account/exports_controller.rb
225
+ - app/controllers/iron/account/imports_controller.rb
209
226
  - app/controllers/iron/application_controller.rb
210
227
  - app/controllers/iron/block_definitions/field_definitions_controller.rb
211
228
  - app/controllers/iron/block_definitions_controller.rb
212
229
  - app/controllers/iron/blocks_controller.rb
213
230
  - app/controllers/iron/content_types/field_definitions_controller.rb
214
231
  - app/controllers/iron/content_types_controller.rb
215
- - app/controllers/iron/contents_controller.rb
216
232
  - app/controllers/iron/entries_controller.rb
217
233
  - app/controllers/iron/field_definitions_controller.rb
218
234
  - app/controllers/iron/first_runs_controller.rb
@@ -221,7 +237,6 @@ files:
221
237
  - app/controllers/iron/locales_controller.rb
222
238
  - app/controllers/iron/passwords_controller.rb
223
239
  - app/controllers/iron/references_controller.rb
224
- - app/controllers/iron/schemas_controller.rb
225
240
  - app/controllers/iron/sessions_controller.rb
226
241
  - app/controllers/iron/settings_controller.rb
227
242
  - app/controllers/iron/users_controller.rb
@@ -247,28 +262,26 @@ files:
247
262
  - app/javascript/iron/controllers/toggle_controller.js
248
263
  - app/javascript/iron/lib/lexorank.js
249
264
  - app/jobs/iron/application_job.rb
265
+ - app/jobs/iron/export_job.rb
250
266
  - app/jobs/iron/generate_entry_routes_job.rb
267
+ - app/jobs/iron/import_job.rb
251
268
  - app/mailers/iron/application_mailer.rb
252
269
  - app/mailers/passwords_mailer.rb
253
- - app/models/concerns/iron/csv_serializable.rb
270
+ - app/models/concerns/iron/broadcastable.rb
254
271
  - app/models/concerns/iron/instance_scoped.rb
272
+ - app/models/concerns/iron/processable.rb
255
273
  - app/models/iron/account.rb
274
+ - app/models/iron/account/export.rb
275
+ - app/models/iron/account/import.rb
256
276
  - app/models/iron/account/joinable.rb
257
277
  - app/models/iron/application_record.rb
258
- - app/models/iron/archive.rb
259
278
  - app/models/iron/block_definition.rb
260
- - app/models/iron/block_definition/portable.rb
261
- - app/models/iron/content_export.rb
262
- - app/models/iron/content_import.rb
263
- - app/models/iron/content_import/entry_builder.rb
264
- - app/models/iron/content_import/entry_snapshot.rb
265
- - app/models/iron/content_import/field_reconstructor.rb
266
- - app/models/iron/content_import/field_snapshot.rb
267
- - app/models/iron/content_import/registry.rb
268
- - app/models/iron/content_import/session.rb
279
+ - app/models/iron/block_definition/exportable.rb
280
+ - app/models/iron/block_definition/importable.rb
269
281
  - app/models/iron/content_type.rb
282
+ - app/models/iron/content_type/exportable.rb
270
283
  - app/models/iron/content_type/field_queryable.rb
271
- - app/models/iron/content_type/portable.rb
284
+ - app/models/iron/content_type/importable.rb
272
285
  - app/models/iron/content_type/titlable.rb
273
286
  - app/models/iron/content_type/web_publishable.rb
274
287
  - app/models/iron/content_types/collection.rb
@@ -276,16 +289,17 @@ files:
276
289
  - app/models/iron/current.rb
277
290
  - app/models/iron/entry.rb
278
291
  - app/models/iron/entry/deep_validation.rb
279
- - app/models/iron/entry/portable.rb
292
+ - app/models/iron/entry/exportable.rb
293
+ - app/models/iron/entry/importable.rb
280
294
  - app/models/iron/entry/presentable.rb
281
295
  - app/models/iron/entry/schemable.rb
282
296
  - app/models/iron/entry/titlable.rb
283
297
  - app/models/iron/entry/web_publishable.rb
284
298
  - app/models/iron/field.rb
285
299
  - app/models/iron/field/belongs_to_entry.rb
286
- - app/models/iron/field/portable.rb
287
300
  - app/models/iron/field_definition.rb
288
- - app/models/iron/field_definition/portable.rb
301
+ - app/models/iron/field_definition/exportable.rb
302
+ - app/models/iron/field_definition/importable.rb
289
303
  - app/models/iron/field_definitions/block.rb
290
304
  - app/models/iron/field_definitions/block_list.rb
291
305
  - app/models/iron/field_definitions/boolean.rb
@@ -312,17 +326,16 @@ files:
312
326
  - app/models/iron/icon_catalog.rb
313
327
  - app/models/iron/locale.rb
314
328
  - app/models/iron/reference.rb
315
- - app/models/iron/schema_archive.rb
316
- - app/models/iron/schema_exporter.rb
317
- - app/models/iron/schema_importer.rb
318
- - app/models/iron/schema_importer/import_strategy.rb
319
- - app/models/iron/schema_importer/merge_strategy.rb
320
- - app/models/iron/schema_importer/replace_strategy.rb
321
- - app/models/iron/schema_importer/safe_strategy.rb
322
329
  - app/models/iron/session.rb
323
330
  - app/models/iron/user.rb
324
331
  - app/models/iron/user/role.rb
325
332
  - app/views/active_storage/blobs/_blob.html.erb
333
+ - app/views/iron/account/exports/index.html.erb
334
+ - app/views/iron/account/exports/new.html.erb
335
+ - app/views/iron/account/exports/show.html.erb
336
+ - app/views/iron/account/imports/index.html.erb
337
+ - app/views/iron/account/imports/new.html.erb
338
+ - app/views/iron/account/imports/show.html.erb
326
339
  - app/views/iron/block_definitions/_block_definition.html.erb
327
340
  - app/views/iron/block_definitions/_form.html.erb
328
341
  - app/views/iron/block_definitions/edit.html.erb
@@ -336,7 +349,6 @@ files:
336
349
  - app/views/iron/content_types/index.html.erb
337
350
  - app/views/iron/content_types/new.html.erb
338
351
  - app/views/iron/content_types/show.html.erb
339
- - app/views/iron/contents/new.html.erb
340
352
  - app/views/iron/entries/_entry.html.erb
341
353
  - app/views/iron/entries/_entry_option.html.erb
342
354
  - app/views/iron/entries/_form.html.erb
@@ -387,7 +399,6 @@ files:
387
399
  - app/views/iron/passwords_mailer/reset.text.erb
388
400
  - app/views/iron/published_pages/show.html.erb
389
401
  - app/views/iron/references/new.turbo_stream.erb
390
- - app/views/iron/schemas/new.html.erb
391
402
  - app/views/iron/sessions/new.html.erb
392
403
  - app/views/iron/settings/show.html.erb
393
404
  - app/views/iron/shared/_icon_picker.html.erb
@@ -424,6 +435,8 @@ files:
424
435
  - db/migrate/20250609091605_add_web_publishing_to_iron_content_types.rb
425
436
  - db/migrate/20250609091813_add_route_to_iron_entries.rb
426
437
  - db/migrate/20250908203158_add_instance_token_to_iron_accounts.rb
438
+ - db/migrate/20251209103109_create_iron_account_exports.rb
439
+ - db/migrate/20251209103110_create_iron_account_imports.rb
427
440
  - lib/generators/iron/pages/pages_generator.rb
428
441
  - lib/generators/iron/pages/templates/pages_controller.rb
429
442
  - lib/generators/iron/pages/templates/show.html.erb
@@ -441,7 +454,6 @@ files:
441
454
  - lib/iron/lexorank/utils.rb
442
455
  - lib/iron/routing.rb
443
456
  - lib/iron/sdk.rb
444
- - lib/iron/test_fixtures.rb
445
457
  - lib/iron/version.rb
446
458
  - lib/puma/plugin/iron_tailwindcss.rb
447
459
  - lib/tasks/iron_tailwindcss.rake
@@ -1,33 +0,0 @@
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
@@ -1,32 +0,0 @@
1
- module Iron
2
- class SchemasController < ApplicationController
3
- def export
4
- send_data SchemaExporter.export,
5
- filename: "iron-schema-#{Date.current}.tar",
6
- type: "application/x-tar",
7
- disposition: "attachment"
8
- end
9
-
10
- def new
11
- @import_modes = SchemaImporter::IMPORT_MODES
12
- end
13
-
14
- def import
15
- file = params[:schema_file]
16
- mode = params[:import_mode]
17
-
18
- unless file.present?
19
- redirect_to schema_path, alert: "Please select a file to import"
20
- return
21
- end
22
-
23
- result = SchemaImporter.import(file, mode:)
24
-
25
- if result.success?
26
- redirect_to content_types_path, notice: "Schema imported successfully"
27
- else
28
- redirect_to schema_path, alert: result.errors.join(". ")
29
- end
30
- end
31
- end
32
- end
@@ -1,28 +0,0 @@
1
- module Iron
2
- module CsvSerializable
3
- extend ActiveSupport::Concern
4
-
5
- class_methods do
6
- def to_csv
7
- CSV.generate do |csv|
8
- csv << csv_headers
9
- csv_scope.find_each do |record|
10
- csv << record.to_csv_row
11
- end
12
- end
13
- end
14
-
15
- def csv_headers
16
- raise NotImplementedError, "#{name} must implement csv_headers"
17
- end
18
-
19
- def csv_scope
20
- all
21
- end
22
- end
23
-
24
- def to_csv_row
25
- raise NotImplementedError, "#{self.class.name} must implement to_csv_row"
26
- end
27
- end
28
- end
@@ -1,69 +0,0 @@
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
@@ -1,20 +0,0 @@
1
- module Iron
2
- module BlockDefinition::Portable
3
- extend ActiveSupport::Concern
4
- include ::Iron::CsvSerializable
5
-
6
- class_methods do
7
- def csv_headers
8
- %w[handle name description]
9
- end
10
- end
11
-
12
- def to_csv_row
13
- [
14
- handle,
15
- name,
16
- description
17
- ]
18
- end
19
- end
20
- end
@@ -1,73 +0,0 @@
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
@@ -1,80 +0,0 @@
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
@@ -1,23 +0,0 @@
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