iron-cms 0.17.2 → 0.18.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 +88 -4
- data/app/assets/builds/iron.css +255 -106
- data/app/assets/tailwind/iron/application.css +1 -0
- data/app/assets/tailwind/iron/components/file-upload.css +26 -0
- data/app/assets/tailwind/iron/lexxy.css +111 -87
- data/app/controllers/concerns/iron/schema_editing.rb +19 -0
- data/app/controllers/iron/api/schema/base_controller.rb +25 -0
- data/app/controllers/iron/api/schema/block_definitions_controller.rb +49 -0
- data/app/controllers/iron/api/schema/content_types_controller.rb +64 -0
- data/app/controllers/iron/api/schema/field_definitions_controller.rb +88 -0
- data/app/controllers/iron/api/schema/locales_controller.rb +52 -0
- data/app/controllers/iron/application_controller.rb +1 -1
- data/app/controllers/iron/block_definitions_controller.rb +1 -0
- data/app/controllers/iron/content_types_controller.rb +1 -0
- data/app/controllers/iron/field_definitions_controller.rb +2 -1
- data/app/controllers/iron/first_runs_controller.rb +1 -1
- data/app/controllers/iron/locales_controller.rb +1 -0
- data/app/controllers/iron/sessions_controller.rb +1 -1
- data/app/helpers/iron/avatar_helper.rb +24 -2
- data/app/javascript/iron/controllers/file_upload_controller.js +38 -7
- data/app/models/iron/account.rb +1 -1
- data/app/models/iron/api/openapi_spec.rb +37 -15
- data/app/models/iron/block_definition/exportable.rb +1 -1
- data/app/models/iron/block_definition.rb +3 -1
- data/app/models/iron/content.rb +176 -0
- data/app/models/iron/content_type/exportable.rb +3 -1
- data/app/models/iron/content_type/importable.rb +1 -1
- data/app/models/iron/content_type.rb +6 -1
- data/app/models/iron/entry/content_assignable.rb +10 -2
- data/app/models/iron/exporter.rb +1 -26
- data/app/models/iron/field/length_constrained.rb +17 -0
- data/app/models/iron/field/validatable.rb +16 -0
- data/app/models/iron/field.rb +1 -1
- data/app/models/iron/field_definition/exportable.rb +3 -4
- data/app/models/iron/field_definition/importable.rb +16 -9
- data/app/models/iron/field_definition/ranked.rb +1 -1
- data/app/models/iron/field_definition/searchable.rb +2 -0
- data/app/models/iron/field_definition/validatable.rb +40 -0
- data/app/models/iron/field_definition/validations.rb +175 -0
- data/app/models/iron/field_definition.rb +1 -1
- data/app/models/iron/field_definitions/date.rb +2 -0
- data/app/models/iron/field_definitions/file.rb +3 -11
- data/app/models/iron/field_definitions/number.rb +2 -0
- data/app/models/iron/field_definitions/reference.rb +2 -0
- data/app/models/iron/field_definitions/rich_text_area.rb +1 -0
- data/app/models/iron/field_definitions/text_area.rb +2 -0
- data/app/models/iron/field_definitions/text_field.rb +1 -9
- data/app/models/iron/fields/block.rb +5 -1
- data/app/models/iron/fields/block_list.rb +5 -1
- data/app/models/iron/fields/date.rb +19 -2
- data/app/models/iron/fields/file.rb +5 -3
- data/app/models/iron/fields/number.rb +28 -1
- data/app/models/iron/fields/reference.rb +2 -0
- data/app/models/iron/fields/rich_text_area.rb +2 -0
- data/app/models/iron/fields/text_area.rb +4 -0
- data/app/models/iron/fields/text_field.rb +9 -5
- data/app/models/iron/importer.rb +1 -54
- data/app/models/iron/locale.rb +2 -0
- data/app/models/iron/schema/auto_dumpable.rb +26 -0
- data/app/models/iron/schema/diff.rb +194 -0
- data/app/models/iron/schema/validation.rb +214 -0
- data/app/models/iron/schema.rb +282 -0
- data/app/models/iron/seed.rb +5 -3
- data/app/models/iron/system.rb +7 -0
- data/app/models/iron/user/deactivatable.rb +5 -0
- data/app/models/iron/user.rb +18 -1
- data/app/views/iron/api/fields/_rich_text_area.json.jbuilder +2 -2
- data/app/views/iron/api/schema/block_definitions/_block_definition.json.jbuilder +4 -0
- data/app/views/iron/api/schema/block_definitions/index.json.jbuilder +1 -0
- data/app/views/iron/api/schema/block_definitions/show.json.jbuilder +1 -0
- data/app/views/iron/api/schema/content_types/_content_type.json.jbuilder +5 -0
- data/app/views/iron/api/schema/content_types/index.json.jbuilder +1 -0
- data/app/views/iron/api/schema/content_types/show.json.jbuilder +1 -0
- data/app/views/iron/api/schema/field_definitions/_field_definition.json.jbuilder +7 -0
- data/app/views/iron/api/schema/field_definitions/show.json.jbuilder +1 -0
- data/app/views/iron/api/schema/locales/_locale.json.jbuilder +4 -0
- data/app/views/iron/api/schema/locales/index.json.jbuilder +1 -0
- data/app/views/iron/api/schema/locales/show.json.jbuilder +1 -0
- data/app/views/iron/block_definitions/_empty_state.html.erb +5 -3
- data/app/views/iron/block_definitions/_form.html.erb +3 -1
- data/app/views/iron/block_definitions/edit.html.erb +19 -17
- data/app/views/iron/block_definitions/index.html.erb +7 -3
- data/app/views/iron/block_definitions/show.html.erb +14 -8
- data/app/views/iron/content_types/_content_type.html.erb +1 -1
- data/app/views/iron/content_types/_empty_state.html.erb +5 -3
- data/app/views/iron/content_types/_form.html.erb +12 -8
- data/app/views/iron/content_types/edit.html.erb +19 -17
- data/app/views/iron/content_types/index.html.erb +7 -3
- data/app/views/iron/content_types/show.html.erb +14 -8
- data/app/views/iron/entries/_empty_state.html.erb +1 -1
- data/app/views/iron/entries/edit.html.erb +1 -1
- data/app/views/iron/entries/entry.html.erb +1 -1
- data/app/views/iron/entries/fields/_block.html.erb +4 -0
- data/app/views/iron/entries/fields/_block_list.html.erb +2 -0
- data/app/views/iron/entries/fields/_boolean.html.erb +1 -0
- data/app/views/iron/entries/fields/_date.html.erb +3 -2
- data/app/views/iron/entries/fields/_field_errors.html.erb +7 -0
- data/app/views/iron/entries/fields/_field_label.html.erb +8 -0
- data/app/views/iron/entries/fields/_file.html.erb +11 -19
- data/app/views/iron/entries/fields/_number.html.erb +6 -2
- data/app/views/iron/entries/fields/_reference.html.erb +2 -1
- data/app/views/iron/entries/fields/_reference_list.html.erb +2 -0
- data/app/views/iron/entries/fields/_rich_text_area.html.erb +2 -1
- data/app/views/iron/entries/fields/_text_area.html.erb +6 -2
- data/app/views/iron/entries/fields/_text_field.html.erb +5 -12
- data/app/views/iron/field_definitions/_field_definition.html.erb +51 -29
- data/app/views/iron/field_definitions/date/_form.html.erb +5 -1
- data/app/views/iron/field_definitions/edit.html.erb +10 -8
- data/app/views/iron/field_definitions/file/_form.html.erb +6 -0
- data/app/views/iron/field_definitions/new.html.erb +5 -3
- data/app/views/iron/field_definitions/number/_form.html.erb +23 -1
- data/app/views/iron/field_definitions/reference/_form.html.erb +5 -0
- data/app/views/iron/field_definitions/rich_text_area/_form.html.erb +5 -0
- data/app/views/iron/field_definitions/text_area/_form.html.erb +23 -0
- data/app/views/iron/field_definitions/text_field/_form.html.erb +20 -1
- data/app/views/iron/home/_content_types.html.erb +1 -1
- data/app/views/iron/locales/_form.html.erb +5 -3
- data/app/views/iron/locales/edit.html.erb +1 -1
- data/app/views/iron/locales/index.html.erb +12 -6
- data/app/views/iron/shared/_schema_lock_badge.html.erb +19 -0
- data/config/locales/en.yml +13 -1
- data/config/locales/it.yml +18 -1
- data/config/routes.rb +11 -0
- data/db/migrate/20260612131538_create_iron_systems.rb +9 -0
- data/exe/iron +5 -0
- data/lib/generators/iron/agents/agents_generator.rb +52 -0
- data/lib/generators/iron/agents/templates/AGENTS.md +24 -0
- data/lib/generators/iron/agents/templates/SKILL.md +423 -0
- data/lib/generators/iron/install/install_generator.rb +118 -0
- data/lib/generators/iron/install/templates/iron_release.rake +5 -0
- data/lib/generators/iron/install/templates/schema.json +12 -0
- data/lib/generators/iron/install/templates/seeds.rb +13 -0
- data/lib/generators/iron/pages/pages_generator.rb +5 -0
- data/lib/generators/iron/pages/templates/pages_controller.rb +1 -1
- data/lib/generators/iron/pages/templates/show.html.erb +1 -1
- data/lib/install/template.rb +9 -0
- data/lib/iron/cli.rb +43 -0
- data/lib/iron/version.rb +1 -1
- data/lib/tasks/iron_content.rake +82 -0
- data/lib/tasks/iron_schema.rake +45 -0
- metadata +62 -3
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Iron
|
|
2
|
+
module FieldDefinition::Validatable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
class_attribute :supported_validations, instance_writer: false, default: [].freeze
|
|
7
|
+
|
|
8
|
+
validate :declared_validations_are_well_formed
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class_methods do
|
|
12
|
+
def supports_validations(*keys)
|
|
13
|
+
keys = keys.map(&:to_s)
|
|
14
|
+
self.supported_validations = keys.freeze
|
|
15
|
+
store_accessor :metadata, *keys
|
|
16
|
+
|
|
17
|
+
keys.each do |key|
|
|
18
|
+
kind = FieldDefinition::Validations::RULES.fetch(key).fetch(:kind)
|
|
19
|
+
define_method(key) { FieldDefinition::Validations.cast(kind, super()) }
|
|
20
|
+
define_method("#{key}=") { |value| super(FieldDefinition::Validations.cast(kind, value)) }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def required?
|
|
26
|
+
ActiveModel::Type::Boolean.new.cast(metadata&.dig("required")) || false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def metadata=(config)
|
|
30
|
+
super(FieldDefinition::Validations.cast_rules(config))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
def declared_validations_are_well_formed
|
|
35
|
+
FieldDefinition::Validations.violations(type_handle, metadata).each do |violation|
|
|
36
|
+
errors.add(violation.attribute, violation.message)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
module Iron
|
|
2
|
+
module FieldDefinition::Validations
|
|
3
|
+
Violation = Data.define(:attribute, :message)
|
|
4
|
+
|
|
5
|
+
RULES = {
|
|
6
|
+
"required" => { kind: :boolean },
|
|
7
|
+
"allowed_values" => { kind: :string_list },
|
|
8
|
+
"min_length" => { kind: :positive_integer, ceiling: "max_length" },
|
|
9
|
+
"max_length" => { kind: :positive_integer },
|
|
10
|
+
"min" => { kind: :number, ceiling: "max" },
|
|
11
|
+
"max" => { kind: :number },
|
|
12
|
+
"file_scope" => { kind: :file_scope },
|
|
13
|
+
"selected_presets" => { kind: :preset_list }
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def keys_for(type_handle)
|
|
18
|
+
FieldDefinition.classify_type(type_handle).constantize.supported_validations
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def violations(type_handle, metadata)
|
|
22
|
+
return [ Violation.new(attribute: :metadata, message: "must be an object") ] unless metadata.nil? || metadata.is_a?(Hash)
|
|
23
|
+
|
|
24
|
+
rules = (metadata || {}).transform_keys(&:to_s).slice(*RULES.keys).reject { |_key, value| absent?(value) }
|
|
25
|
+
return [] if rules.empty?
|
|
26
|
+
|
|
27
|
+
allowed = keys_for(type_handle)
|
|
28
|
+
return [ Violation.new(attribute: :metadata, message: "type #{type_handle} supports no validations") ] if allowed.empty?
|
|
29
|
+
|
|
30
|
+
rules.flat_map { |key, value| rule_violations(type_handle, allowed, key, value) } + bound_violations(rules, allowed)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def cast_rules(config)
|
|
34
|
+
return config unless config.is_a?(Hash)
|
|
35
|
+
|
|
36
|
+
config.each_with_object({}) do |(key, value), typed|
|
|
37
|
+
key = key.to_s
|
|
38
|
+
rule = RULES[key]
|
|
39
|
+
typed[key] = rule ? cast(rule.fetch(:kind), value) : value
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def cast(kind, value)
|
|
44
|
+
return nil if value.nil?
|
|
45
|
+
|
|
46
|
+
case kind
|
|
47
|
+
when :boolean then cast_boolean(value)
|
|
48
|
+
when :positive_integer then cast_positive_integer(value)
|
|
49
|
+
when :number then cast_number(value)
|
|
50
|
+
when :string_list then cast_string_list(value)
|
|
51
|
+
when :file_scope then cast_file_scope(value)
|
|
52
|
+
when :preset_list then cast_preset_list(value)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
# Blank rules read as absent: existing databases store allowed_values: [] for blank textareas.
|
|
58
|
+
def absent?(value)
|
|
59
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def rule_violations(type_handle, allowed, key, value)
|
|
63
|
+
unless allowed.include?(key)
|
|
64
|
+
return [ Violation.new(attribute: :metadata, message: %(unknown validation "#{key}" for #{type_handle} (valid: #{allowed.join(', ')}))) ]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
value_violations(key, value)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def value_violations(key, value)
|
|
71
|
+
case RULES.fetch(key).fetch(:kind)
|
|
72
|
+
when :boolean
|
|
73
|
+
[ Violation.new(attribute: key.to_sym, message: "must be true or false") ] unless value == true || value == false
|
|
74
|
+
when :positive_integer
|
|
75
|
+
[ Violation.new(attribute: key.to_sym, message: "must be a positive integer (omit the key instead of 0)") ] unless positive_integer?(value)
|
|
76
|
+
when :number
|
|
77
|
+
[ Violation.new(attribute: key.to_sym, message: "must be a number") ] unless value.is_a?(Numeric)
|
|
78
|
+
when :string_list
|
|
79
|
+
[ Violation.new(attribute: key.to_sym, message: "must be a non-empty array of distinct, non-blank strings") ] unless clean_string_list?(value)
|
|
80
|
+
when :file_scope
|
|
81
|
+
[ Violation.new(attribute: key.to_sym, message: %(must be "all" or "specific")) ] unless %w[all specific].include?(value)
|
|
82
|
+
when :preset_list
|
|
83
|
+
preset_list_violations(key, value)
|
|
84
|
+
end || []
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def preset_list_violations(key, value)
|
|
88
|
+
unless value.is_a?(Array) && value.all? { |item| item.is_a?(String) }
|
|
89
|
+
return [ Violation.new(attribute: key.to_sym, message: "must be an array of file type presets (#{preset_keys.join(', ')})") ]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
(value.uniq - preset_keys).map do |unknown|
|
|
93
|
+
Violation.new(attribute: key.to_sym, message: %(contains unknown file type "#{unknown}" (valid: #{preset_keys.join(', ')})))
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def bound_violations(rules, allowed)
|
|
98
|
+
RULES.filter_map do |floor_key, rule|
|
|
99
|
+
ceiling_key = rule[:ceiling]
|
|
100
|
+
next unless ceiling_key && allowed.include?(floor_key)
|
|
101
|
+
|
|
102
|
+
floor, ceiling = rules[floor_key], rules[ceiling_key]
|
|
103
|
+
next unless comparable?(rule.fetch(:kind), floor) && comparable?(rule.fetch(:kind), ceiling)
|
|
104
|
+
next unless floor > ceiling
|
|
105
|
+
|
|
106
|
+
Violation.new(attribute: floor_key.to_sym, message: "(#{floor}) cannot exceed #{ceiling_key} (#{ceiling})")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def comparable?(kind, value)
|
|
111
|
+
kind == :positive_integer ? positive_integer?(value) : value.is_a?(Numeric)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def positive_integer?(value)
|
|
115
|
+
value.is_a?(Integer) && value >= 1
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def clean_string_list?(value)
|
|
119
|
+
value.is_a?(Array) && value.any? &&
|
|
120
|
+
value.all? { |item| item.is_a?(String) && !item.empty? && item == item.strip } &&
|
|
121
|
+
value.uniq.size == value.size
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def cast_boolean(value)
|
|
125
|
+
case value.to_s
|
|
126
|
+
when "" then nil
|
|
127
|
+
when "0", "f", "F", "false", "FALSE", "False", "off", "OFF" then false
|
|
128
|
+
when "1", "t", "T", "true", "TRUE", "True", "on", "ON" then true
|
|
129
|
+
else value
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def cast_positive_integer(value)
|
|
134
|
+
case value
|
|
135
|
+
when Integer then value
|
|
136
|
+
when Float then value % 1 == 0 ? value.to_i : value
|
|
137
|
+
when String then value.strip.empty? ? nil : (Integer(value, exception: false) || value)
|
|
138
|
+
else value
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def cast_number(value)
|
|
143
|
+
case value
|
|
144
|
+
when Numeric then value.to_f
|
|
145
|
+
when String then value.strip.empty? ? nil : (Float(value, exception: false) || value)
|
|
146
|
+
else value
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def cast_string_list(value)
|
|
151
|
+
return value unless value.is_a?(Array) && value.all? { |item| item.is_a?(String) }
|
|
152
|
+
|
|
153
|
+
value.map(&:strip).reject(&:empty?).presence
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def cast_file_scope(value)
|
|
157
|
+
scope = value.is_a?(Symbol) ? value.to_s : value
|
|
158
|
+
scope.is_a?(String) && scope.strip.empty? ? nil : scope
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def cast_preset_list(value)
|
|
162
|
+
return value unless value.is_a?(Array) && value.all? { |item| item.is_a?(String) }
|
|
163
|
+
|
|
164
|
+
members = value.map(&:strip).reject(&:empty?)
|
|
165
|
+
return value unless (members - preset_keys).empty?
|
|
166
|
+
|
|
167
|
+
preset_keys & members
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def preset_keys
|
|
171
|
+
FieldDefinitions::File::FILE_TYPE_PRESETS.keys
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Iron
|
|
2
2
|
class FieldDefinition < ApplicationRecord
|
|
3
|
-
include Ranked, Searchable, Exportable, Importable
|
|
3
|
+
include Ranked, Searchable, Validatable, Exportable, Importable, Schema::AutoDumpable
|
|
4
4
|
|
|
5
5
|
TYPES = %w[text_field text_area rich_text_area number file boolean date block block_list reference_list reference].freeze
|
|
6
6
|
|
|
@@ -28,7 +28,7 @@ module Iron
|
|
|
28
28
|
}
|
|
29
29
|
}.freeze
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
supports_validations :required, :file_scope, :selected_presets
|
|
32
32
|
|
|
33
33
|
def file_scope
|
|
34
34
|
super || "all"
|
|
@@ -41,21 +41,13 @@ module Iron
|
|
|
41
41
|
def accepted_extensions
|
|
42
42
|
return nil if file_scope == "all"
|
|
43
43
|
|
|
44
|
-
extensions
|
|
45
|
-
selected_presets.each do |preset|
|
|
46
|
-
extensions.concat(FILE_TYPE_PRESETS[preset][:extensions]) if FILE_TYPE_PRESETS[preset]
|
|
47
|
-
end
|
|
48
|
-
extensions.uniq
|
|
44
|
+
selected_presets.flat_map { |preset| FILE_TYPE_PRESETS.dig(preset, :extensions) || [] }.uniq
|
|
49
45
|
end
|
|
50
46
|
|
|
51
47
|
def accepted_mime_types
|
|
52
48
|
return nil if file_scope == "all"
|
|
53
49
|
|
|
54
|
-
mime_types
|
|
55
|
-
selected_presets.each do |preset|
|
|
56
|
-
mime_types.concat(FILE_TYPE_PRESETS[preset][:mime_types]) if FILE_TYPE_PRESETS[preset]
|
|
57
|
-
end
|
|
58
|
-
mime_types.uniq
|
|
50
|
+
selected_presets.flat_map { |preset| FILE_TYPE_PRESETS.dig(preset, :mime_types) || [] }.uniq
|
|
59
51
|
end
|
|
60
52
|
|
|
61
53
|
def validation_description
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Iron
|
|
2
2
|
class FieldDefinitions::TextField < FieldDefinition
|
|
3
|
-
|
|
3
|
+
supports_validations :required, :allowed_values, :min_length, :max_length
|
|
4
4
|
|
|
5
5
|
def allowed_values_text
|
|
6
6
|
Array(allowed_values).join("\n")
|
|
@@ -10,14 +10,6 @@ module Iron
|
|
|
10
10
|
self.allowed_values = text.to_s.lines.map(&:strip).reject(&:blank?)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def required
|
|
14
|
-
ActiveModel::Type::Boolean.new.cast(super)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def required=(value)
|
|
18
|
-
super(ActiveModel::Type::Boolean.new.cast(value))
|
|
19
|
-
end
|
|
20
|
-
|
|
21
13
|
def value_column
|
|
22
14
|
:value_string
|
|
23
15
|
end
|
|
@@ -22,7 +22,11 @@ module Iron
|
|
|
22
22
|
|
|
23
23
|
block_definition.field_definitions.each do |nested_def|
|
|
24
24
|
raw = Field.content_fetch(value, nested_def.handle)
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
if raw == CONTENT_MISSING
|
|
27
|
+
fields.build(type: nested_def.field_type, entry: entry, definition: nested_def) if nested_def.required?
|
|
28
|
+
next
|
|
29
|
+
end
|
|
26
30
|
|
|
27
31
|
field = fields.build(type: nested_def.field_type, entry: entry, definition: nested_def)
|
|
28
32
|
field.content_value = raw
|
|
@@ -32,7 +32,11 @@ module Iron
|
|
|
32
32
|
|
|
33
33
|
block_def.field_definitions.each do |nested_def|
|
|
34
34
|
raw = Field.content_fetch(block_data, nested_def.handle)
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
if raw == CONTENT_MISSING
|
|
37
|
+
block.fields.build(type: nested_def.field_type, entry: entry, definition: nested_def) if nested_def.required?
|
|
38
|
+
next
|
|
39
|
+
end
|
|
36
40
|
|
|
37
41
|
field = block.fields.build(type: nested_def.field_type, entry: entry, definition: nested_def)
|
|
38
42
|
field.content_value = raw
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
module Iron
|
|
2
2
|
class Fields::Date < Field
|
|
3
|
+
TIME_DESIGNATOR = /[Tt ]/
|
|
4
|
+
|
|
3
5
|
def content_value=(value)
|
|
4
6
|
@content_errors = nil
|
|
5
7
|
if value.blank?
|
|
6
8
|
self.value_datetime = nil
|
|
7
9
|
else
|
|
8
|
-
self.value_datetime =
|
|
10
|
+
self.value_datetime = parse_iso8601(value.to_s)
|
|
9
11
|
end
|
|
10
12
|
rescue ArgumentError
|
|
11
|
-
add_content_error("must be a valid ISO8601 datetime")
|
|
13
|
+
add_content_error("must be a valid ISO8601 date or datetime")
|
|
12
14
|
end
|
|
13
15
|
|
|
16
|
+
def filled? = !value_datetime.nil?
|
|
17
|
+
|
|
14
18
|
def value
|
|
15
19
|
value_datetime
|
|
16
20
|
end
|
|
@@ -18,5 +22,18 @@ module Iron
|
|
|
18
22
|
def export_value
|
|
19
23
|
{ type: "date", value: value_datetime&.iso8601 }
|
|
20
24
|
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# A failed parse of a string with a time designator is a malformed
|
|
29
|
+
# datetime, not a date-only value — re-raise rather than letting the
|
|
30
|
+
# date fallback silently drop its time part.
|
|
31
|
+
def parse_iso8601(value)
|
|
32
|
+
Time.iso8601(value)
|
|
33
|
+
rescue ArgumentError
|
|
34
|
+
raise if value.match?(TIME_DESIGNATOR)
|
|
35
|
+
|
|
36
|
+
::Date.iso8601(value).in_time_zone
|
|
37
|
+
end
|
|
21
38
|
end
|
|
22
39
|
end
|
|
@@ -8,6 +8,8 @@ module Iron
|
|
|
8
8
|
@content_errors = nil
|
|
9
9
|
if value.nil?
|
|
10
10
|
self.file = nil
|
|
11
|
+
elsif !value.is_a?(String)
|
|
12
|
+
add_content_error("must be an upload token (signed id)")
|
|
11
13
|
elsif value.present?
|
|
12
14
|
self.file = ActiveStorage::Blob.find_signed!(value)
|
|
13
15
|
end
|
|
@@ -15,6 +17,8 @@ module Iron
|
|
|
15
17
|
add_content_error("has an invalid upload token")
|
|
16
18
|
end
|
|
17
19
|
|
|
20
|
+
def filled? = file.attached?
|
|
21
|
+
|
|
18
22
|
def value
|
|
19
23
|
file.attached? ? file : nil
|
|
20
24
|
end
|
|
@@ -42,9 +46,7 @@ module Iron
|
|
|
42
46
|
end
|
|
43
47
|
|
|
44
48
|
def definition_restricts_file_type?
|
|
45
|
-
file.attached? &&
|
|
46
|
-
definition.file_scope == "specific" &&
|
|
47
|
-
definition.accepted_mime_types.present?
|
|
49
|
+
file.attached? && definition.accepted_mime_types.present?
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
def file_type_matches_restrictions
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
module Iron
|
|
2
2
|
class Fields::Number < Field
|
|
3
|
+
validate :value_within_bounds
|
|
4
|
+
|
|
3
5
|
def content_value=(value)
|
|
4
|
-
|
|
6
|
+
@content_errors = nil
|
|
7
|
+
|
|
8
|
+
if value.nil? || value.is_a?(Numeric) || numeric_string?(value)
|
|
9
|
+
self.value_float = value
|
|
10
|
+
else
|
|
11
|
+
add_content_error("must be a number")
|
|
12
|
+
end
|
|
5
13
|
end
|
|
6
14
|
|
|
15
|
+
def filled? = !value_float.nil?
|
|
16
|
+
|
|
7
17
|
def value
|
|
8
18
|
value_float
|
|
9
19
|
end
|
|
@@ -11,5 +21,22 @@ module Iron
|
|
|
11
21
|
def export_value
|
|
12
22
|
{ type: "number", value: value_float }
|
|
13
23
|
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def value_within_bounds
|
|
28
|
+
return if value_float.nil?
|
|
29
|
+
|
|
30
|
+
errors.add(:base, :greater_than_or_equal_to, count: displayed_bound(definition.min)) if definition.min && value_float < definition.min
|
|
31
|
+
errors.add(:base, :less_than_or_equal_to, count: displayed_bound(definition.max)) if definition.max && value_float > definition.max
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def numeric_string?(value)
|
|
35
|
+
value.is_a?(String) && !Float(value, exception: false).nil?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def displayed_bound(bound)
|
|
39
|
+
bound % 1 == 0 ? bound.to_i : bound
|
|
40
|
+
end
|
|
14
41
|
end
|
|
15
42
|
end
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
module Iron
|
|
2
2
|
class Fields::TextField < Field
|
|
3
|
-
|
|
3
|
+
include LengthConstrained
|
|
4
|
+
|
|
5
|
+
validate :value_is_an_allowed_value
|
|
4
6
|
|
|
5
7
|
def content_value=(value)
|
|
6
8
|
self.value_string = value
|
|
7
9
|
end
|
|
8
10
|
|
|
11
|
+
def filled? = value_string.present?
|
|
12
|
+
|
|
9
13
|
def searchable_text
|
|
10
14
|
value
|
|
11
15
|
end
|
|
@@ -20,11 +24,11 @@ module Iron
|
|
|
20
24
|
|
|
21
25
|
private
|
|
22
26
|
|
|
23
|
-
def
|
|
24
|
-
return
|
|
27
|
+
def value_is_an_allowed_value
|
|
28
|
+
return if value_string.blank? || definition.allowed_values.blank?
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
errors.add(:base, :
|
|
30
|
+
unless definition.allowed_values.include?(value_string)
|
|
31
|
+
errors.add(:base, "must be one of: #{definition.allowed_values.join(', ')}")
|
|
28
32
|
end
|
|
29
33
|
end
|
|
30
34
|
end
|
data/app/models/iron/importer.rb
CHANGED
|
@@ -39,16 +39,7 @@ module Iron
|
|
|
39
39
|
def import_schema(zip)
|
|
40
40
|
return unless zip.find_entry("schema.json")
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
ActiveRecord::Base.transaction do
|
|
45
|
-
import_locales(schema[:locales] || [])
|
|
46
|
-
import_block_definitions(schema[:block_definitions] || [])
|
|
47
|
-
populate_block_field_definitions(schema[:block_definitions] || [])
|
|
48
|
-
import_content_types(schema[:content_types] || [])
|
|
49
|
-
resolve_content_type_references(schema[:content_types] || [])
|
|
50
|
-
restore_default_locale(schema[:default_locale])
|
|
51
|
-
end
|
|
42
|
+
Schema.import(read_schema(zip))
|
|
52
43
|
end
|
|
53
44
|
|
|
54
45
|
def import_content(zip, files_dir)
|
|
@@ -163,49 +154,5 @@ module Iron
|
|
|
163
154
|
def parse_json(content)
|
|
164
155
|
JSON.parse(content, symbolize_names: true)
|
|
165
156
|
end
|
|
166
|
-
|
|
167
|
-
def import_locales(locales)
|
|
168
|
-
locales.each do |attrs|
|
|
169
|
-
Locale.find_or_create_by!(code: attrs[:code]) do |locale|
|
|
170
|
-
locale.name = attrs[:name]
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def restore_default_locale(locale_code)
|
|
176
|
-
return unless locale_code
|
|
177
|
-
|
|
178
|
-
locale = Locale.find_by(code: locale_code)
|
|
179
|
-
Current.account.update!(default_locale: locale) if locale
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def import_block_definitions(definitions)
|
|
183
|
-
definitions.each { |attrs| BlockDefinition.import_from_attributes(attrs) }
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def populate_block_field_definitions(definitions)
|
|
187
|
-
definitions.each { |attrs| BlockDefinition.populate_field_definitions(attrs) }
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def import_content_types(definitions)
|
|
191
|
-
definitions.each { |attrs| ContentType.import_from_attributes(attrs) }
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def resolve_content_type_references(definitions)
|
|
195
|
-
definitions.each do |attrs|
|
|
196
|
-
content_type = ContentType.find_by(handle: attrs[:handle])
|
|
197
|
-
next unless content_type
|
|
198
|
-
|
|
199
|
-
if attrs[:title_field_handle].present?
|
|
200
|
-
field = content_type.field_definitions.find_by(handle: attrs[:title_field_handle])
|
|
201
|
-
content_type.update!(title_field_definition: field) if field
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
if attrs[:web_page_title_field_handle].present?
|
|
205
|
-
field = content_type.field_definitions.find_by(handle: attrs[:web_page_title_field_handle])
|
|
206
|
-
content_type.update!(web_page_title_field_definition: field) if field
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
157
|
end
|
|
211
158
|
end
|
data/app/models/iron/locale.rb
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Iron
|
|
2
|
+
module Schema::AutoDumpable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
after_save :note_iron_schema_change
|
|
7
|
+
after_destroy :note_iron_schema_change
|
|
8
|
+
after_commit :auto_dump_iron_schema
|
|
9
|
+
after_rollback :forget_iron_schema_change
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def note_iron_schema_change
|
|
15
|
+
Iron::Schema.note_change
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def auto_dump_iron_schema
|
|
19
|
+
Iron::Schema.auto_dump
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def forget_iron_schema_change
|
|
23
|
+
Iron::Schema.forget_change
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|