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.
Files changed (240) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +174 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/builds/iron.css +2 -0
  6. data/app/assets/config/iron_manifest.js +1 -0
  7. data/app/assets/images/iron/icons/sprite.svg +9425 -0
  8. data/app/assets/tailwind/iron/actiontext.css +550 -0
  9. data/app/assets/tailwind/iron/application.css +66 -0
  10. data/app/assets/tailwind/iron/components/button.css +74 -0
  11. data/app/assets/tailwind/iron/components/dropdown.css +42 -0
  12. data/app/assets/tailwind/iron/components/fieldset.css +18 -0
  13. data/app/assets/tailwind/iron/components/form.css +14 -0
  14. data/app/assets/tailwind/iron/components/icon-picker.css +9 -0
  15. data/app/assets/tailwind/iron/components/input.css +44 -0
  16. data/app/assets/tailwind/iron/components/page.css +7 -0
  17. data/app/assets/tailwind/iron/components/select.css +36 -0
  18. data/app/assets/tailwind/iron/components/sidebar.css +21 -0
  19. data/app/assets/tailwind/iron/components/textarea.css +27 -0
  20. data/app/controllers/concerns/iron/authentication.rb +66 -0
  21. data/app/controllers/concerns/iron/authorization.rb +11 -0
  22. data/app/controllers/concerns/iron/locale_aware.rb +20 -0
  23. data/app/controllers/concerns/iron/web_page.rb +18 -0
  24. data/app/controllers/iron/application_controller.rb +7 -0
  25. data/app/controllers/iron/block_definitions/field_definitions_controller.rb +8 -0
  26. data/app/controllers/iron/block_definitions_controller.rb +52 -0
  27. data/app/controllers/iron/blocks_controller.rb +12 -0
  28. data/app/controllers/iron/content_types/field_definitions_controller.rb +8 -0
  29. data/app/controllers/iron/content_types_controller.rb +52 -0
  30. data/app/controllers/iron/entries_controller.rb +103 -0
  31. data/app/controllers/iron/field_definitions_controller.rb +55 -0
  32. data/app/controllers/iron/first_runs_controller.rb +29 -0
  33. data/app/controllers/iron/home_controller.rb +6 -0
  34. data/app/controllers/iron/icons_controller.rb +10 -0
  35. data/app/controllers/iron/locales_controller.rb +52 -0
  36. data/app/controllers/iron/passwords_controller.rb +36 -0
  37. data/app/controllers/iron/references_controller.rb +11 -0
  38. data/app/controllers/iron/schemas_controller.rb +32 -0
  39. data/app/controllers/iron/sessions_controller.rb +32 -0
  40. data/app/controllers/iron/settings_controller.rb +23 -0
  41. data/app/controllers/iron/users_controller.rb +66 -0
  42. data/app/helpers/iron/application_helper.rb +25 -0
  43. data/app/helpers/iron/avatar_helper.rb +35 -0
  44. data/app/helpers/iron/block_helper.rb +4 -0
  45. data/app/helpers/iron/buttons_helper.rb +14 -0
  46. data/app/helpers/iron/components/badge_helper.rb +70 -0
  47. data/app/helpers/iron/components/dropdown_helper.rb +161 -0
  48. data/app/helpers/iron/content_types_helper.rb +11 -0
  49. data/app/helpers/iron/entries_helper.rb +13 -0
  50. data/app/helpers/iron/field_definitions_helper.rb +5 -0
  51. data/app/helpers/iron/form_builder.rb +43 -0
  52. data/app/helpers/iron/icons_helper.rb +30 -0
  53. data/app/helpers/iron/image_helper.rb +50 -0
  54. data/app/javascript/iron/application.js +6 -0
  55. data/app/javascript/iron/controllers/application.js +9 -0
  56. data/app/javascript/iron/controllers/element_controller.js +11 -0
  57. data/app/javascript/iron/controllers/file_upload_controller.js +18 -0
  58. data/app/javascript/iron/controllers/form_controller.js +7 -0
  59. data/app/javascript/iron/controllers/handle_controller.js +27 -0
  60. data/app/javascript/iron/controllers/icon_picker_controller.js +48 -0
  61. data/app/javascript/iron/controllers/index.js +4 -0
  62. data/app/javascript/iron/controllers/sortable_list_controller.js +125 -0
  63. data/app/javascript/iron/controllers/toggle_controller.js +14 -0
  64. data/app/javascript/iron/controllers/trix_controller.js +19 -0
  65. data/app/javascript/iron/lib/lexorank.js +51 -0
  66. data/app/jobs/iron/application_job.rb +4 -0
  67. data/app/jobs/iron/generate_entry_routes_job.rb +15 -0
  68. data/app/mailers/iron/application_mailer.rb +6 -0
  69. data/app/mailers/passwords_mailer.rb +6 -0
  70. data/app/models/concerns/iron/csv_serializable.rb +28 -0
  71. data/app/models/iron/account/joinable.rb +16 -0
  72. data/app/models/iron/account.rb +13 -0
  73. data/app/models/iron/application_record.rb +5 -0
  74. data/app/models/iron/block_definition/portable.rb +20 -0
  75. data/app/models/iron/block_definition.rb +10 -0
  76. data/app/models/iron/content_type/field_queryable.rb +101 -0
  77. data/app/models/iron/content_type/portable.rb +30 -0
  78. data/app/models/iron/content_type/titlable.rb +13 -0
  79. data/app/models/iron/content_type/web_publishable.rb +47 -0
  80. data/app/models/iron/content_type.rb +39 -0
  81. data/app/models/iron/content_types/collection.rb +6 -0
  82. data/app/models/iron/content_types/single.rb +9 -0
  83. data/app/models/iron/current.rb +12 -0
  84. data/app/models/iron/entry/deep_validation.rb +32 -0
  85. data/app/models/iron/entry/presentable.rb +40 -0
  86. data/app/models/iron/entry/schemable.rb +16 -0
  87. data/app/models/iron/entry/titlable.rb +35 -0
  88. data/app/models/iron/entry/web_publishable.rb +85 -0
  89. data/app/models/iron/entry.rb +16 -0
  90. data/app/models/iron/field/belongs_to_entry.rb +29 -0
  91. data/app/models/iron/field.rb +20 -0
  92. data/app/models/iron/field_definition/portable.rb +36 -0
  93. data/app/models/iron/field_definition.rb +42 -0
  94. data/app/models/iron/field_definitions/block.rb +23 -0
  95. data/app/models/iron/field_definitions/block_list.rb +8 -0
  96. data/app/models/iron/field_definitions/boolean.rb +7 -0
  97. data/app/models/iron/field_definitions/date.rb +7 -0
  98. data/app/models/iron/field_definitions/file.rb +67 -0
  99. data/app/models/iron/field_definitions/number.rb +7 -0
  100. data/app/models/iron/field_definitions/reference.rb +8 -0
  101. data/app/models/iron/field_definitions/reference_list.rb +8 -0
  102. data/app/models/iron/field_definitions/rich_text_area.rb +4 -0
  103. data/app/models/iron/field_definitions/text_area.rb +7 -0
  104. data/app/models/iron/field_definitions/text_field.rb +25 -0
  105. data/app/models/iron/fields/block.rb +40 -0
  106. data/app/models/iron/fields/block_list.rb +16 -0
  107. data/app/models/iron/fields/boolean.rb +7 -0
  108. data/app/models/iron/fields/date.rb +7 -0
  109. data/app/models/iron/fields/file.rb +29 -0
  110. data/app/models/iron/fields/number.rb +7 -0
  111. data/app/models/iron/fields/reference.rb +14 -0
  112. data/app/models/iron/fields/reference_list.rb +17 -0
  113. data/app/models/iron/fields/rich_text_area.rb +9 -0
  114. data/app/models/iron/fields/text_area.rb +7 -0
  115. data/app/models/iron/fields/text_field.rb +19 -0
  116. data/app/models/iron/first_run.rb +13 -0
  117. data/app/models/iron/icon_catalog.rb +45 -0
  118. data/app/models/iron/locale.rb +30 -0
  119. data/app/models/iron/reference.rb +8 -0
  120. data/app/models/iron/schema_archive.rb +71 -0
  121. data/app/models/iron/schema_exporter.rb +15 -0
  122. data/app/models/iron/schema_importer.rb +278 -0
  123. data/app/models/iron/session.rb +5 -0
  124. data/app/models/iron/user/role.rb +21 -0
  125. data/app/models/iron/user.rb +37 -0
  126. data/app/views/active_storage/blobs/_blob.html.erb +14 -0
  127. data/app/views/iron/block_definitions/_block_definition.html.erb +21 -0
  128. data/app/views/iron/block_definitions/_form.html.erb +46 -0
  129. data/app/views/iron/block_definitions/edit.html.erb +8 -0
  130. data/app/views/iron/block_definitions/index.html.erb +18 -0
  131. data/app/views/iron/block_definitions/new.html.erb +8 -0
  132. data/app/views/iron/block_definitions/show.html.erb +34 -0
  133. data/app/views/iron/blocks/new.turbo_stream.erb +6 -0
  134. data/app/views/iron/content_types/_content_type.html.erb +21 -0
  135. data/app/views/iron/content_types/_form.html.erb +137 -0
  136. data/app/views/iron/content_types/edit.html.erb +8 -0
  137. data/app/views/iron/content_types/index.html.erb +28 -0
  138. data/app/views/iron/content_types/new.html.erb +8 -0
  139. data/app/views/iron/content_types/show.html.erb +49 -0
  140. data/app/views/iron/entries/_entry.html.erb +17 -0
  141. data/app/views/iron/entries/_entry_option.html.erb +19 -0
  142. data/app/views/iron/entries/_form.html.erb +63 -0
  143. data/app/views/iron/entries/edit.html.erb +52 -0
  144. data/app/views/iron/entries/entry.html.erb +21 -0
  145. data/app/views/iron/entries/fields/_block.html.erb +92 -0
  146. data/app/views/iron/entries/fields/_block_list.html.erb +45 -0
  147. data/app/views/iron/entries/fields/_boolean.html.erb +13 -0
  148. data/app/views/iron/entries/fields/_date.html.erb +11 -0
  149. data/app/views/iron/entries/fields/_file.html.erb +60 -0
  150. data/app/views/iron/entries/fields/_number.html.erb +11 -0
  151. data/app/views/iron/entries/fields/_reference.html.erb +12 -0
  152. data/app/views/iron/entries/fields/_reference_item.html.erb +28 -0
  153. data/app/views/iron/entries/fields/_reference_list.html.erb +44 -0
  154. data/app/views/iron/entries/fields/_rich_text_area.html.erb +13 -0
  155. data/app/views/iron/entries/fields/_text_area.html.erb +11 -0
  156. data/app/views/iron/entries/fields/_text_field.html.erb +28 -0
  157. data/app/views/iron/entries/index.html.erb +21 -0
  158. data/app/views/iron/entries/new.html.erb +8 -0
  159. data/app/views/iron/entries/search.html.erb +33 -0
  160. data/app/views/iron/field_definitions/_field_definition.html.erb +37 -0
  161. data/app/views/iron/field_definitions/block/_form.html.erb +16 -0
  162. data/app/views/iron/field_definitions/block_list/_form.html.erb +13 -0
  163. data/app/views/iron/field_definitions/boolean/_form.html.erb +2 -0
  164. data/app/views/iron/field_definitions/date/_form.html.erb +2 -0
  165. data/app/views/iron/field_definitions/edit.html.erb +16 -0
  166. data/app/views/iron/field_definitions/file/_form.html.erb +40 -0
  167. data/app/views/iron/field_definitions/index.html.erb +14 -0
  168. data/app/views/iron/field_definitions/layouts/_form.html.erb +47 -0
  169. data/app/views/iron/field_definitions/new.html.erb +22 -0
  170. data/app/views/iron/field_definitions/number/_form.html.erb +2 -0
  171. data/app/views/iron/field_definitions/reference/_form.html.erb +13 -0
  172. data/app/views/iron/field_definitions/reference_list/_form.html.erb +13 -0
  173. data/app/views/iron/field_definitions/rich_text_area/_form.html.erb +2 -0
  174. data/app/views/iron/field_definitions/text_area/_form.html.erb +2 -0
  175. data/app/views/iron/field_definitions/text_field/_form.html.erb +17 -0
  176. data/app/views/iron/first_runs/show.html.erb +44 -0
  177. data/app/views/iron/home/show.html.erb +2 -0
  178. data/app/views/iron/icons/index.html.erb +21 -0
  179. data/app/views/iron/locales/_form.html.erb +24 -0
  180. data/app/views/iron/locales/_locale.html.erb +15 -0
  181. data/app/views/iron/locales/edit.html.erb +8 -0
  182. data/app/views/iron/locales/index.html.erb +12 -0
  183. data/app/views/iron/locales/new.html.erb +8 -0
  184. data/app/views/iron/passwords/edit.html.erb +7 -0
  185. data/app/views/iron/passwords/new.html.erb +26 -0
  186. data/app/views/iron/passwords_mailer/reset.html.erb +4 -0
  187. data/app/views/iron/passwords_mailer/reset.text.erb +2 -0
  188. data/app/views/iron/published_pages/show.html.erb +10 -0
  189. data/app/views/iron/references/new.turbo_stream.erb +6 -0
  190. data/app/views/iron/schemas/new.html.erb +57 -0
  191. data/app/views/iron/sessions/new.html.erb +36 -0
  192. data/app/views/iron/settings/show.html.erb +38 -0
  193. data/app/views/iron/shared/_icon_picker.html.erb +60 -0
  194. data/app/views/iron/users/_form.html.erb +21 -0
  195. data/app/views/iron/users/_user.html.erb +27 -0
  196. data/app/views/iron/users/edit.html.erb +12 -0
  197. data/app/views/iron/users/index.html.erb +21 -0
  198. data/app/views/iron/users/new.html.erb +29 -0
  199. data/app/views/iron/users/show.html.erb +21 -0
  200. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  201. data/app/views/layouts/iron/_sidebar.html.erb +83 -0
  202. data/app/views/layouts/iron/_sidebar_item.html.erb +14 -0
  203. data/app/views/layouts/iron/_toast.html.erb +29 -0
  204. data/app/views/layouts/iron/application.html.erb +46 -0
  205. data/app/views/layouts/iron/authentication.html.erb +41 -0
  206. data/config/importmap.rb +9 -0
  207. data/config/routes.rb +40 -0
  208. data/db/migrate/20250422131656_initial_iron_schema.rb +105 -0
  209. data/db/migrate/20250427100754_create_iron_accounts.rb +10 -0
  210. data/db/migrate/20250427104020_add_role_to_iron_users.rb +5 -0
  211. data/db/migrate/20250428094923_add_default_iron_locale_to_iron_account.rb +5 -0
  212. data/db/migrate/20250504144731_create_join_table_content_types_field_definitions.rb +25 -0
  213. data/db/migrate/20250505090716_add_title_field_definition_to_iron_content_types.rb +5 -0
  214. data/db/migrate/20250512210352_create_iron_references.rb +11 -0
  215. data/db/migrate/20250524220355_add_referenced_entry_id_to_iron_fields.rb +5 -0
  216. data/db/migrate/20250529211703_add_metadata_to_iron_field_definitions.rb +5 -0
  217. data/db/migrate/20250529211704_add_value_boolean_to_iron_fields.rb +5 -0
  218. data/db/migrate/20250531172837_add_type_to_iron_content_types.rb +5 -0
  219. data/db/migrate/20250601080146_add_icon_to_iron_content_types.rb +5 -0
  220. data/db/migrate/20250609091605_add_web_publishing_to_iron_content_types.rb +7 -0
  221. data/db/migrate/20250609091813_add_route_to_iron_entries.rb +6 -0
  222. data/lib/generators/iron/pages/pages_generator.rb +34 -0
  223. data/lib/generators/iron/pages/templates/pages_controller.rb +14 -0
  224. data/lib/generators/iron/pages/templates/show.html.erb +9 -0
  225. data/lib/generators/iron/template/template_generator.rb +35 -0
  226. data/lib/generators/iron/template/templates/content_type.html.erb +5 -0
  227. data/lib/iron/cva.rb +69 -0
  228. data/lib/iron/engine.rb +42 -0
  229. data/lib/iron/image_analyzer.rb +26 -0
  230. data/lib/iron/lexorank/exceptions.rb +4 -0
  231. data/lib/iron/lexorank/rankable.rb +185 -0
  232. data/lib/iron/lexorank/rebalance_rank_job.rb +10 -0
  233. data/lib/iron/lexorank/utils.rb +77 -0
  234. data/lib/iron/lexorank.rb +7 -0
  235. data/lib/iron/routing.rb +66 -0
  236. data/lib/iron/sdk.rb +19 -0
  237. data/lib/iron/version.rb +3 -0
  238. data/lib/iron.rb +8 -0
  239. data/lib/puma/plugin/iron_tailwindcss.rb +78 -0
  240. metadata +458 -0
