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,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
module AdminTurboTable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
# Check if this is a Turbo Frame request
|
|
8
|
+
# @return [Boolean]
|
|
9
|
+
def turbo_frame_request?
|
|
10
|
+
request.headers["Turbo-Frame"].present?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Render index action with Turbo Frame support
|
|
14
|
+
# If Turbo Frame request, renders only the table content
|
|
15
|
+
# Otherwise renders full page
|
|
16
|
+
# @param turbo_frame_id [String] Turbo Frame ID (default: "admin_table_content")
|
|
17
|
+
def turbo_render_index(turbo_frame_id: "admin_table_content")
|
|
18
|
+
if turbo_frame_request?
|
|
19
|
+
render partial: turbo_frame_id, layout: false
|
|
20
|
+
else
|
|
21
|
+
render :index
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Redirect with Turbo Frame support
|
|
26
|
+
# If Turbo Frame request, renders Turbo Stream redirect
|
|
27
|
+
# Otherwise performs normal redirect
|
|
28
|
+
# @param url [String] URL to redirect to
|
|
29
|
+
# @param options [Hash] Redirect options (notice, alert, etc.)
|
|
30
|
+
def turbo_redirect_to(url, **)
|
|
31
|
+
if turbo_frame_request?
|
|
32
|
+
# For Turbo Frame requests, we can't redirect directly
|
|
33
|
+
# Instead, we should render a Turbo Stream that updates the frame
|
|
34
|
+
# or redirect the parent window
|
|
35
|
+
else
|
|
36
|
+
redirect_to(url, **)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Get Turbo Frame ID from request
|
|
41
|
+
# @return [String, nil]
|
|
42
|
+
def turbo_frame_id
|
|
43
|
+
request.headers["Turbo-Frame"]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Check if request expects Turbo Stream response
|
|
47
|
+
# @return [Boolean]
|
|
48
|
+
def turbo_stream_request?
|
|
49
|
+
request.headers["Accept"]&.include?("text/vnd.turbo-stream.html")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Render Turbo Stream update for table
|
|
53
|
+
# @param turbo_frame_id [String] Turbo Frame ID
|
|
54
|
+
# @param partial [String] Partial to render (default: turbo_frame_id)
|
|
55
|
+
def turbo_stream_update_table(turbo_frame_id: "admin_table_content", partial: nil)
|
|
56
|
+
partial ||= turbo_frame_id
|
|
57
|
+
render turbo_stream: turbo_stream.update(turbo_frame_id, partial:)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Render Turbo Stream replace for table
|
|
61
|
+
# @param turbo_frame_id [String] Turbo Frame ID
|
|
62
|
+
# @param partial [String] Partial to render (default: turbo_frame_id)
|
|
63
|
+
def turbo_stream_replace_table(turbo_frame_id: "admin_table_content", partial: nil)
|
|
64
|
+
partial ||= turbo_frame_id
|
|
65
|
+
render turbo_stream: turbo_stream.replace(turbo_frame_id, partial:)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
# Include in controllers to track page views via Ahoy.
|
|
5
|
+
# Requires the host app to have Ahoy installed (via RubyCMS install generator).
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# class PagesController < ApplicationController
|
|
9
|
+
# include RubyCms::PageTracking
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# Sets @page_name to controller_name by default. Override in actions:
|
|
13
|
+
# @page_name = "custom_page_name"
|
|
14
|
+
#
|
|
15
|
+
module PageTracking
|
|
16
|
+
extend ActiveSupport::Concern
|
|
17
|
+
|
|
18
|
+
included do
|
|
19
|
+
before_action :set_page_name
|
|
20
|
+
after_action :track_page_view
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def set_page_name
|
|
26
|
+
@page_name = controller_name if @page_name.blank?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def track_page_view
|
|
30
|
+
return unless should_track_page_view?
|
|
31
|
+
|
|
32
|
+
ahoy.track "page_view",
|
|
33
|
+
page_name: @page_name,
|
|
34
|
+
request_path: request.path
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
Rails.logger.error "[RubyCMS] Failed to track page view: #{e.message}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def should_track_page_view?
|
|
40
|
+
# Only track if @page_name is set
|
|
41
|
+
return false if @page_name.blank?
|
|
42
|
+
|
|
43
|
+
# Skip admin paths
|
|
44
|
+
return false if request.path.start_with?("/admin")
|
|
45
|
+
|
|
46
|
+
# Skip Turbo frame requests (optional - adjust based on needs)
|
|
47
|
+
return false if request.headers["Turbo-Frame"].present?
|
|
48
|
+
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
# Include in ApplicationController to capture public-site errors to VisitorError.
|
|
5
|
+
# Skips admin controllers (paths under /admin) and development environment by default.
|
|
6
|
+
#
|
|
7
|
+
# Usage in ApplicationController:
|
|
8
|
+
# include RubyCms::VisitorErrorCapture
|
|
9
|
+
# rescue_from StandardError, with: :handle_visitor_error
|
|
10
|
+
#
|
|
11
|
+
# Or use the class method to add both:
|
|
12
|
+
# RubyCms::VisitorErrorCapture.install(self)
|
|
13
|
+
module VisitorErrorCapture
|
|
14
|
+
extend ActiveSupport::Concern
|
|
15
|
+
|
|
16
|
+
class_methods do
|
|
17
|
+
def install(controller_class)
|
|
18
|
+
controller_class.include RubyCms::VisitorErrorCapture
|
|
19
|
+
controller_class.rescue_from StandardError, with: :handle_visitor_error
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def handle_visitor_error(exception)
|
|
26
|
+
return if skip_visitor_error_capture?
|
|
27
|
+
|
|
28
|
+
RubyCms::VisitorError.log_error(exception, request)
|
|
29
|
+
ensure
|
|
30
|
+
raise exception
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def skip_visitor_error_capture?
|
|
34
|
+
return true if Rails.env.development?
|
|
35
|
+
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
module RubyCms
|
|
6
|
+
module Admin
|
|
7
|
+
class AnalyticsController < BaseController
|
|
8
|
+
before_action { require_permission!(:manage_analytics) }
|
|
9
|
+
before_action :set_date_range
|
|
10
|
+
before_action :validate_date_range
|
|
11
|
+
|
|
12
|
+
def index
|
|
13
|
+
report = RubyCms::Analytics::Report.new(
|
|
14
|
+
start_date: @start_date,
|
|
15
|
+
end_date: @end_date,
|
|
16
|
+
period: @period
|
|
17
|
+
)
|
|
18
|
+
@stats = report.dashboard_stats
|
|
19
|
+
@stats.each {|key, value| instance_variable_set(:"@#{key}", value) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def page_details
|
|
23
|
+
@page_name = sanitize_page_name(params[:page_name])
|
|
24
|
+
unless @page_name
|
|
25
|
+
return redirect_to ruby_cms_admin_analytics_path,
|
|
26
|
+
alert: t("ruby_cms.admin.analytics.invalid_page_name",
|
|
27
|
+
default: "Invalid page name.")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
report = RubyCms::Analytics::Report.new(
|
|
31
|
+
start_date: @start_date,
|
|
32
|
+
end_date: @end_date,
|
|
33
|
+
period: @period
|
|
34
|
+
)
|
|
35
|
+
data = report.page_stats(@page_name)
|
|
36
|
+
@page_views = data[:page_views]
|
|
37
|
+
@page_stats = data[:stats]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def visitor_details
|
|
41
|
+
@ip_address = sanitize_ip_address(params[:ip_address])
|
|
42
|
+
unless @ip_address
|
|
43
|
+
return redirect_to ruby_cms_admin_analytics_path,
|
|
44
|
+
alert: t("ruby_cms.admin.analytics.invalid_ip_address",
|
|
45
|
+
default: "Invalid IP address.")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
report = RubyCms::Analytics::Report.new(
|
|
49
|
+
start_date: @start_date,
|
|
50
|
+
end_date: @end_date,
|
|
51
|
+
period: @period
|
|
52
|
+
)
|
|
53
|
+
data = report.visitor_stats(@ip_address)
|
|
54
|
+
@visitor_views = data[:visitor_views]
|
|
55
|
+
@visitor_stats = data[:stats]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def set_date_range
|
|
61
|
+
@period = sanitize_period(params[:period]) || default_period
|
|
62
|
+
|
|
63
|
+
@start_date, @end_date = parsed_date_range || default_date_range
|
|
64
|
+
rescue Date::Error
|
|
65
|
+
@start_date, @end_date = fallback_date_range
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def validate_date_range
|
|
69
|
+
max_days = RubyCms::Settings.get(:analytics_max_date_range_days, default: 365).to_i
|
|
70
|
+
return if valid_date_range?(max_days)
|
|
71
|
+
|
|
72
|
+
redirect_to ruby_cms_admin_analytics_path,
|
|
73
|
+
alert: "Invalid date range. Maximum range is #{max_days} days."
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def sanitize_period(value)
|
|
77
|
+
%w[day week month year].include?(value.to_s) ? value.to_s : nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def default_period
|
|
81
|
+
RubyCms::Settings.get(:analytics_default_period, default: "week").to_s
|
|
82
|
+
rescue StandardError
|
|
83
|
+
"week"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def period_start_date(period, end_date)
|
|
87
|
+
case period
|
|
88
|
+
when "day" then end_date
|
|
89
|
+
when "week" then end_date - 6.days
|
|
90
|
+
when "month" then end_date - 29.days
|
|
91
|
+
else end_date - 364.days
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def sanitize_page_name(page_name)
|
|
96
|
+
page_name.to_s.gsub(%r{[^a-zA-Z0-9_\-/]}, "").presence
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def sanitize_ip_address(ip_address)
|
|
100
|
+
return nil if ip_address.blank?
|
|
101
|
+
|
|
102
|
+
IPAddr.new(ip_address)
|
|
103
|
+
ip_address
|
|
104
|
+
rescue IPAddr::InvalidAddressError
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
helper_method :format_chart_date, :format_chart_date_short
|
|
109
|
+
|
|
110
|
+
def format_chart_date(date_string)
|
|
111
|
+
format_chart_date_by_granularity(date_string, long: true)
|
|
112
|
+
rescue Date::Error
|
|
113
|
+
date_string.to_s
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def format_chart_date_short(date_string)
|
|
117
|
+
format_chart_date_by_granularity(date_string, long: false)
|
|
118
|
+
rescue Date::Error
|
|
119
|
+
date_string.to_s
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def format_daily_date(date_string)
|
|
123
|
+
date = Date.parse(date_string.to_s)
|
|
124
|
+
if @period == "month"
|
|
125
|
+
end_date = [date + 2.days, @end_date].min
|
|
126
|
+
return "#{date.strftime('%b %d')} - #{end_date.strftime('%b %d')}" if date != end_date
|
|
127
|
+
end
|
|
128
|
+
date.strftime("%B %d, %Y")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def format_daily_date_short(date_string)
|
|
132
|
+
date = Date.parse(date_string.to_s)
|
|
133
|
+
if @period == "month"
|
|
134
|
+
end_date = [date + 2.days, @end_date].min
|
|
135
|
+
return "#{date.strftime('%m/%d')}-#{end_date.strftime('%m/%d')}" if date != end_date
|
|
136
|
+
end
|
|
137
|
+
date.strftime("%m/%d")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def parsed_date_range
|
|
141
|
+
return nil unless params[:start_date].present? && params[:end_date].present?
|
|
142
|
+
|
|
143
|
+
[
|
|
144
|
+
Date.parse(params[:start_date]),
|
|
145
|
+
Date.parse(params[:end_date])
|
|
146
|
+
]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def default_date_range
|
|
150
|
+
end_date = Date.current
|
|
151
|
+
[period_start_date(@period, end_date), end_date]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def fallback_date_range
|
|
155
|
+
end_date = Date.current
|
|
156
|
+
[end_date - 6.days, end_date]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def valid_date_range?(max_days)
|
|
160
|
+
return false unless @end_date.between?(@start_date, Date.current)
|
|
161
|
+
|
|
162
|
+
(@end_date - @start_date).to_i <= max_days
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def format_chart_date_by_granularity(date_string, long:)
|
|
166
|
+
str = date_string.to_s
|
|
167
|
+
|
|
168
|
+
if str.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
169
|
+
return long ? format_daily_date(str) : format_daily_date_short(str)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
if str.match?(/\A\d{4}-\d{2}\z/)
|
|
173
|
+
format_monthly_date(str, long:)
|
|
174
|
+
elsif str.match?(/\A\d{2}\z/)
|
|
175
|
+
format_hourly_date(str, long:)
|
|
176
|
+
else
|
|
177
|
+
str
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def format_monthly_date(date_string, long:)
|
|
182
|
+
date = Date.parse("#{date_string}-01")
|
|
183
|
+
long ? date.strftime("%B %Y") : date.strftime("%b")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def format_hourly_date(date_string, long:)
|
|
187
|
+
long ? "#{date_string}:00" : "#{date_string}h"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
module Admin
|
|
5
|
+
# Base for all /admin controllers. Ensures authentication and permission enforcement.
|
|
6
|
+
# Inherits from the host's ApplicationController (or config.admin_base_controller).
|
|
7
|
+
# This layout must not be used for public pages.
|
|
8
|
+
class BaseController < Rails.application.config.ruby_cms.admin_base_controller.constantize
|
|
9
|
+
layout -> { Rails.application.config.ruby_cms.admin_layout.presence || "admin/admin" }
|
|
10
|
+
before_action :set_cms_locale
|
|
11
|
+
before_action :require_cms_access
|
|
12
|
+
|
|
13
|
+
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
|
|
14
|
+
|
|
15
|
+
helper_method :current_user_cms, :require_permission!
|
|
16
|
+
|
|
17
|
+
# Expose the engine's route helpers (ruby_cms_admin_*_path) in controllers.
|
|
18
|
+
# The host's _routes does not expose the mounted engine's named routes to
|
|
19
|
+
# the engine's own controllers; we keep _routes as the host's for
|
|
20
|
+
# new_session_path, root_path, etc.
|
|
21
|
+
include RubyCms::Engine.routes.url_helpers
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def require_cms_access
|
|
26
|
+
ensure_authenticated
|
|
27
|
+
require_permission!(:manage_admin)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ensure_authenticated
|
|
31
|
+
return if current_user_cms
|
|
32
|
+
|
|
33
|
+
if respond_to?(:require_authentication, true)
|
|
34
|
+
send(:require_authentication)
|
|
35
|
+
else
|
|
36
|
+
redirect_to cms_redirect_path, alert: t("ruby_cms.admin.base.authentication_required")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Forbid (403) or redirect with flash. Default-deny: unknown permission = forbidden.
|
|
41
|
+
def require_permission!(permission_key, record: nil)
|
|
42
|
+
return if current_user_cms&.can?(permission_key, record:)
|
|
43
|
+
|
|
44
|
+
respond_to do |format|
|
|
45
|
+
format.html do
|
|
46
|
+
redirect_to(cms_redirect_path, alert: t("ruby_cms.admin.base.not_authorized"))
|
|
47
|
+
end
|
|
48
|
+
format.any { head :forbidden }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Optional role gate. Example: before_action { require_role!(:admin) } in a subclass.
|
|
53
|
+
# Uses user.admin? when role is :admin (if the host's User responds to :admin?).
|
|
54
|
+
def require_role!(role)
|
|
55
|
+
return if current_user_cms.nil?
|
|
56
|
+
return if role == :admin && current_user_cms.respond_to?(:admin?) && current_user_cms.admin?
|
|
57
|
+
|
|
58
|
+
respond_to do |format|
|
|
59
|
+
format.html do
|
|
60
|
+
redirect_to cms_redirect_path, alert: t("ruby_cms.admin.base.not_authorized")
|
|
61
|
+
end
|
|
62
|
+
format.any { head :forbidden }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def cms_redirect_path
|
|
67
|
+
Rails.application.config.ruby_cms.unauthorized_redirect_path.presence || "/"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def current_user_cms
|
|
71
|
+
@current_user_cms ||= resolve_current_user
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def resolve_current_user
|
|
75
|
+
if respond_to?(:current_user, true)
|
|
76
|
+
send(:current_user)
|
|
77
|
+
else
|
|
78
|
+
Rails.application.config.ruby_cms.current_user_resolver&.call(self)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def render_not_found
|
|
83
|
+
render "ruby_cms/errors/not_found",
|
|
84
|
+
status: :not_found,
|
|
85
|
+
layout: Rails.application.config.ruby_cms.admin_layout.presence || "admin/admin"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def set_cms_locale
|
|
89
|
+
if session[:ruby_cms_locale].present? &&
|
|
90
|
+
I18n.available_locales.include?(session[:ruby_cms_locale].to_sym)
|
|
91
|
+
I18n.locale = session[:ruby_cms_locale].to_sym
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Resolve parameter key for model params
|
|
96
|
+
# Checks if a specific key exists in params, otherwise falls back to model's param_key
|
|
97
|
+
# @param model_class [Class] The model class (e.g., ContentBlock)
|
|
98
|
+
# @param param_name [Symbol] The expected parameter name (e.g., :page)
|
|
99
|
+
# @return [Symbol] The resolved parameter key
|
|
100
|
+
def model_param_key(model_class, param_name)
|
|
101
|
+
params.key?(param_name) ? param_name : model_class.model_name.param_key.to_sym
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|