iron-cms 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +174 -0
- data/Rakefile +8 -0
- data/app/assets/builds/iron.css +2 -0
- data/app/assets/config/iron_manifest.js +1 -0
- data/app/assets/images/iron/icons/sprite.svg +9425 -0
- data/app/assets/tailwind/iron/actiontext.css +550 -0
- data/app/assets/tailwind/iron/application.css +66 -0
- data/app/assets/tailwind/iron/components/button.css +74 -0
- data/app/assets/tailwind/iron/components/dropdown.css +42 -0
- data/app/assets/tailwind/iron/components/fieldset.css +18 -0
- data/app/assets/tailwind/iron/components/form.css +14 -0
- data/app/assets/tailwind/iron/components/icon-picker.css +9 -0
- data/app/assets/tailwind/iron/components/input.css +44 -0
- data/app/assets/tailwind/iron/components/page.css +7 -0
- data/app/assets/tailwind/iron/components/select.css +36 -0
- data/app/assets/tailwind/iron/components/sidebar.css +21 -0
- data/app/assets/tailwind/iron/components/textarea.css +27 -0
- data/app/controllers/concerns/iron/authentication.rb +66 -0
- data/app/controllers/concerns/iron/authorization.rb +11 -0
- data/app/controllers/concerns/iron/locale_aware.rb +20 -0
- data/app/controllers/concerns/iron/web_page.rb +18 -0
- data/app/controllers/iron/application_controller.rb +7 -0
- data/app/controllers/iron/block_definitions/field_definitions_controller.rb +8 -0
- data/app/controllers/iron/block_definitions_controller.rb +52 -0
- data/app/controllers/iron/blocks_controller.rb +12 -0
- data/app/controllers/iron/content_types/field_definitions_controller.rb +8 -0
- data/app/controllers/iron/content_types_controller.rb +52 -0
- data/app/controllers/iron/entries_controller.rb +103 -0
- data/app/controllers/iron/field_definitions_controller.rb +55 -0
- data/app/controllers/iron/first_runs_controller.rb +29 -0
- data/app/controllers/iron/home_controller.rb +6 -0
- data/app/controllers/iron/icons_controller.rb +10 -0
- data/app/controllers/iron/locales_controller.rb +52 -0
- data/app/controllers/iron/passwords_controller.rb +36 -0
- data/app/controllers/iron/references_controller.rb +11 -0
- data/app/controllers/iron/schemas_controller.rb +32 -0
- data/app/controllers/iron/sessions_controller.rb +32 -0
- data/app/controllers/iron/settings_controller.rb +23 -0
- data/app/controllers/iron/users_controller.rb +66 -0
- data/app/helpers/iron/application_helper.rb +25 -0
- data/app/helpers/iron/avatar_helper.rb +35 -0
- data/app/helpers/iron/block_helper.rb +4 -0
- data/app/helpers/iron/buttons_helper.rb +14 -0
- data/app/helpers/iron/components/badge_helper.rb +70 -0
- data/app/helpers/iron/components/dropdown_helper.rb +161 -0
- data/app/helpers/iron/content_types_helper.rb +11 -0
- data/app/helpers/iron/entries_helper.rb +13 -0
- data/app/helpers/iron/field_definitions_helper.rb +5 -0
- data/app/helpers/iron/form_builder.rb +43 -0
- data/app/helpers/iron/icons_helper.rb +30 -0
- data/app/helpers/iron/image_helper.rb +50 -0
- data/app/javascript/iron/application.js +6 -0
- data/app/javascript/iron/controllers/application.js +9 -0
- data/app/javascript/iron/controllers/element_controller.js +11 -0
- data/app/javascript/iron/controllers/file_upload_controller.js +18 -0
- data/app/javascript/iron/controllers/form_controller.js +7 -0
- data/app/javascript/iron/controllers/handle_controller.js +27 -0
- data/app/javascript/iron/controllers/icon_picker_controller.js +48 -0
- data/app/javascript/iron/controllers/index.js +4 -0
- data/app/javascript/iron/controllers/sortable_list_controller.js +125 -0
- data/app/javascript/iron/controllers/toggle_controller.js +14 -0
- data/app/javascript/iron/controllers/trix_controller.js +19 -0
- data/app/javascript/iron/lib/lexorank.js +51 -0
- data/app/jobs/iron/application_job.rb +4 -0
- data/app/jobs/iron/generate_entry_routes_job.rb +15 -0
- data/app/mailers/iron/application_mailer.rb +6 -0
- data/app/mailers/passwords_mailer.rb +6 -0
- data/app/models/concerns/iron/csv_serializable.rb +28 -0
- data/app/models/iron/account/joinable.rb +16 -0
- data/app/models/iron/account.rb +13 -0
- data/app/models/iron/application_record.rb +5 -0
- data/app/models/iron/block_definition/portable.rb +20 -0
- data/app/models/iron/block_definition.rb +10 -0
- data/app/models/iron/content_type/field_queryable.rb +101 -0
- data/app/models/iron/content_type/portable.rb +30 -0
- data/app/models/iron/content_type/titlable.rb +13 -0
- data/app/models/iron/content_type/web_publishable.rb +47 -0
- data/app/models/iron/content_type.rb +39 -0
- data/app/models/iron/content_types/collection.rb +6 -0
- data/app/models/iron/content_types/single.rb +9 -0
- data/app/models/iron/current.rb +12 -0
- data/app/models/iron/entry/deep_validation.rb +32 -0
- data/app/models/iron/entry/presentable.rb +40 -0
- data/app/models/iron/entry/schemable.rb +16 -0
- data/app/models/iron/entry/titlable.rb +35 -0
- data/app/models/iron/entry/web_publishable.rb +85 -0
- data/app/models/iron/entry.rb +16 -0
- data/app/models/iron/field/belongs_to_entry.rb +29 -0
- data/app/models/iron/field.rb +20 -0
- data/app/models/iron/field_definition/portable.rb +36 -0
- data/app/models/iron/field_definition.rb +42 -0
- data/app/models/iron/field_definitions/block.rb +23 -0
- data/app/models/iron/field_definitions/block_list.rb +8 -0
- data/app/models/iron/field_definitions/boolean.rb +7 -0
- data/app/models/iron/field_definitions/date.rb +7 -0
- data/app/models/iron/field_definitions/file.rb +67 -0
- data/app/models/iron/field_definitions/number.rb +7 -0
- data/app/models/iron/field_definitions/reference.rb +8 -0
- data/app/models/iron/field_definitions/reference_list.rb +8 -0
- data/app/models/iron/field_definitions/rich_text_area.rb +4 -0
- data/app/models/iron/field_definitions/text_area.rb +7 -0
- data/app/models/iron/field_definitions/text_field.rb +25 -0
- data/app/models/iron/fields/block.rb +40 -0
- data/app/models/iron/fields/block_list.rb +16 -0
- data/app/models/iron/fields/boolean.rb +7 -0
- data/app/models/iron/fields/date.rb +7 -0
- data/app/models/iron/fields/file.rb +29 -0
- data/app/models/iron/fields/number.rb +7 -0
- data/app/models/iron/fields/reference.rb +14 -0
- data/app/models/iron/fields/reference_list.rb +17 -0
- data/app/models/iron/fields/rich_text_area.rb +9 -0
- data/app/models/iron/fields/text_area.rb +7 -0
- data/app/models/iron/fields/text_field.rb +19 -0
- data/app/models/iron/first_run.rb +13 -0
- data/app/models/iron/icon_catalog.rb +45 -0
- data/app/models/iron/locale.rb +30 -0
- data/app/models/iron/reference.rb +8 -0
- data/app/models/iron/schema_archive.rb +71 -0
- data/app/models/iron/schema_exporter.rb +15 -0
- data/app/models/iron/schema_importer.rb +278 -0
- data/app/models/iron/session.rb +5 -0
- data/app/models/iron/user/role.rb +21 -0
- data/app/models/iron/user.rb +37 -0
- data/app/views/active_storage/blobs/_blob.html.erb +14 -0
- data/app/views/iron/block_definitions/_block_definition.html.erb +21 -0
- data/app/views/iron/block_definitions/_form.html.erb +46 -0
- data/app/views/iron/block_definitions/edit.html.erb +8 -0
- data/app/views/iron/block_definitions/index.html.erb +18 -0
- data/app/views/iron/block_definitions/new.html.erb +8 -0
- data/app/views/iron/block_definitions/show.html.erb +34 -0
- data/app/views/iron/blocks/new.turbo_stream.erb +6 -0
- data/app/views/iron/content_types/_content_type.html.erb +21 -0
- data/app/views/iron/content_types/_form.html.erb +137 -0
- data/app/views/iron/content_types/edit.html.erb +8 -0
- data/app/views/iron/content_types/index.html.erb +28 -0
- data/app/views/iron/content_types/new.html.erb +8 -0
- data/app/views/iron/content_types/show.html.erb +49 -0
- data/app/views/iron/entries/_entry.html.erb +17 -0
- data/app/views/iron/entries/_entry_option.html.erb +19 -0
- data/app/views/iron/entries/_form.html.erb +63 -0
- data/app/views/iron/entries/edit.html.erb +52 -0
- data/app/views/iron/entries/entry.html.erb +21 -0
- data/app/views/iron/entries/fields/_block.html.erb +92 -0
- data/app/views/iron/entries/fields/_block_list.html.erb +45 -0
- data/app/views/iron/entries/fields/_boolean.html.erb +13 -0
- data/app/views/iron/entries/fields/_date.html.erb +11 -0
- data/app/views/iron/entries/fields/_file.html.erb +60 -0
- data/app/views/iron/entries/fields/_number.html.erb +11 -0
- data/app/views/iron/entries/fields/_reference.html.erb +12 -0
- data/app/views/iron/entries/fields/_reference_item.html.erb +28 -0
- data/app/views/iron/entries/fields/_reference_list.html.erb +44 -0
- data/app/views/iron/entries/fields/_rich_text_area.html.erb +13 -0
- data/app/views/iron/entries/fields/_text_area.html.erb +11 -0
- data/app/views/iron/entries/fields/_text_field.html.erb +28 -0
- data/app/views/iron/entries/index.html.erb +21 -0
- data/app/views/iron/entries/new.html.erb +8 -0
- data/app/views/iron/entries/search.html.erb +33 -0
- data/app/views/iron/field_definitions/_field_definition.html.erb +37 -0
- data/app/views/iron/field_definitions/block/_form.html.erb +16 -0
- data/app/views/iron/field_definitions/block_list/_form.html.erb +13 -0
- data/app/views/iron/field_definitions/boolean/_form.html.erb +2 -0
- data/app/views/iron/field_definitions/date/_form.html.erb +2 -0
- data/app/views/iron/field_definitions/edit.html.erb +16 -0
- data/app/views/iron/field_definitions/file/_form.html.erb +40 -0
- data/app/views/iron/field_definitions/index.html.erb +14 -0
- data/app/views/iron/field_definitions/layouts/_form.html.erb +47 -0
- data/app/views/iron/field_definitions/new.html.erb +22 -0
- data/app/views/iron/field_definitions/number/_form.html.erb +2 -0
- data/app/views/iron/field_definitions/reference/_form.html.erb +13 -0
- data/app/views/iron/field_definitions/reference_list/_form.html.erb +13 -0
- data/app/views/iron/field_definitions/rich_text_area/_form.html.erb +2 -0
- data/app/views/iron/field_definitions/text_area/_form.html.erb +2 -0
- data/app/views/iron/field_definitions/text_field/_form.html.erb +17 -0
- data/app/views/iron/first_runs/show.html.erb +44 -0
- data/app/views/iron/home/show.html.erb +2 -0
- data/app/views/iron/icons/index.html.erb +21 -0
- data/app/views/iron/locales/_form.html.erb +24 -0
- data/app/views/iron/locales/_locale.html.erb +15 -0
- data/app/views/iron/locales/edit.html.erb +8 -0
- data/app/views/iron/locales/index.html.erb +12 -0
- data/app/views/iron/locales/new.html.erb +8 -0
- data/app/views/iron/passwords/edit.html.erb +7 -0
- data/app/views/iron/passwords/new.html.erb +26 -0
- data/app/views/iron/passwords_mailer/reset.html.erb +4 -0
- data/app/views/iron/passwords_mailer/reset.text.erb +2 -0
- data/app/views/iron/published_pages/show.html.erb +10 -0
- data/app/views/iron/references/new.turbo_stream.erb +6 -0
- data/app/views/iron/schemas/new.html.erb +57 -0
- data/app/views/iron/sessions/new.html.erb +36 -0
- data/app/views/iron/settings/show.html.erb +38 -0
- data/app/views/iron/shared/_icon_picker.html.erb +60 -0
- data/app/views/iron/users/_form.html.erb +21 -0
- data/app/views/iron/users/_user.html.erb +27 -0
- data/app/views/iron/users/edit.html.erb +12 -0
- data/app/views/iron/users/index.html.erb +21 -0
- data/app/views/iron/users/new.html.erb +29 -0
- data/app/views/iron/users/show.html.erb +21 -0
- data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
- data/app/views/layouts/iron/_sidebar.html.erb +83 -0
- data/app/views/layouts/iron/_sidebar_item.html.erb +14 -0
- data/app/views/layouts/iron/_toast.html.erb +29 -0
- data/app/views/layouts/iron/application.html.erb +46 -0
- data/app/views/layouts/iron/authentication.html.erb +41 -0
- data/config/importmap.rb +9 -0
- data/config/routes.rb +40 -0
- data/db/migrate/20250422131656_initial_iron_schema.rb +105 -0
- data/db/migrate/20250427100754_create_iron_accounts.rb +10 -0
- data/db/migrate/20250427104020_add_role_to_iron_users.rb +5 -0
- data/db/migrate/20250428094923_add_default_iron_locale_to_iron_account.rb +5 -0
- data/db/migrate/20250504144731_create_join_table_content_types_field_definitions.rb +25 -0
- data/db/migrate/20250505090716_add_title_field_definition_to_iron_content_types.rb +5 -0
- data/db/migrate/20250512210352_create_iron_references.rb +11 -0
- data/db/migrate/20250524220355_add_referenced_entry_id_to_iron_fields.rb +5 -0
- data/db/migrate/20250529211703_add_metadata_to_iron_field_definitions.rb +5 -0
- data/db/migrate/20250529211704_add_value_boolean_to_iron_fields.rb +5 -0
- data/db/migrate/20250531172837_add_type_to_iron_content_types.rb +5 -0
- data/db/migrate/20250601080146_add_icon_to_iron_content_types.rb +5 -0
- data/db/migrate/20250609091605_add_web_publishing_to_iron_content_types.rb +7 -0
- data/db/migrate/20250609091813_add_route_to_iron_entries.rb +6 -0
- data/lib/generators/iron/pages/pages_generator.rb +34 -0
- data/lib/generators/iron/pages/templates/pages_controller.rb +14 -0
- data/lib/generators/iron/pages/templates/show.html.erb +9 -0
- data/lib/generators/iron/template/template_generator.rb +35 -0
- data/lib/generators/iron/template/templates/content_type.html.erb +5 -0
- data/lib/iron/cva.rb +69 -0
- data/lib/iron/engine.rb +42 -0
- data/lib/iron/image_analyzer.rb +26 -0
- data/lib/iron/lexorank/exceptions.rb +4 -0
- data/lib/iron/lexorank/rankable.rb +185 -0
- data/lib/iron/lexorank/rebalance_rank_job.rb +10 -0
- data/lib/iron/lexorank/utils.rb +77 -0
- data/lib/iron/lexorank.rb +7 -0
- data/lib/iron/routing.rb +66 -0
- data/lib/iron/sdk.rb +19 -0
- data/lib/iron/version.rb +3 -0
- data/lib/iron.rb +8 -0
- data/lib/puma/plugin/iron_tailwindcss.rb +78 -0
- metadata +458 -0
@@ -0,0 +1,101 @@
|
|
1
|
+
module Iron
|
2
|
+
module ContentType::FieldQueryable
|
3
|
+
def order_by_field(field_handle, direction = :asc)
|
4
|
+
content_type = proxy_association.owner
|
5
|
+
definition = content_type.field_definitions.find_by!(handle: field_handle)
|
6
|
+
|
7
|
+
table_alias = "order_field_for_#{field_handle}"
|
8
|
+
scope = joins("LEFT OUTER JOIN iron_fields AS #{table_alias} ON #{table_alias}.entry_id = iron_entries.id AND #{table_alias}.definition_id = #{definition.id}")
|
9
|
+
scope.order("#{table_alias}.#{definition.value_column}": direction)
|
10
|
+
end
|
11
|
+
|
12
|
+
def where_fields(conditions)
|
13
|
+
content_type = proxy_association.owner
|
14
|
+
scope = self
|
15
|
+
conditions.each do |handle, condition|
|
16
|
+
definition = content_type.field_definitions.find_by! handle: handle
|
17
|
+
field_scope = joins(:fields).where(iron_fields: { definition: })
|
18
|
+
field_scope = case condition
|
19
|
+
when Hash
|
20
|
+
apply_field_conditions(field_scope, definition.value_column, condition)
|
21
|
+
when Range
|
22
|
+
field_scope.where("iron_fields.#{definition.value_column} BETWEEN ? AND ?", condition)
|
23
|
+
when Array
|
24
|
+
field_scope.where("iron_fields.#{definition.value_column} IN (?)", condition)
|
25
|
+
else
|
26
|
+
field_scope.where("iron_fields.#{definition.value_column} = ?", condition)
|
27
|
+
end
|
28
|
+
|
29
|
+
scope = scope.merge(field_scope)
|
30
|
+
end
|
31
|
+
|
32
|
+
scope.distinct
|
33
|
+
end
|
34
|
+
|
35
|
+
def find_by_fields(conditions)
|
36
|
+
where_fields(conditions).first
|
37
|
+
end
|
38
|
+
|
39
|
+
def find_by_fields!(conditions)
|
40
|
+
record = find_by_fields(conditions)
|
41
|
+
raise ActiveRecord::RecordNotFound unless record.present?
|
42
|
+
record
|
43
|
+
end
|
44
|
+
|
45
|
+
def pluck_field(field_handle, distinct: false)
|
46
|
+
content_type = proxy_association.owner
|
47
|
+
definition = content_type.field_definitions.find_by! handle: field_handle
|
48
|
+
|
49
|
+
scope = definition.fields
|
50
|
+
scope = scope.distinct if distinct
|
51
|
+
scope.pluck(definition.value_column)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def apply_field_conditions(scope, column, conditions)
|
57
|
+
conditions.each do |op, value|
|
58
|
+
scope = case op.to_sym
|
59
|
+
when :eq
|
60
|
+
scope.where("iron_fields.#{column} = ?", value)
|
61
|
+
when :ne, :not
|
62
|
+
scope.where("iron_fields.#{column} != ?", value)
|
63
|
+
when :gt
|
64
|
+
scope.where("iron_fields.#{column} > ?", value)
|
65
|
+
when :gte
|
66
|
+
scope.where("iron_fields.#{column} >= ?", value)
|
67
|
+
when :lt
|
68
|
+
scope.where("iron_fields.#{column} < ?", value)
|
69
|
+
when :lte
|
70
|
+
scope.where("iron_fields.#{column} <= ?", value)
|
71
|
+
when :like
|
72
|
+
scope.where("iron_fields.#{column} LIKE ?", "%#{value}%")
|
73
|
+
when :not_like
|
74
|
+
scope.where("iron_fields.#{column} NOT LIKE ?", "%#{value}%")
|
75
|
+
when :starts_with
|
76
|
+
scope.where("iron_fields.#{column} LIKE ?", "#{value}%")
|
77
|
+
when :ends_with
|
78
|
+
scope.where("iron_fields.#{column} LIKE ?", "%#{value}")
|
79
|
+
when :in
|
80
|
+
scope.where("iron_fields.#{column} IN (?)", value)
|
81
|
+
when :not_in
|
82
|
+
scope.where("iron_fields.#{column} NOT IN (?)", value)
|
83
|
+
when :blank
|
84
|
+
if value
|
85
|
+
scope.where("iron_fields.#{column} IS NULL OR iron_fields.#{column} = ''")
|
86
|
+
else
|
87
|
+
scope.where("iron_fields.#{column} IS NOT NULL AND iron_fields.#{column} != ''")
|
88
|
+
end
|
89
|
+
when :present
|
90
|
+
if value
|
91
|
+
scope.where("iron_fields.#{column} IS NOT NULL AND iron_fields.#{column} != ''")
|
92
|
+
else
|
93
|
+
scope.where("iron_fields.#{column} IS NULL OR iron_fields.#{column} = ''")
|
94
|
+
end
|
95
|
+
else
|
96
|
+
raise ArgumentError, "Unknown operator: #{op}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Iron
|
2
|
+
module ContentType::Portable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
include ::Iron::CsvSerializable
|
5
|
+
|
6
|
+
class_methods do
|
7
|
+
def csv_headers
|
8
|
+
%w[handle name type description icon title_field_handle web_publishing_enabled web_page_title_field_handle base_path]
|
9
|
+
end
|
10
|
+
|
11
|
+
def csv_scope
|
12
|
+
includes(:title_field_definition, :web_page_title_field_definition)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_csv_row
|
17
|
+
[
|
18
|
+
handle,
|
19
|
+
name,
|
20
|
+
type.demodulize.underscore,
|
21
|
+
description,
|
22
|
+
icon,
|
23
|
+
title_field_definition&.handle,
|
24
|
+
web_publishing_enabled,
|
25
|
+
web_page_title_field_definition&.handle,
|
26
|
+
base_path
|
27
|
+
]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Iron
|
2
|
+
module ContentType::Titlable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
belongs_to :title_field_definition, class_name: "Iron::FieldDefinition", optional: true
|
7
|
+
end
|
8
|
+
|
9
|
+
def titlable_definitions
|
10
|
+
field_definitions.where(type: [ Iron::FieldDefinitions::TextField ])
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Iron
|
2
|
+
module ContentType::WebPublishable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
belongs_to :web_page_title_field_definition, class_name: "FieldDefinition", optional: true
|
7
|
+
|
8
|
+
validates :base_path, format: { with: /\A[a-z0-9\-\/]*\z/, message: "can only contain lowercase letters, numbers, hyphens and slashes" }, allow_blank: true
|
9
|
+
|
10
|
+
scope :web_published, -> { where(web_publishing_enabled: true) }
|
11
|
+
|
12
|
+
after_save :generate_entries_routes, if: -> { saved_change_to_web_publishing_enabled? && web_publishing_enabled? }
|
13
|
+
after_save :reload_routes!, if: :should_reload_routes?
|
14
|
+
after_destroy :reload_routes!, if: :web_publishing_enabled?
|
15
|
+
end
|
16
|
+
|
17
|
+
def path_for(entry)
|
18
|
+
return nil unless web_publishing_enabled?
|
19
|
+
|
20
|
+
path_parts = []
|
21
|
+
path_parts << base_path if base_path.present?
|
22
|
+
path_parts << entry.route
|
23
|
+
|
24
|
+
"/" + path_parts.compact.join("/")
|
25
|
+
end
|
26
|
+
|
27
|
+
def index_entry
|
28
|
+
entries.find_by(route: "")
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def generate_entries_routes
|
34
|
+
GenerateEntryRoutesJob.perform_later(self)
|
35
|
+
end
|
36
|
+
|
37
|
+
def should_reload_routes?
|
38
|
+
saved_change_to_web_publishing_enabled? ||
|
39
|
+
saved_change_to_base_path? ||
|
40
|
+
(web_publishing_enabled? && saved_change_to_handle?)
|
41
|
+
end
|
42
|
+
|
43
|
+
def reload_routes!
|
44
|
+
Rails.application.reload_routes!
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Iron
|
2
|
+
class ContentType < ApplicationRecord
|
3
|
+
include Titlable, WebPublishable, Portable
|
4
|
+
|
5
|
+
TYPES = %w[Single Collection].freeze
|
6
|
+
|
7
|
+
validates :name, presence: true
|
8
|
+
validates :handle, presence: true
|
9
|
+
|
10
|
+
has_many :field_definitions, -> { ranked }, as: :schemable, dependent: :destroy
|
11
|
+
|
12
|
+
has_many :entries, -> { extending FieldQueryable }, dependent: :destroy
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def classify_type(type)
|
16
|
+
"Iron::ContentTypes::#{type}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def inherited(subclass)
|
20
|
+
super
|
21
|
+
def subclass.model_name
|
22
|
+
superclass.model_name
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def humanized_type
|
28
|
+
type.demodulize
|
29
|
+
end
|
30
|
+
|
31
|
+
def single?
|
32
|
+
is_a?(ContentTypes::Single)
|
33
|
+
end
|
34
|
+
|
35
|
+
def collection?
|
36
|
+
is_a?(ContentTypes::Collection)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# ActiveStorage file attachments in deeply nested associations don't properly prevent parent records
|
2
|
+
# from saving when validation fails. This is a Rails quirk where attachment validations don't bubble
|
3
|
+
# up through nested attributes. This concern recursively checks all nested fields for validation
|
4
|
+
# errors and prevents the Entry from saving if any file validations fail.
|
5
|
+
|
6
|
+
module Iron
|
7
|
+
class Entry
|
8
|
+
module DeepValidation
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
validate :all_fields_are_valid
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def all_fields_are_valid
|
18
|
+
any_invalid = false
|
19
|
+
walk = lambda do |field|
|
20
|
+
any_invalid ||= field.invalid?
|
21
|
+
if field.is_a?(Iron::Fields::BlockList)
|
22
|
+
field.blocks.each { |b| b.fields.each { |f| walk.call(f) } }
|
23
|
+
elsif field.is_a?(Iron::Fields::Block)
|
24
|
+
field.fields.each { |f| walk.call(f) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
fields.select { |f| f.parent.nil? }.each { |f| walk.call(f) }
|
28
|
+
errors.add(:base, "One or more fields are invalid") if any_invalid
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Iron
|
2
|
+
module Entry::Presentable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
def attributes
|
6
|
+
locale = Iron::Current.locale || Iron::Locale.default
|
7
|
+
|
8
|
+
Rails.cache.fetch("#{cache_key_with_version}/#{locale.code}/attributes") do
|
9
|
+
result = OpenStruct.new(id: id)
|
10
|
+
|
11
|
+
result._metadata = OpenStruct.new(
|
12
|
+
locale: locale,
|
13
|
+
web: OpenStruct.new(
|
14
|
+
title: web_title,
|
15
|
+
path: path
|
16
|
+
)
|
17
|
+
)
|
18
|
+
|
19
|
+
content_type.field_definitions.each do |field_definition|
|
20
|
+
field = find_field_for_presentation(field_definition, locale)
|
21
|
+
result[field_definition.handle] = field&.value
|
22
|
+
end
|
23
|
+
|
24
|
+
result
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
|
31
|
+
def find_field_for_presentation(field_definition, locale)
|
32
|
+
fields.find do |f|
|
33
|
+
f.definition_id == field_definition.id &&
|
34
|
+
f.definition_type == field_definition.class.base_class.to_s &&
|
35
|
+
f.locale_id == locale.id &&
|
36
|
+
f.parent.nil?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Iron
|
2
|
+
module Entry::Schemable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
belongs_to :content_type
|
7
|
+
|
8
|
+
scope :with_content_type, ->(types) {
|
9
|
+
ids = Array(types).map do |ct|
|
10
|
+
ct.is_a?(Iron::ContentType) ? ct.id : ct
|
11
|
+
end
|
12
|
+
where(content_type_id: ids)
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Iron
|
2
|
+
module Entry::Titlable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
scope :search_by_title, ->(query, locale = nil) {
|
7
|
+
locale ||= Iron::Locale.default
|
8
|
+
|
9
|
+
joins(:fields)
|
10
|
+
.joins(:content_type)
|
11
|
+
.where("iron_content_types.title_field_definition_id = iron_fields.definition_id")
|
12
|
+
.where("iron_fields.locale_id = ?", locale.id)
|
13
|
+
.where("iron_fields.value_string LIKE ?", "%#{query}%")
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def title
|
18
|
+
return default_title if title_field.blank?
|
19
|
+
|
20
|
+
title_field.value.presence || default_title
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def title_field
|
26
|
+
return nil unless content_type.title_field_definition.present?
|
27
|
+
|
28
|
+
@title_field ||= fields.find_by(definition: content_type.title_field_definition)
|
29
|
+
end
|
30
|
+
|
31
|
+
def default_title
|
32
|
+
"#{content_type.name} #{id}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Iron
|
2
|
+
module Entry::WebPublishable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
before_validation :generate_route, if: :should_generate_route?
|
7
|
+
before_validation :normalize_route
|
8
|
+
|
9
|
+
validates :route, uniqueness: { scope: :content_type_id, allow_blank: true }, if: :route_required?
|
10
|
+
validates :route, format: { with: /\A[a-z0-9\-\/]*\z/, message: "can only contain lowercase letters, numbers, hyphens and slashes" }, allow_blank: true
|
11
|
+
validate :index_route_uniqueness, if: :use_as_index
|
12
|
+
end
|
13
|
+
|
14
|
+
def use_as_index=(value)
|
15
|
+
if ActiveModel::Type::Boolean.new.cast(value)
|
16
|
+
self.route = ""
|
17
|
+
elsif route.blank?
|
18
|
+
self.route = nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def use_as_index
|
23
|
+
route == ""
|
24
|
+
end
|
25
|
+
|
26
|
+
def web_title
|
27
|
+
return nil unless content_type.web_page_title_field_definition
|
28
|
+
|
29
|
+
title_field = fields.find_by(
|
30
|
+
definition: content_type.web_page_title_field_definition,
|
31
|
+
locale: Iron::Locale.default
|
32
|
+
) || fields.detect { |f|
|
33
|
+
f.definition_id == content_type.web_page_title_field_definition_id &&
|
34
|
+
f.locale == Iron::Locale.default
|
35
|
+
}
|
36
|
+
|
37
|
+
title_field&.value
|
38
|
+
end
|
39
|
+
|
40
|
+
def path
|
41
|
+
content_type.path_for(self) if content_type.web_publishing_enabled?
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def should_generate_route?
|
47
|
+
content_type.web_publishing_enabled? && route.nil?
|
48
|
+
end
|
49
|
+
|
50
|
+
def route_required?
|
51
|
+
content_type.web_publishing_enabled?
|
52
|
+
end
|
53
|
+
|
54
|
+
def generate_route
|
55
|
+
return unless web_title.present?
|
56
|
+
|
57
|
+
base_handle = web_title.parameterize
|
58
|
+
handle = base_handle
|
59
|
+
counter = 1
|
60
|
+
|
61
|
+
while Entry.where(content_type: content_type, route: handle).where.not(id: id).exists?
|
62
|
+
handle = "#{base_handle}-#{counter}"
|
63
|
+
counter += 1
|
64
|
+
end
|
65
|
+
|
66
|
+
self.route = handle
|
67
|
+
end
|
68
|
+
|
69
|
+
def normalize_route
|
70
|
+
return if route.blank?
|
71
|
+
|
72
|
+
self.route = route.downcase
|
73
|
+
.gsub(/[^a-z0-9\-\/]+/, "-") # Replace non-allowed chars with hyphens
|
74
|
+
.gsub(/\-+/, "-") # Replace multiple hyphens with single hyphen
|
75
|
+
.gsub(/\A[\-\/]+|[\-\/]+\z/, "") # Remove leading/trailing hyphens and slashes
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def index_route_uniqueness
|
80
|
+
if content_type.index_entry && content_type.index_entry.id != id
|
81
|
+
errors.add(:use_as_index, "is already taken - another entry is already set as the index page")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Iron
|
2
|
+
class Entry < ApplicationRecord
|
3
|
+
include Titlable, Schemable, WebPublishable, Presentable, DeepValidation
|
4
|
+
|
5
|
+
belongs_to :creator, class_name: "Iron::User", default: -> { Current.user }
|
6
|
+
has_many :fields, inverse_of: :entry, dependent: :destroy
|
7
|
+
|
8
|
+
accepts_nested_attributes_for :fields, allow_destroy: true
|
9
|
+
|
10
|
+
def find_or_build_field(definition, locale)
|
11
|
+
field = fields.find { |f| f.definition == definition && f.locale == locale } ||
|
12
|
+
fields.build(type: definition.field_type, definition:, locale:)
|
13
|
+
field
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Iron
|
2
|
+
module Field::BelongsToEntry
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
after_save :ensure_entry
|
7
|
+
|
8
|
+
belongs_to :entry, inverse_of: :fields, optional: true, touch: true
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def ensure_entry
|
14
|
+
return if entry_id.present?
|
15
|
+
|
16
|
+
current_parent = parent
|
17
|
+
|
18
|
+
while current_parent.present?
|
19
|
+
if current_parent.entry_id.present?
|
20
|
+
update_column(:entry_id, current_parent.entry_id)
|
21
|
+
break
|
22
|
+
end
|
23
|
+
current_parent = current_parent.parent
|
24
|
+
end
|
25
|
+
|
26
|
+
raise "Field must be bound to an entry" unless entry_id.present?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Iron
|
2
|
+
class Field < ApplicationRecord
|
3
|
+
include BelongsToEntry
|
4
|
+
|
5
|
+
TYPES = %w[text_field text_area rich_text_area number file boolean date block block_list reference_list reference].freeze
|
6
|
+
|
7
|
+
belongs_to :definition, polymorphic: true
|
8
|
+
belongs_to :locale
|
9
|
+
belongs_to :parent, polymorphic: true, optional: true, touch: true
|
10
|
+
|
11
|
+
|
12
|
+
def type_handle
|
13
|
+
type.demodulize.underscore
|
14
|
+
end
|
15
|
+
|
16
|
+
def value
|
17
|
+
raise "Field type '#{type}' value method not supported"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Iron
|
2
|
+
module FieldDefinition::Portable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
include ::Iron::CsvSerializable
|
5
|
+
|
6
|
+
class_methods do
|
7
|
+
def csv_headers
|
8
|
+
%w[parent_type parent_handle handle name type rank metadata supported_block_definitions]
|
9
|
+
end
|
10
|
+
|
11
|
+
def csv_scope
|
12
|
+
includes(:schemable)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_csv_row
|
17
|
+
[
|
18
|
+
schemable_type == "Iron::ContentType" ? "content_type" : "block",
|
19
|
+
schemable.handle,
|
20
|
+
handle,
|
21
|
+
name,
|
22
|
+
type.demodulize.underscore,
|
23
|
+
rank,
|
24
|
+
metadata.to_json,
|
25
|
+
export_supported_block_definitions
|
26
|
+
]
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def export_supported_block_definitions
|
32
|
+
return nil unless respond_to?(:supported_block_definitions)
|
33
|
+
supported_block_definitions.pluck(:handle).join("|")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Iron
|
2
|
+
class FieldDefinition < ApplicationRecord
|
3
|
+
include Portable
|
4
|
+
|
5
|
+
TYPES = %w[text_field text_area rich_text_area number file boolean date block block_list reference_list reference].freeze
|
6
|
+
|
7
|
+
belongs_to :schemable, polymorphic: true
|
8
|
+
|
9
|
+
has_rank scoped_by: :schemable
|
10
|
+
before_create :move_to_bottom
|
11
|
+
|
12
|
+
has_many :fields, as: :definition, inverse_of: :definition, dependent: :destroy
|
13
|
+
|
14
|
+
has_one :titlable_content_type, class_name: "Iron::ContentType", inverse_of: :title_field_definition, dependent: :nullify
|
15
|
+
has_one :web_publishable_content_type, class_name: "Iron::ContentType", foreign_key: :web_page_title_field_definition_id, inverse_of: :web_page_title_field_definition, dependent: :nullify
|
16
|
+
|
17
|
+
validates :name, presence: true
|
18
|
+
validates :handle, presence: true
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def classify_type(type)
|
22
|
+
"Iron::FieldDefinitions::#{type.classify}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def humanized_type
|
27
|
+
type.demodulize.humanize
|
28
|
+
end
|
29
|
+
|
30
|
+
def type_handle
|
31
|
+
type.demodulize.underscore
|
32
|
+
end
|
33
|
+
|
34
|
+
def field_type
|
35
|
+
"Iron::Fields::#{type.demodulize}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def value_column
|
39
|
+
raise "value_column not implemented by '#{type}'"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Iron
|
2
|
+
class FieldDefinitions::Block < FieldDefinition
|
3
|
+
has_and_belongs_to_many :supported_block_definitions,
|
4
|
+
class_name: "::Iron::BlockDefinition",
|
5
|
+
foreign_key: "field_definition_id",
|
6
|
+
association_foreign_key: "block_definition_id"
|
7
|
+
|
8
|
+
validate :validate_exactly_one_supported_block_definition
|
9
|
+
|
10
|
+
def supported_block_definition
|
11
|
+
supported_block_definitions.first
|
12
|
+
end
|
13
|
+
|
14
|
+
def supported_block_definition_id
|
15
|
+
supported_block_definition&.id
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def validate_exactly_one_supported_block_definition
|
20
|
+
errors.add(:supported_block_definitions, "must have exactly one supported block definition") unless supported_block_definitions.size == 1
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|