@@ -0,0 +1,10 @@
1
+ class CreateIronAccounts < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :iron_accounts do |t|
4
+ t.string :name, null: false
5
+ t.string :join_code, null: false
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ class AddRoleToIronUsers < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :iron_users, :role, :string, null: false, default: "member"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddDefaultIronLocaleToIronAccount < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_reference :iron_accounts, :default_locale, null: false, foreign_key: { to_table: :iron_locales }
4
+ end
5
+ end
@@ -0,0 +1,25 @@
1
+ class CreateJoinTableContentTypesFieldDefinitions < ActiveRecord::Migration[8.0]
2
+ def change
3
+ # create_join_table :content_types, :field_definitions do |t|
4
+ # # t.index [:content_type_id, :field_definition_id]
5
+ # # t.index [:field_definition_id, :content_type_id]
6
+ # end
7
+ #
8
+ # create_table "iron_content_types_field_definitions", id: false, force: :cascade do |t|
9
+ # t.integer "content_type_id", null: false
10
+ # t.integer "field_definition_id", null: false
11
+ # t.index [ "content_type_id", "field_definition_id" ], name: "idx_on_block_definition_id_field_definition_id_b671d92478", unique: true
12
+ # t.index [ "content_type_id" ], name: "idx_on_block_definition_id_601f892a8c"
13
+ # t.index [ "field_definition_id", "block_definition_id" ], name: "idx_on_field_definition_id_block_definition_id_be1c71c596", unique: true
14
+ # t.index [ "field_definition_id" ], name: "idx_on_field_definition_id_d46765b01a"
15
+ # end
16
+
17
+ create_join_table :content_types, :field_definitions, table_name: :iron_content_types_field_definitions do |t|
18
+ t.index [ :content_type_id, :field_definition_id ], name: "idx_on_content_type_id_field_definition_id", unique: true
19
+ t.index [ :field_definition_id, :content_type_id ], name: "idx_on_field_definition_id_content_type_id", unique: true
20
+ end
21
+
22
+ add_foreign_key :iron_content_types_field_definitions, :iron_content_types, column: :content_type_id
23
+ add_foreign_key :iron_content_types_field_definitions, :iron_field_definitions, column: :field_definition_id
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ class AddTitleFieldDefinitionToIronContentTypes < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_reference :iron_content_types, :title_field_definition, foreign_key: { to_table: :iron_field_definitions }
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ class CreateIronReferences < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :iron_references do |t|
4
+ t.references :entry, null: false, foreign_key: { to_table: :iron_entries }
5
+ t.references :list, null: false, foreign_key: { to_table: :iron_fields }
6
+ t.text :rank
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ class AddReferencedEntryIdToIronFields < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_reference :iron_fields, :referenced_entry, foreign_key: { to_table: :iron_entries }
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddMetadataToIronFieldDefinitions < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :iron_field_definitions, :metadata, :jsonb, default: {}, null: false
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddValueBooleanToIronFields < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :iron_fields, :value_boolean, :boolean
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddTypeToIronContentTypes < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :iron_content_types, :type, :string, default: "Iron::ContentTypes::Collection", null: false
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddIconToIronContentTypes < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :iron_content_types, :icon, :string
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ class AddWebPublishingToIronContentTypes < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :iron_content_types, :web_publishing_enabled, :boolean, default: false
4
+ add_reference :iron_content_types, :web_page_title_field_definition, null: true, foreign_key: { to_table: :iron_field_definitions }
5
+ add_column :iron_content_types, :base_path, :string
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ class AddRouteToIronEntries < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :iron_entries, :route, :string
4
+ add_index :iron_entries, [ :content_type_id, :route ], unique: true
5
+ end
6
+ end
@@ -0,0 +1,34 @@
1
+ require "rails/generators"
2
+
3
+ module Iron
4
+ module Generators
5
+ class PagesGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ def create_controller
9
+ template "pages_controller.rb", "app/controllers/pages_controller.rb"
10
+ end
11
+
12
+ def create_views
13
+ template "show.html.erb", "app/views/pages/show.html.erb"
14
+ end
15
+
16
+ def add_routes
17
+ route "iron_pages"
18
+ end
19
+
20
+ def display_instructions
21
+ say "\n=== Iron CMS Pages Setup Complete ===\n", :green
22
+ say "The pages controller has been installed in your application.\n"
23
+ say "\nNext steps:"
24
+ say "1. Create custom views for your content types in app/views/templates/"
25
+ say " For a content type with handle 'article', create:"
26
+ say " - app/views/templates/article.html.erb"
27
+ say "\n2. The default view (app/views/pages/show.html.erb) will be used"
28
+ say " for any content types without custom views."
29
+ say "\n3. The iron_entry_path helper is available from the Iron engine."
30
+ say "\n4. Remember to restart your Rails server after installation."
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,14 @@
1
+ class PagesController < ApplicationController
2
+ include Iron::LocaleAware
3
+
4
+ def show
5
+ @content_type = Iron::ContentType.web_published.find_by_handle!(params[:content_type])
6
+ @entry = Iron::SDK.send(@content_type.handle).find(params[:route])
7
+
8
+ raise ActiveRecord::RecordNotFound unless @entry
9
+
10
+ render "templates/#{@content_type.handle}"
11
+ rescue ActionView::MissingTemplate
12
+ render "pages/show"
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ <h1><%%= @entry._metadata.web.title %></h1>
2
+
3
+ <hr>
4
+
5
+ <p>
6
+ This is the default Iron CMS view. To create a custom view for this content type, create:
7
+ </p>
8
+
9
+ <pre><code>app/views/templates/<%%= @content_type.handle %>.html.erb</code></pre>
@@ -0,0 +1,35 @@
1
+ require "rails/generators"
2
+
3
+ module Iron
4
+ module Generators
5
+ class TemplateGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ argument :content_type_handle, type: :string, desc: "The handle of the content type"
9
+
10
+ def create_template_file
11
+ @content_type = Iron::ContentType.find_by!(handle: content_type_handle)
12
+ template "content_type.html.erb", "app/views/templates/#{content_type_handle}.html.erb"
13
+ end
14
+
15
+ def display_instructions
16
+ say "\n=== Template created for '#{content_type_name}' ===\n", :green
17
+ say "You can now customize the template at:"
18
+ say " app/views/templates/#{content_type_handle}.html.erb"
19
+ say "\nAvailable variables in the template:"
20
+ say " @entry - The current entry with all its fields"
21
+ say " @content_type - The content type definition"
22
+ end
23
+
24
+ private
25
+
26
+ def content_type_name
27
+ @content_type.name
28
+ end
29
+
30
+ def content_type_fields
31
+ @content_type.fields
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ <h1><%%= @entry.attributes._metadata.web.title %></h1>
2
+
3
+ <p>
4
+ Edit this template in <code>app/views/templates/<%= content_type_handle %>.html.erb</code>
5
+ </p>
data/lib/iron/cva.rb ADDED
@@ -0,0 +1,69 @@
1
+ module Iron
2
+ module Cva
3
+ class VariantBuilder
4
+ def initialize(base_classes: "", variants: {}, defaults: {}, compound_variants: [], required: [])
5
+ @base_classes = base_classes.split(" ")
6
+ @variants = normalize_keys(variants)
7
+ @defaults = normalize_keys(defaults)
8
+ @compound_variants = compound_variants.map { |cv| normalize_keys(cv) }
9
+ @required = required.map(&:to_sym)
10
+ end
11
+
12
+ def call(options = {})
13
+ options = normalize_keys(options)
14
+ additional_classes = options.delete(:class)&.split(" ") || []
15
+ classes = @base_classes.dup
16
+
17
+ # Apply default variants
18
+ options = @defaults.merge(options)
19
+
20
+ # Apply variants
21
+ @variants.each do |variant_name, variant_options|
22
+ if value = options[variant_name]
23
+ # Convert the value to symbol if the variant options use symbol keys
24
+ value = value.to_sym if variant_options.keys.first.is_a?(Symbol)
25
+ if variant_classes = variant_options[value]
26
+ classes.concat(variant_classes.split(" "))
27
+ end
28
+ end
29
+ end
30
+
31
+ # Apply compound variants
32
+ @compound_variants.each do |compound|
33
+ conditions = compound.except(:class)
34
+ matches = conditions.all? do |key, expected_value|
35
+ actual_value = options[key]
36
+ # Convert values to the same type for comparison
37
+ if expected_value.is_a?(Symbol)
38
+ actual_value = actual_value.to_sym if actual_value
39
+ elsif expected_value.is_a?(String)
40
+ actual_value = actual_value.to_s if actual_value
41
+ end
42
+ actual_value == expected_value
43
+ end
44
+
45
+ if matches && compound[:class]
46
+ classes.concat(compound[:class].split(" "))
47
+ end
48
+ end
49
+
50
+ # Validate required variants
51
+ @required.each do |required_variant|
52
+ unless options.key?(required_variant)
53
+ raise ArgumentError, "Missing required variant: #{required_variant}"
54
+ end
55
+ end
56
+
57
+ result = classes.uniq
58
+ result.concat(additional_classes)
59
+ result.join(" ")
60
+ end
61
+
62
+ private
63
+
64
+ def normalize_keys(hash)
65
+ hash.transform_keys(&:to_sym)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,42 @@
1
+ require "importmap-rails"
2
+ require "turbo-rails"
3
+ require "stimulus-rails"
4
+ require "active_storage/engine"
5
+ require "action_text/engine"
6
+ require "bcrypt"
7
+ require "ostruct"
8
+ require "iron/lexorank"
9
+ require "iron/sdk"
10
+ require "iron/routing"
11
+ require "iron/image_analyzer"
12
+
13
+ module Iron
14
+ class Engine < ::Rails::Engine
15
+ isolate_namespace Iron
16
+
17
+ initializer "iron.setup", before: "importmap" do |app|
18
+ app.config.assets.paths.unshift(root.join("vendor/javascript"))
19
+ app.config.assets.paths << root.join("app/javascript")
20
+ app.config.importmap.paths << root.join("config/importmap.rb")
21
+ app.config.importmap.cache_sweepers << root.join("app/javascript")
22
+
23
+ app.config.assets.precompile += %w[iron_manifest]
24
+ end
25
+
26
+ initializer "iron.routing" do
27
+ ActionDispatch::Routing::Mapper.include Iron::Routing
28
+ end
29
+
30
+ initializer "iron.active_storage_analyzers" do |app|
31
+ app.config.active_storage.analyzers.prepend Iron::ImageAnalyzer
32
+ end
33
+
34
+ config.to_prepare do
35
+ ActionView::Base.include(Module.new do
36
+ def attachment_url(attachment, **options)
37
+ attachment.url(**options)
38
+ end
39
+ end)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,26 @@
1
+ module Iron
2
+ class ImageAnalyzer < ActiveStorage::Analyzer::ImageAnalyzer::Vips
3
+ def metadata
4
+ download_blob_to_tempfile do |file|
5
+ super.merge(blur_data_uri: generate_blur_placeholder(file))
6
+ end
7
+ end
8
+
9
+ private
10
+
11
+ def generate_blur_placeholder(file)
12
+ processed = ImageProcessing::Vips
13
+ .source(file)
14
+ .resize_to_limit(20, nil)
15
+ .gaussblur(2)
16
+ .convert("jpg")
17
+ .saver(quality: 20, strip: true)
18
+ .call
19
+
20
+ image_data = File.read(processed.path)
21
+ "data:image/jpeg;base64,#{Base64.strict_encode64(image_data)}"
22
+ ensure
23
+ processed&.close if processed.respond_to?(:close)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,4 @@
1
+ module Iron::Lexorank::Exceptions
2
+ class InvalidRankError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,185 @@
1
+ module Iron
2
+ module Lexorank
3
+ LONG_RANK_SIZE = 10
4
+ module Rankable
5
+ module HasRank
6
+ attr_reader :rank_column, :rank_scoped_by, :has_rank_applied
7
+
8
+ def has_rank(column: :rank, scoped_by: nil)
9
+ self.class_variable_set("@@rank_column", column)
10
+ cattr_reader :rank_column, instance_reader: false
11
+
12
+ self.class_variable_set("@@rank_scoped_by", grab_column(scoped_by))
13
+ cattr_reader :rank_scoped_by, instance_reader: false
14
+
15
+ @has_rank_applied = true
16
+
17
+ scope :ranked, ->(direction: :asc) { where.not("#{column}": nil).order("#{column}": direction) }
18
+
19
+ after_save_commit :request_rank_rebalance, if: -> {
20
+ rank.present? && rank.length > Lexorank::LONG_RANK_SIZE
21
+ }
22
+
23
+ include InstanceMethods
24
+ extend ClassMethods
25
+ end
26
+
27
+ private
28
+
29
+ def grab_column(column)
30
+ return unless column
31
+
32
+ if association = reflect_on_association(column)
33
+ if association.options[:polymorphic]
34
+ [ association.foreign_type.to_sym, association.foreign_key.to_sym ]
35
+ else
36
+ association.foreign_key.to_sym
37
+ end
38
+ else
39
+ column
40
+ end
41
+ end
42
+ end
43
+
44
+ module ClassMethods
45
+ include Iron::Lexorank::Utils
46
+
47
+ def rebalance(rank_scope = nil)
48
+ if rank_scope.present?
49
+ if rank_scoped_by.is_a?(Array)
50
+ type_column, id_column = rank_scoped_by
51
+ ranked_scoped_items = ranked.where(
52
+ "#{type_column}": rank_scope.first,
53
+ "#{id_column}": rank_scope.second
54
+ )
55
+ else
56
+ ranked_scoped_items = ranked.where("#{rank_scoped_by}": rank_scope)
57
+ end
58
+
59
+ interval = optimal_rank_numeric_interval_for(ranked_scoped_items.count)
60
+
61
+ self.transaction do
62
+ ranked_scoped_items.each.with_index(1) do |item, index|
63
+ item.rank = to_rank(interval * index) + "-rebalancing"
64
+ item.save!
65
+ end
66
+
67
+ ranked_scoped_items.each do |item|
68
+ item.rank = item.rank.gsub("-rebalancing", "")
69
+ item.save!
70
+ end
71
+ end
72
+ else
73
+ raise "Not implemented yet"
74
+ end
75
+ end
76
+ end
77
+
78
+ module InstanceMethods
79
+ include Iron::Lexorank::Utils
80
+
81
+ def move_to(position)
82
+ # exceptions:
83
+ # move to the beginning (aka move to position 0)
84
+ # move to end (aka position = collection.size - 1)
85
+ # when moving to the end of the collection the offset and limit statement automatically handles
86
+ # that 'after' is nil which is the same like [collection.last, nil]
87
+ before, after =
88
+ if position.zero?
89
+ [ nil, scoped_collection.first ]
90
+ else
91
+ scoped_collection.where.not(id: id).offset(position - 1).limit(2)
92
+ end
93
+
94
+ rank =
95
+ if self == after && send(rank_column).present?
96
+ send(rank_column)
97
+ else
98
+ value_between(before&.send(rank_column), after&.send(rank_column))
99
+ end
100
+
101
+ send("#{rank_column}=", rank)
102
+ end
103
+
104
+ def move_to!(position)
105
+ move_to(position)
106
+ save
107
+ end
108
+
109
+ def move_to_top
110
+ after = scoped_collection.first
111
+ rank = value_between(nil, after&.send(rank_column))
112
+ send("#{rank_column}=", rank)
113
+ end
114
+
115
+ def move_to_top!
116
+ move_to_top
117
+ save
118
+ end
119
+
120
+ def move_to_bottom
121
+ before = scoped_collection.last
122
+ rank = value_between(before&.send(rank_column), nil)
123
+ send("#{rank_column}=", rank)
124
+ end
125
+
126
+ def move_to_bottom!
127
+ move_to_bottom
128
+ save
129
+ end
130
+
131
+ def no_rank?
132
+ !send(rank_column)
133
+ end
134
+
135
+ private
136
+
137
+ def rank_column
138
+ self.class.rank_column
139
+ end
140
+
141
+ def rank_scoped_by
142
+ self.class.rank_scoped_by
143
+ end
144
+
145
+ def scoped_collection
146
+ collection = base_class.ranked
147
+ if rank_scoped_by.present?
148
+ if rank_scoped_by.is_a?(Array)
149
+ type_column, id_column = rank_scoped_by
150
+ collection = collection.where(
151
+ "#{type_column}": send(type_column),
152
+ "#{id_column}": send(id_column)
153
+ )
154
+ else
155
+ collection = collection.where("#{rank_scoped_by}": send(rank_scoped_by))
156
+ end
157
+ end
158
+ collection
159
+ end
160
+
161
+ def base_class
162
+ if self.class.has_rank_applied
163
+ self.class
164
+ else
165
+ self.class.base_class
166
+ end
167
+ end
168
+
169
+ def request_rank_rebalance
170
+ if rank_scoped_by.is_a?(Array)
171
+ type_column, id_column = rank_scoped_by
172
+ rank_scope = [ send(type_column), send(id_column) ]
173
+ else
174
+ rank_scope = send(rank_scoped_by)
175
+ end
176
+ Iron::Lexorank::RebalanceRankJob.perform_later(base_class.name, rank_scope)
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ ActiveSupport.on_load :active_record do
184
+ extend Iron::Lexorank::Rankable::HasRank
185
+ end
@@ -0,0 +1,10 @@
1
+ module Iron::Lexorank
2
+ class RebalanceRankJob < ActiveJob::Base
3
+ queue_as :default
4
+
5
+ def perform(model_name, rank_scope)
6
+ model_class = model_name.constantize
7
+ model_class.rebalance(rank_scope)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,77 @@
1
+ module Iron::Lexorank::Utils
2
+ MIN_CHAR = "0"
3
+ MAX_CHAR = "z"
4
+ BASE = MAX_CHAR.ord - MIN_CHAR.ord + 1
5
+
6
+ def value_between(before_, after_)
7
+ before = before_ || MIN_CHAR
8
+ after = after_ || MAX_CHAR
9
+
10
+ rank = ""
11
+
12
+ (before.length + after.length).times do |i|
13
+ prev_char = get_char(before, i, MIN_CHAR)
14
+ after_char = get_char(after, i, MAX_CHAR)
15
+
16
+ if prev_char == after_char
17
+ rank += prev_char
18
+ next
19
+ end
20
+
21
+ mid_char = mid(prev_char, after_char)
22
+ if mid_char == prev_char || mid_char == after_char
23
+ rank += prev_char
24
+ next
25
+ end
26
+
27
+ rank += mid_char
28
+ break
29
+ end
30
+
31
+ # Problem: If we try to get a rank before the character '0' or after 'z' the algorithm would return the same char
32
+ # This first of all breaks a possible unique constraint and of course makes no sense when ordering the items.
33
+ #
34
+ # Thoughts: I think this issue will never happen with the Lexorank::Rankable module
35
+ # Why? Let's look at '0' as a rank:
36
+ # Because the algorithm always chooses the char in between two other chars, '0' can only happen when before is nil and after is '1'
37
+ # In this case the algorithm will return '0U' though. This means there will never be an item with rank '0' which is why this condition
38
+ # should never equal to true.
39
+ #
40
+ # Please report if you have another opinion about that or if you reached the exception! (of course you can force it by using `value_between(nil, '0')`)
41
+ if rank >= after
42
+ raise Lexorank::Exceptions::InvalidRankError,
43
+ "This rank should not be achievable using the Lexorank::Rankable module! " \
44
+ "The supplied ranks were #{before_.inspect} and #{after_.inspect}."
45
+ end
46
+ rank
47
+ end
48
+
49
+ def mid(prev, after)
50
+ middle_ascii = ((prev.ord + after.ord) / 2).round
51
+ middle_ascii.chr
52
+ end
53
+
54
+ def get_char(string, index, default_char)
55
+ index >= string.length ? default_char : string[index]
56
+ end
57
+
58
+ def to_rank(number)
59
+ return MIN_CHAR if number.zero?
60
+
61
+ result = ""
62
+ while number > 0
63
+ remainder = number % BASE
64
+ number /= BASE
65
+ result = (MIN_CHAR.ord + remainder).chr + result
66
+ end
67
+ result
68
+ end
69
+
70
+ def optimal_rank_numeric_interval_for(items_count)
71
+ i = 1
72
+ while items_count >= BASE ** i
73
+ i += 1
74
+ end
75
+ (BASE ** i) / (items_count + 1)
76
+ end
77
+ end
@@ -0,0 +1,7 @@
1
+ module Iron::Lexorank
2
+ end
3
+
4
+ require "iron/lexorank/exceptions"
5
+ require "iron/lexorank/utils"
6
+ require "iron/lexorank/rankable"
7
+ require "iron/lexorank/rebalance_rank_job"