ruby_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/.cursor/dhh.mdc +698 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/README.md +235 -0
- data/Rakefile +30 -0
- data/app/components/ruby_cms/admin/admin_page/admin_table_content.rb +32 -0
- data/app/components/ruby_cms/admin/admin_page.rb +345 -0
- data/app/components/ruby_cms/admin/base_component.rb +78 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table.rb +149 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_actions.rb +127 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_body.rb +15 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_cell.rb +41 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_head.rb +33 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_delete_modal.rb +174 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header.rb +59 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header_bar.rb +159 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_pagination.rb +192 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_row.rb +97 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +137 -0
- data/app/controllers/concerns/ruby_cms/admin_pagination.rb +120 -0
- data/app/controllers/concerns/ruby_cms/admin_turbo_table.rb +68 -0
- data/app/controllers/concerns/ruby_cms/page_tracking.rb +52 -0
- data/app/controllers/concerns/ruby_cms/visitor_error_capture.rb +39 -0
- data/app/controllers/ruby_cms/admin/analytics_controller.rb +191 -0
- data/app/controllers/ruby_cms/admin/base_controller.rb +105 -0
- data/app/controllers/ruby_cms/admin/content_blocks_controller.rb +390 -0
- data/app/controllers/ruby_cms/admin/dashboard_controller.rb +50 -0
- data/app/controllers/ruby_cms/admin/locale_controller.rb +20 -0
- data/app/controllers/ruby_cms/admin/permissions_controller.rb +66 -0
- data/app/controllers/ruby_cms/admin/settings_controller.rb +223 -0
- data/app/controllers/ruby_cms/admin/user_permissions_controller.rb +59 -0
- data/app/controllers/ruby_cms/admin/users_controller.rb +107 -0
- data/app/controllers/ruby_cms/admin/visitor_errors_controller.rb +89 -0
- data/app/controllers/ruby_cms/admin/visual_editor_controller.rb +322 -0
- data/app/controllers/ruby_cms/errors_controller.rb +35 -0
- data/app/helpers/ruby_cms/admin/admin_page_helper.rb +21 -0
- data/app/helpers/ruby_cms/admin/bulk_action_table_helper.rb +159 -0
- data/app/helpers/ruby_cms/application_helper.rb +41 -0
- data/app/helpers/ruby_cms/bulk_action_table_helper.rb +151 -0
- data/app/helpers/ruby_cms/content_blocks_helper.rb +375 -0
- data/app/helpers/ruby_cms/settings_helper.rb +160 -0
- data/app/javascript/controllers/ruby_cms/auto_save_preference_controller.js +73 -0
- data/app/javascript/controllers/ruby_cms/bulk_action_table_controller.js +553 -0
- data/app/javascript/controllers/ruby_cms/clickable_row_controller.js +28 -0
- data/app/javascript/controllers/ruby_cms/flash_messages_controller.js +29 -0
- data/app/javascript/controllers/ruby_cms/index.js +104 -0
- data/app/javascript/controllers/ruby_cms/locale_tabs_controller.js +34 -0
- data/app/javascript/controllers/ruby_cms/mobile_menu_controller.js +55 -0
- data/app/javascript/controllers/ruby_cms/nav_order_sortable_controller.js +192 -0
- data/app/javascript/controllers/ruby_cms/page_preview_controller.js +135 -0
- data/app/javascript/controllers/ruby_cms/toggle_controller.js +39 -0
- data/app/javascript/controllers/ruby_cms/visual_editor_controller.js +321 -0
- data/app/models/concerns/content_block/publishable.rb +54 -0
- data/app/models/concerns/content_block/searchable.rb +22 -0
- data/app/models/content_block.rb +155 -0
- data/app/models/ruby_cms/content_block.rb +8 -0
- data/app/models/ruby_cms/permission.rb +28 -0
- data/app/models/ruby_cms/permittable.rb +39 -0
- data/app/models/ruby_cms/preference.rb +111 -0
- data/app/models/ruby_cms/user_permission.rb +12 -0
- data/app/models/ruby_cms/visitor_error.rb +109 -0
- data/app/services/ruby_cms/analytics/report.rb +362 -0
- data/app/services/ruby_cms/security_tracker.rb +92 -0
- data/app/views/layouts/ruby_cms/_admin_flash_messages.html.erb +37 -0
- data/app/views/layouts/ruby_cms/_admin_sidebar.html.erb +121 -0
- data/app/views/layouts/ruby_cms/admin.html.erb +81 -0
- data/app/views/layouts/ruby_cms/minimal.html.erb +181 -0
- data/app/views/ruby_cms/admin/analytics/index.html.erb +160 -0
- data/app/views/ruby_cms/admin/analytics/page_details.html.erb +84 -0
- data/app/views/ruby_cms/admin/analytics/partials/_back_button.html.erb +3 -0
- data/app/views/ruby_cms/admin/analytics/partials/_browser_device.html.erb +40 -0
- data/app/views/ruby_cms/admin/analytics/partials/_daily_activity_chart.html.erb +58 -0
- data/app/views/ruby_cms/admin/analytics/partials/_hourly_activity_chart.html.erb +51 -0
- data/app/views/ruby_cms/admin/analytics/partials/_recent_activity.html.erb +31 -0
- data/app/views/ruby_cms/admin/analytics/partials/_security_alert.html.erb +4 -0
- data/app/views/ruby_cms/admin/analytics/partials/_top_referrers.html.erb +21 -0
- data/app/views/ruby_cms/admin/analytics/visitor_details.html.erb +125 -0
- data/app/views/ruby_cms/admin/content_blocks/_form.html.erb +161 -0
- data/app/views/ruby_cms/admin/content_blocks/_row.html.erb +25 -0
- data/app/views/ruby_cms/admin/content_blocks/edit.html.erb +17 -0
- data/app/views/ruby_cms/admin/content_blocks/index.html.erb +66 -0
- data/app/views/ruby_cms/admin/content_blocks/new.html.erb +5 -0
- data/app/views/ruby_cms/admin/content_blocks/show.html.erb +110 -0
- data/app/views/ruby_cms/admin/dashboard/index.html.erb +198 -0
- data/app/views/ruby_cms/admin/permissions/_row.html.erb +11 -0
- data/app/views/ruby_cms/admin/permissions/index.html.erb +62 -0
- data/app/views/ruby_cms/admin/settings/index.html.erb +220 -0
- data/app/views/ruby_cms/admin/shared/_bulk_action_table_index.html.erb +56 -0
- data/app/views/ruby_cms/admin/user_permissions/index.html.erb +55 -0
- data/app/views/ruby_cms/admin/users/_row.html.erb +14 -0
- data/app/views/ruby_cms/admin/users/index.html.erb +70 -0
- data/app/views/ruby_cms/admin/visitor_errors/_row.html.erb +35 -0
- data/app/views/ruby_cms/admin/visitor_errors/index.html.erb +57 -0
- data/app/views/ruby_cms/admin/visitor_errors/show.html.erb +147 -0
- data/app/views/ruby_cms/admin/visual_editor/index.html.erb +144 -0
- data/app/views/ruby_cms/errors/not_found.html.erb +92 -0
- data/config/database.yml +6 -0
- data/config/importmap.rb +36 -0
- data/config/locales/en.yml +101 -0
- data/config/routes.rb +65 -0
- data/db/migrate/20260125000001_create_ruby_cms_permissions.rb +14 -0
- data/db/migrate/20260125000002_create_ruby_cms_user_permissions.rb +14 -0
- data/db/migrate/20260125000003_create_ruby_cms_content_blocks.rb +19 -0
- data/db/migrate/20260125000010_add_indexes_to_ruby_cms_tables.rb +9 -0
- data/db/migrate/20260127000001_add_locale_to_ruby_cms_content_blocks.rb +34 -0
- data/db/migrate/20260129000001_create_ruby_cms_visitor_errors.rb +24 -0
- data/db/migrate/20260130000001_add_referer_and_query_to_ruby_cms_visitor_errors.rb +8 -0
- data/db/migrate/20260130000002_create_ruby_cms_preferences.rb +16 -0
- data/db/migrate/20260130000003_add_category_to_ruby_cms_preferences.rb +8 -0
- data/db/migrate/20260211000001_add_ruby_cms_analytics_fields_to_ahoy_events.rb +19 -0
- data/db/migrate/20260212000001_use_unprefixed_cms_tables.rb +146 -0
- data/exe/ruby_cms +25 -0
- data/lib/generators/ruby_cms/install_generator.rb +1062 -0
- data/lib/generators/ruby_cms/templates/admin.html.erb +82 -0
- data/lib/generators/ruby_cms/templates/ruby_cms.rb +86 -0
- data/lib/ruby_cms/app_integration.rb +82 -0
- data/lib/ruby_cms/cli.rb +169 -0
- data/lib/ruby_cms/content_blocks_grouping.rb +41 -0
- data/lib/ruby_cms/content_blocks_sync.rb +329 -0
- data/lib/ruby_cms/css_compiler.rb +35 -0
- data/lib/ruby_cms/engine.rb +498 -0
- data/lib/ruby_cms/settings.rb +145 -0
- data/lib/ruby_cms/settings_registry.rb +289 -0
- data/lib/ruby_cms/version.rb +5 -0
- data/lib/ruby_cms.rb +195 -0
- data/lib/tasks/ruby_cms.rake +27 -0
- data/log/test.log +17875 -0
- data/sig/ruby_cms.rbs +4 -0
- metadata +223 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "settings_registry"
|
|
4
|
+
require_relative "settings"
|
|
5
|
+
|
|
6
|
+
module RubyCms
|
|
7
|
+
class Engine < ::Rails::Engine
|
|
8
|
+
# Do not isolate namespace so we can use /admin and explicit table names.
|
|
9
|
+
# Engine models use unprefixed table names: content_blocks, preferences, permissions, user_permissions, visitor_errors.
|
|
10
|
+
|
|
11
|
+
config.ruby_cms = ActiveSupport::OrderedOptions.new
|
|
12
|
+
|
|
13
|
+
# Base controller for all /admin controllers. Must provide current_user and
|
|
14
|
+
# run require_authentication (or equivalent). Default: ApplicationController.
|
|
15
|
+
config.ruby_cms.admin_base_controller = "ApplicationController"
|
|
16
|
+
# Layout used for /admin pages. Default: "admin/admin" (app's layouts/admin/admin.html.erb).
|
|
17
|
+
config.ruby_cms.admin_layout = "admin/admin"
|
|
18
|
+
config.ruby_cms.user_class_name = "User"
|
|
19
|
+
|
|
20
|
+
# When true, allow user.admin? as bypass when no Permission records exist (bootstrap).
|
|
21
|
+
config.ruby_cms.bootstrap_admin_with_role = true
|
|
22
|
+
|
|
23
|
+
# Path to redirect to when unauthenticated or not permitted (e.g. "/" or "/session/new").
|
|
24
|
+
# main_app.root_path is not used by default because the host may not define a root route.
|
|
25
|
+
config.ruby_cms.unauthorized_redirect_path = "/"
|
|
26
|
+
|
|
27
|
+
# Callable to resolve "current user" from the request. Receives controller, returns user or nil.
|
|
28
|
+
# Default: ->(c) { c.respond_to?(:current_user) ? c.current_user : nil }
|
|
29
|
+
config.ruby_cms.current_user_resolver = lambda {|controller|
|
|
30
|
+
controller.respond_to?(:current_user) ? controller.current_user : nil
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Visual editor: allowlist of page_key => template path (e.g. "home" => "pages/home").
|
|
34
|
+
# Can be extended by Page model: Page.preview_templates_hash merges config with Page records.
|
|
35
|
+
config.ruby_cms.preview_templates = {}
|
|
36
|
+
# Proc to inject preview data: ->(page_key, view_context) { { @products => [] } }
|
|
37
|
+
config.ruby_cms.preview_data = ->(_page_key, _view) { {} }
|
|
38
|
+
# Optional: audit edits from the visual editor. ->(content_block_id, user_id, changes) { }
|
|
39
|
+
config.ruby_cms.audit_editor_edit = nil
|
|
40
|
+
|
|
41
|
+
# Content blocks: reserved key prefixes (e.g. "admin_") cannot be used.
|
|
42
|
+
config.ruby_cms.reserved_key_prefixes = %w[admin_]
|
|
43
|
+
# Content blocks: default translation namespace
|
|
44
|
+
# (e.g., "content_blocks" or "cms")
|
|
45
|
+
# When set, content_block helper will try translations
|
|
46
|
+
# at namespace.key before root-level key
|
|
47
|
+
# Example: If namespace is "content_blocks",
|
|
48
|
+
# it tries "content_blocks.home_hero_title" then "home_hero_title"
|
|
49
|
+
config.ruby_cms.content_blocks_translation_namespace = nil
|
|
50
|
+
# Image attachment: allowed content types and max size.
|
|
51
|
+
config.ruby_cms.image_content_types = %w[image/png image/jpeg image/gif image/webp]
|
|
52
|
+
# Keep this numeric so engine boot does not depend on core-ext load order.
|
|
53
|
+
config.ruby_cms.image_max_size = 5 * 1024 * 1024
|
|
54
|
+
|
|
55
|
+
# Ensure Ahoy is loaded before host's config/initializers/ahoy.rb runs
|
|
56
|
+
initializer "ruby_cms.require_ahoy", before: :load_config_initializers do
|
|
57
|
+
require "ahoy_matey"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
initializer "ruby_cms.i18n" do |app|
|
|
61
|
+
app.config.i18n.load_path += Dir[config.root.join("config", "locales", "**", "*.yml")]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
initializer "ruby_cms.helpers" do
|
|
65
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
66
|
+
helper RubyCms::ApplicationHelper
|
|
67
|
+
helper RubyCms::ContentBlocksHelper
|
|
68
|
+
helper RubyCms::SettingsHelper
|
|
69
|
+
helper RubyCms::BulkActionTableHelper
|
|
70
|
+
helper RubyCms::Admin::BulkActionTableHelper
|
|
71
|
+
helper RubyCms::Admin::AdminPageHelper
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
initializer "ruby_cms.assets", before: :load_config_initializers do |app|
|
|
76
|
+
# Add JavaScript controllers to asset pipeline (before importmap resolves)
|
|
77
|
+
app.config.assets.paths.unshift(config.root.join("app/javascript")) if app.config.respond_to?(:assets)
|
|
78
|
+
# Add stylesheets to asset pipeline
|
|
79
|
+
app.config.assets.paths << config.root.join("app/assets/stylesheets") if app.config.respond_to?(:assets)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
initializer "ruby_cms.importmap", before: "importmap" do |app|
|
|
83
|
+
# For importmap: ensure engine's importmap is loaded
|
|
84
|
+
if app.config.respond_to?(:importmap)
|
|
85
|
+
app.config.importmap.paths << config.root.join("config/importmap.rb")
|
|
86
|
+
app.config.importmap.cache_sweepers << config.root.join("app/javascript")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
initializer "ruby_cms.nav" do
|
|
91
|
+
Rails.application.config.to_prepare do
|
|
92
|
+
RubyCms::Engine.register_main_nav_items
|
|
93
|
+
RubyCms::Engine.register_settings_nav_items
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
initializer "ruby_cms.settings_import", after: :load_config_initializers do
|
|
98
|
+
RubyCms::Settings.import_initializer_values!
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.register_main_nav_items
|
|
102
|
+
RubyCms.nav_register(
|
|
103
|
+
key: :dashboard,
|
|
104
|
+
label: "Dashboard",
|
|
105
|
+
path: lambda(&:ruby_cms_admin_root_path),
|
|
106
|
+
icon: dashboard_icon_path,
|
|
107
|
+
section: RubyCms::NAV_SECTION_MAIN,
|
|
108
|
+
order: 1
|
|
109
|
+
)
|
|
110
|
+
RubyCms.nav_register(
|
|
111
|
+
key: :visual_editor,
|
|
112
|
+
label: "Visual editor",
|
|
113
|
+
path: lambda(&:ruby_cms_admin_visual_editor_path),
|
|
114
|
+
icon: visual_editor_icon_path,
|
|
115
|
+
section: RubyCms::NAV_SECTION_MAIN,
|
|
116
|
+
order: 2
|
|
117
|
+
)
|
|
118
|
+
RubyCms.nav_register(
|
|
119
|
+
key: :content_blocks,
|
|
120
|
+
label: "Content blocks",
|
|
121
|
+
path: lambda(&:ruby_cms_admin_content_blocks_path),
|
|
122
|
+
icon: content_blocks_icon_path,
|
|
123
|
+
section: RubyCms::NAV_SECTION_MAIN,
|
|
124
|
+
order: 3
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.dashboard_icon_path
|
|
129
|
+
# Heroicons HomeIcon (outline)
|
|
130
|
+
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
|
|
131
|
+
'd="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3' \
|
|
132
|
+
'm-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>'
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.content_blocks_icon_path
|
|
136
|
+
# Heroicons DocumentDuplicateIcon (outline)
|
|
137
|
+
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
|
|
138
|
+
'd="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414' \
|
|
139
|
+
'a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 ' \
|
|
140
|
+
'0 002-2v-2"></path>'
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def self.visual_editor_icon_path
|
|
144
|
+
# Heroicons PencilSquareIcon (outline)
|
|
145
|
+
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
|
|
146
|
+
'd="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 ' \
|
|
147
|
+
'0 012.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>'
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.register_settings_nav_items
|
|
151
|
+
RubyCms.nav_register(
|
|
152
|
+
key: :analytics,
|
|
153
|
+
label: "Analytics",
|
|
154
|
+
path: lambda(&:ruby_cms_admin_analytics_path),
|
|
155
|
+
section: RubyCms::NAV_SECTION_BOTTOM,
|
|
156
|
+
icon: analytics_icon_path,
|
|
157
|
+
permission: :manage_analytics,
|
|
158
|
+
order: 1
|
|
159
|
+
)
|
|
160
|
+
RubyCms.nav_register(
|
|
161
|
+
key: :permissions,
|
|
162
|
+
label: "Permissions",
|
|
163
|
+
path: lambda(&:ruby_cms_admin_permissions_path),
|
|
164
|
+
section: RubyCms::NAV_SECTION_BOTTOM,
|
|
165
|
+
icon: permissions_icon_path,
|
|
166
|
+
order: 2
|
|
167
|
+
)
|
|
168
|
+
RubyCms.nav_register(
|
|
169
|
+
key: :visitor_errors,
|
|
170
|
+
label: "Visitor errors",
|
|
171
|
+
path: lambda(&:ruby_cms_admin_visitor_errors_path),
|
|
172
|
+
section: RubyCms::NAV_SECTION_BOTTOM,
|
|
173
|
+
icon: visitor_errors_icon_path,
|
|
174
|
+
order: 3
|
|
175
|
+
)
|
|
176
|
+
RubyCms.nav_register(
|
|
177
|
+
key: :users,
|
|
178
|
+
label: "Users",
|
|
179
|
+
path: lambda(&:ruby_cms_admin_users_path),
|
|
180
|
+
section: RubyCms::NAV_SECTION_BOTTOM,
|
|
181
|
+
icon: users_icon_path,
|
|
182
|
+
order: 4
|
|
183
|
+
)
|
|
184
|
+
RubyCms.nav_register(
|
|
185
|
+
key: :settings,
|
|
186
|
+
label: "Settings",
|
|
187
|
+
path: lambda(&:ruby_cms_admin_settings_path),
|
|
188
|
+
section: RubyCms::NAV_SECTION_BOTTOM,
|
|
189
|
+
icon: settings_icon_path,
|
|
190
|
+
order: 5
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def self.settings_icon_path
|
|
195
|
+
# Heroicons Cog6ToothIcon (outline)
|
|
196
|
+
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
|
|
197
|
+
'd="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 ' \
|
|
198
|
+
'1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 ' \
|
|
199
|
+
'1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 ' \
|
|
200
|
+
'6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 ' \
|
|
201
|
+
'0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 ' \
|
|
202
|
+
'1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 ' \
|
|
203
|
+
'6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 ' \
|
|
204
|
+
'1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 ' \
|
|
205
|
+
'1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"></path>' \
|
|
206
|
+
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>'
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def self.visitor_errors_icon_path
|
|
210
|
+
# Heroicons ExclamationTriangleIcon (outline)
|
|
211
|
+
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
|
|
212
|
+
'd="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 ' \
|
|
213
|
+
'4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>'
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def self.permissions_icon_path
|
|
217
|
+
# Heroicons ShieldCheckIcon (outline)
|
|
218
|
+
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
|
|
219
|
+
'd="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01' \
|
|
220
|
+
'-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 ' \
|
|
221
|
+
'9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>'
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def self.users_icon_path
|
|
225
|
+
# Heroicons UserGroupIcon (outline)
|
|
226
|
+
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
|
|
227
|
+
'd="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 ' \
|
|
228
|
+
'00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>'
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def self.analytics_icon_path
|
|
232
|
+
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
|
|
233
|
+
'd="M3 3v18h18"></path>' \
|
|
234
|
+
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
|
|
235
|
+
'd="M7 13l3-3 3 2 4-5"></path>'
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def self.compile_admin_css(dest_path)
|
|
239
|
+
gem_root = begin
|
|
240
|
+
root
|
|
241
|
+
rescue StandardError
|
|
242
|
+
Pathname.new(File.expand_path("../..", __dir__))
|
|
243
|
+
end
|
|
244
|
+
RubyCms::CssCompiler.compile(gem_root, dest_path)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
config.paths.add "db/migrate", with: "db/migrate"
|
|
248
|
+
|
|
249
|
+
rake_tasks do # rubocop:disable Metrics/BlockLength
|
|
250
|
+
namespace :ruby_cms do # rubocop:disable Metrics/BlockLength
|
|
251
|
+
desc "Create default permissions/settings and optionally grant manage_admin to admin users"
|
|
252
|
+
task seed_permissions: :environment do
|
|
253
|
+
RubyCms::Permission.ensure_defaults!
|
|
254
|
+
RubyCms::Settings.ensure_defaults!
|
|
255
|
+
RubyCms::Settings.import_initializer_values!
|
|
256
|
+
RubyCms::Engine.grant_admin_permissions_to_admin_users
|
|
257
|
+
end
|
|
258
|
+
desc "Import RubyCMS initializer values into DB settings once"
|
|
259
|
+
task import_initializer_settings: :environment do
|
|
260
|
+
result = RubyCms::Settings.import_initializer_values!
|
|
261
|
+
if result[:skipped]
|
|
262
|
+
puts "Initializer import skipped: #{result[:reason]}" # rubocop:disable Rails/Output
|
|
263
|
+
else
|
|
264
|
+
puts "Imported #{result[:imported_count]} initializer setting(s)." # rubocop:disable Rails/Output
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
desc "Interactively create or select the first admin user " \
|
|
269
|
+
"and grant full permissions (manage_admin, manage_permissions, " \
|
|
270
|
+
"manage_content_blocks, etc.)"
|
|
271
|
+
task setup_admin: :environment do
|
|
272
|
+
require "ruby_cms/cli"
|
|
273
|
+
RubyCms::RunSetupAdmin.call(shell: Thor::Shell::Basic.new)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
desc "Grant manage_admin to a user by email. " \
|
|
277
|
+
"Usage: rails ruby_cms:grant_manage_admin email=user@example.com"
|
|
278
|
+
task :grant_manage_admin, [:email] => :environment do |_t, args|
|
|
279
|
+
email = RubyCms::Engine.extract_email_from_args(args)
|
|
280
|
+
RubyCms::Engine.validate_email_present(email)
|
|
281
|
+
|
|
282
|
+
RubyCms::Permission.ensure_defaults!
|
|
283
|
+
user = RubyCms::Engine.find_user_by_email(email)
|
|
284
|
+
RubyCms::Engine.validate_user_found(user, email)
|
|
285
|
+
|
|
286
|
+
RubyCms::Engine.grant_manage_admin_permission(user, email)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
namespace :content_blocks do # rubocop:disable Metrics/BlockLength
|
|
290
|
+
desc "Export content blocks from database to YAML locale files"
|
|
291
|
+
task :export, %i[namespace locales_dir] => :environment do |_t, args|
|
|
292
|
+
require "ruby_cms/content_blocks_sync"
|
|
293
|
+
|
|
294
|
+
namespace = args[:namespace].presence
|
|
295
|
+
locales_dir = RubyCms::Engine.parse_locales_dir(args[:locales_dir])
|
|
296
|
+
flatten = ENV["flatten"] == "true"
|
|
297
|
+
|
|
298
|
+
sync = RubyCms::ContentBlocksSync.new(namespace:, locales_dir:)
|
|
299
|
+
summary = sync.export_to_yaml(only_published: true, flatten_keys: flatten)
|
|
300
|
+
|
|
301
|
+
RubyCms::Engine.display_export_summary(summary)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
desc "Import content blocks from YAML locale files to database"
|
|
305
|
+
task :import, %i[locale namespace locales_dir] => :environment do |_t, args|
|
|
306
|
+
require "ruby_cms/content_blocks_sync"
|
|
307
|
+
|
|
308
|
+
locale = args[:locale].presence&.to_sym
|
|
309
|
+
namespace = args[:namespace].presence ||
|
|
310
|
+
Rails.application.config.ruby_cms.content_blocks_translation_namespace
|
|
311
|
+
locales_dir = RubyCms::Engine.parse_locales_dir(args[:locales_dir]) ||
|
|
312
|
+
Rails.root.join("config/locales")
|
|
313
|
+
import_options = RubyCms::Engine.parse_import_options
|
|
314
|
+
|
|
315
|
+
sync = RubyCms::ContentBlocksSync.new(namespace:, locales_dir:)
|
|
316
|
+
summary = sync.import_from_yaml(locale:, **import_options)
|
|
317
|
+
|
|
318
|
+
RubyCms::Engine.display_import_summary(summary)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
desc "Sync content blocks: export DB to YAML, optionally import from YAML"
|
|
322
|
+
task :sync, %i[namespace locales_dir] => :environment do |_t, args|
|
|
323
|
+
require "ruby_cms/content_blocks_sync"
|
|
324
|
+
|
|
325
|
+
namespace = args[:namespace].presence
|
|
326
|
+
locales_dir = RubyCms::Engine.parse_locales_dir(args[:locales_dir])
|
|
327
|
+
import_after = ENV["import_after"] == "true"
|
|
328
|
+
|
|
329
|
+
sync = RubyCms::ContentBlocksSync.new(namespace:, locales_dir:)
|
|
330
|
+
result = sync.sync(import_after_export: import_after)
|
|
331
|
+
|
|
332
|
+
RubyCms::Engine.display_sync_summary(result, import_after)
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
namespace :css do
|
|
337
|
+
desc "Compile RubyCMS admin.css from component files (for gem development)"
|
|
338
|
+
task compile_gem: :environment do
|
|
339
|
+
dest = RubyCms::Engine.root.join("app/assets/stylesheets/ruby_cms/admin.css")
|
|
340
|
+
RubyCms::Engine.compile_admin_css(dest)
|
|
341
|
+
puts "✓ Compiled admin.css in gem" # rubocop:disable Rails/Output
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
desc "Compile RubyCMS CSS to host app (combines component files)"
|
|
345
|
+
task compile: :environment do
|
|
346
|
+
require "fileutils"
|
|
347
|
+
dest_dir = Rails.root.join("app/assets/stylesheets/ruby_cms")
|
|
348
|
+
FileUtils.mkdir_p(dest_dir)
|
|
349
|
+
dest = dest_dir.join("admin.css")
|
|
350
|
+
RubyCms::Engine.compile_admin_css(dest)
|
|
351
|
+
puts "✓ Compiled admin.css to #{dest}" # rubocop:disable Rails/Output
|
|
352
|
+
puts "✓ RubyCMS CSS compilation complete!" # rubocop:disable Rails/Output
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def self.grant_admin_permissions_to_admin_users
|
|
359
|
+
return unless defined?(::User) && User.column_names.include?("admin")
|
|
360
|
+
|
|
361
|
+
permission_keys = %w[
|
|
362
|
+
manage_admin
|
|
363
|
+
manage_permissions
|
|
364
|
+
manage_content_blocks
|
|
365
|
+
manage_visitor_errors
|
|
366
|
+
manage_analytics
|
|
367
|
+
]
|
|
368
|
+
permissions = RubyCms::Permission.where(key: permission_keys).index_by(&:key)
|
|
369
|
+
User.where(admin: true).find_each do |u|
|
|
370
|
+
permission_keys.each do |key|
|
|
371
|
+
perm = permissions[key]
|
|
372
|
+
next if perm.nil?
|
|
373
|
+
|
|
374
|
+
RubyCms::UserPermission.find_or_create_by!(user: u, permission: perm)
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def self.extract_email_from_args(args)
|
|
380
|
+
args[:email] || ENV["email"] || ENV.fetch("EMAIL", nil)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def self.validate_email_present(email)
|
|
384
|
+
return if email.present?
|
|
385
|
+
|
|
386
|
+
warn "Usage: rails ruby_cms:grant_manage_admin " \
|
|
387
|
+
"email=user@example.com"
|
|
388
|
+
raise "Email is required"
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def self.find_user_by_email(email)
|
|
392
|
+
user_class = Rails.application.config.ruby_cms.user_class_name
|
|
393
|
+
.constantize
|
|
394
|
+
find_user_by_email_address(user_class, email) ||
|
|
395
|
+
find_user_by_email_column(user_class, email)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def self.find_user_by_email_address(user_class, email)
|
|
399
|
+
return unless user_class.column_names.include?("email_address")
|
|
400
|
+
|
|
401
|
+
user_class.find_by(email_address: email)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def self.find_user_by_email_column(user_class, email)
|
|
405
|
+
return unless user_class.column_names.include?("email")
|
|
406
|
+
|
|
407
|
+
user_class.find_by(email:)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def self.validate_user_found(user, email)
|
|
411
|
+
return if user
|
|
412
|
+
|
|
413
|
+
warn "User not found: #{email}"
|
|
414
|
+
raise "User not found: #{email}"
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def self.grant_manage_admin_permission(user, email)
|
|
418
|
+
RubyCms::Permission.ensure_defaults!
|
|
419
|
+
%w[
|
|
420
|
+
manage_admin manage_permissions manage_content_blocks manage_visitor_errors
|
|
421
|
+
manage_analytics
|
|
422
|
+
].each do |key|
|
|
423
|
+
perm = RubyCms::Permission.find_by!(key:)
|
|
424
|
+
RubyCms::UserPermission.find_or_create_by!(user: user, permission: perm)
|
|
425
|
+
end
|
|
426
|
+
puts "Granted full admin permissions to #{email}" # rubocop:disable Rails/Output
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def self.parse_locales_dir(locales_dir_arg)
|
|
430
|
+
return nil unless locales_dir_arg.presence
|
|
431
|
+
|
|
432
|
+
Pathname.new(locales_dir_arg)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def self.parse_import_options
|
|
436
|
+
{
|
|
437
|
+
create_missing: ENV["create_missing"] != "false",
|
|
438
|
+
update_existing: ENV["update_existing"] != "false",
|
|
439
|
+
published: ENV["published"] == "true"
|
|
440
|
+
}
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def self.display_export_summary(summary)
|
|
444
|
+
if summary.empty?
|
|
445
|
+
puts "No content blocks found to export." # rubocop:disable Rails/Output
|
|
446
|
+
else
|
|
447
|
+
puts "Exported content blocks to locale files:" # rubocop:disable Rails/Output
|
|
448
|
+
summary.each do |locale, count|
|
|
449
|
+
# rubocop:disable Rails/Output
|
|
450
|
+
puts " #{locale}: #{count} block(s) updated " \
|
|
451
|
+
"in config/locales/#{locale}.yml"
|
|
452
|
+
# rubocop:enable Rails/Output
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def self.display_import_summary(summary)
|
|
458
|
+
$stdout.puts "Import summary:"
|
|
459
|
+
$stdout.puts " Created: #{summary[:created]}"
|
|
460
|
+
$stdout.puts " Updated: #{summary[:updated]}"
|
|
461
|
+
$stdout.puts " Skipped: #{summary[:skipped]}"
|
|
462
|
+
return unless summary[:errors].any?
|
|
463
|
+
|
|
464
|
+
$stdout.puts " Errors:"
|
|
465
|
+
summary[:errors].each {|e| $stdout.puts " - #{e}" }
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def self.display_sync_summary(result, import_after)
|
|
469
|
+
display_export_results(result[:export])
|
|
470
|
+
display_import_results(result[:import], import_after) if import_after
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def self.display_export_results(export_data)
|
|
474
|
+
$stdout.puts "Sync complete!"
|
|
475
|
+
$stdout.puts "\nExport summary:"
|
|
476
|
+
export_data.each do |locale, count|
|
|
477
|
+
$stdout.puts " #{locale}: #{count} block(s) updated"
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def self.display_import_results(import_data, import_after)
|
|
482
|
+
return unless import_after && import_data.any?
|
|
483
|
+
|
|
484
|
+
$stdout.puts "\nImport summary:"
|
|
485
|
+
$stdout.puts " Created: #{import_data[:created]}"
|
|
486
|
+
$stdout.puts " Updated: #{import_data[:updated]}"
|
|
487
|
+
$stdout.puts " Skipped: #{import_data[:skipped]}"
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
initializer "ruby_cms.load_migrations" do |app|
|
|
491
|
+
next unless app.config.respond_to?(:paths)
|
|
492
|
+
|
|
493
|
+
config.paths["db/migrate"].expanded.each do |path|
|
|
494
|
+
app.config.paths["db/migrate"] << path
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "settings_registry"
|
|
4
|
+
|
|
5
|
+
module RubyCms
|
|
6
|
+
module Settings
|
|
7
|
+
IMPORT_SENTINEL_KEY = "__internal_initializer_import_v1"
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def get(key, default: nil)
|
|
11
|
+
k = key.to_s
|
|
12
|
+
pref = fetch_preference(k)
|
|
13
|
+
return pref.typed_value if pref
|
|
14
|
+
|
|
15
|
+
entry = RubyCms::SettingsRegistry.fetch(k)
|
|
16
|
+
return entry.default unless entry.nil?
|
|
17
|
+
|
|
18
|
+
default
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def set(key, value)
|
|
22
|
+
raise "Settings table not available yet" unless preference_table_available?
|
|
23
|
+
|
|
24
|
+
k = key.to_s
|
|
25
|
+
entry = RubyCms::SettingsRegistry.fetch(k)
|
|
26
|
+
coerced = coerce_by_entry(value, entry)
|
|
27
|
+
|
|
28
|
+
RubyCms::Preference.set(k, coerced)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def ensure_defaults!
|
|
32
|
+
RubyCms::SettingsRegistry.seed_defaults!
|
|
33
|
+
RubyCms::Preference.ensure_defaults!
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def all
|
|
37
|
+
return {} unless preference_table_available?
|
|
38
|
+
|
|
39
|
+
RubyCms::Preference.all_as_hash
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Imports initializer values for any key that exists in SettingsRegistry
|
|
43
|
+
# and is explicitly set on config.ruby_cms.
|
|
44
|
+
def import_initializer_values!(force: false)
|
|
45
|
+
return skipped_result("preferences table unavailable") unless preference_table_available?
|
|
46
|
+
|
|
47
|
+
ensure_defaults!
|
|
48
|
+
|
|
49
|
+
return skipped_result("already imported") if imported_from_initializer? && !force
|
|
50
|
+
|
|
51
|
+
config = ruby_cms_config
|
|
52
|
+
return skipped_result("ruby_cms config unavailable") if config.nil?
|
|
53
|
+
|
|
54
|
+
imported_keys = []
|
|
55
|
+
|
|
56
|
+
RubyCms::SettingsRegistry.each do |entry|
|
|
57
|
+
key = entry.key.to_sym
|
|
58
|
+
next unless config.respond_to?(key)
|
|
59
|
+
|
|
60
|
+
value = config.public_send(key)
|
|
61
|
+
next if value.nil?
|
|
62
|
+
|
|
63
|
+
set(key, value)
|
|
64
|
+
imported_keys << entry.key
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
mark_imported!(imported_keys)
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
skipped: false,
|
|
71
|
+
imported_count: imported_keys.size,
|
|
72
|
+
imported_keys: imported_keys
|
|
73
|
+
}
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
skipped_result(e.message)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def imported_from_initializer?
|
|
79
|
+
return false unless preference_table_available?
|
|
80
|
+
|
|
81
|
+
RubyCms::Preference.exists?(key: IMPORT_SENTINEL_KEY)
|
|
82
|
+
rescue StandardError
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def skipped_result(reason)
|
|
89
|
+
{ skipped: true, reason: reason, imported_count: 0 }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def ruby_cms_config
|
|
93
|
+
return nil unless defined?(Rails) && Rails.application.config.respond_to?(:ruby_cms)
|
|
94
|
+
|
|
95
|
+
Rails.application.config.ruby_cms
|
|
96
|
+
rescue StandardError
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def mark_imported!(imported_keys)
|
|
101
|
+
RubyCms::Preference.set(
|
|
102
|
+
IMPORT_SENTINEL_KEY,
|
|
103
|
+
{
|
|
104
|
+
version: 1,
|
|
105
|
+
imported_at: Time.current.iso8601,
|
|
106
|
+
imported_keys: imported_keys
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def fetch_preference(key)
|
|
112
|
+
return nil unless preference_table_available?
|
|
113
|
+
|
|
114
|
+
RubyCms::Preference.find_by(key:)
|
|
115
|
+
rescue StandardError
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def preference_table_available?
|
|
120
|
+
return false unless defined?(ActiveRecord::Base) && defined?(RubyCms::Preference)
|
|
121
|
+
|
|
122
|
+
ActiveRecord::Base.connection.data_source_exists?("preferences")
|
|
123
|
+
rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
|
|
124
|
+
false
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def coerce_by_entry(value, entry)
|
|
128
|
+
return value if entry.nil?
|
|
129
|
+
|
|
130
|
+
case entry.type
|
|
131
|
+
when :integer
|
|
132
|
+
value.to_i
|
|
133
|
+
when :boolean
|
|
134
|
+
ActiveModel::Type::Boolean.new.cast(value)
|
|
135
|
+
when :json
|
|
136
|
+
value.kind_of?(String) ? JSON.parse(value) : value
|
|
137
|
+
else
|
|
138
|
+
value.to_s
|
|
139
|
+
end
|
|
140
|
+
rescue JSON::ParserError
|
|
141
|
+
value
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|