railspress-engine 0.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +20 -0
- data/README.md +195 -25
- data/app/assets/javascripts/railspress/admin.js +39 -0
- data/app/assets/javascripts/railspress/markdown_mode.js +343 -0
- data/app/assets/stylesheets/application.css +0 -0
- data/app/assets/stylesheets/railspress/admin/badges.css +70 -0
- data/app/assets/stylesheets/railspress/admin/base.css +25 -0
- data/app/assets/stylesheets/railspress/admin/buttons.css +140 -0
- data/app/assets/stylesheets/railspress/admin/cards.css +52 -0
- data/app/assets/stylesheets/railspress/admin/components/exports.css +55 -0
- data/app/assets/stylesheets/railspress/admin/components/focal_point.css +801 -0
- data/app/assets/stylesheets/railspress/admin/components/imports.css +144 -0
- data/app/assets/stylesheets/railspress/admin/components/lexxy.css +156 -0
- data/app/assets/stylesheets/railspress/admin/filters.css +73 -0
- data/app/assets/stylesheets/railspress/admin/flash.css +26 -0
- data/app/assets/stylesheets/railspress/admin/forms.css +459 -0
- data/app/assets/stylesheets/railspress/admin/layout.css +256 -0
- data/app/assets/stylesheets/railspress/admin/lists.css +24 -0
- data/app/assets/stylesheets/railspress/admin/page.css +111 -0
- data/app/assets/stylesheets/railspress/admin/responsive.css +174 -0
- data/app/assets/stylesheets/railspress/admin/stats.css +43 -0
- data/app/assets/stylesheets/railspress/admin/tables.css +163 -0
- data/app/assets/stylesheets/railspress/admin/utilities.css +202 -0
- data/app/assets/stylesheets/railspress/admin/variables.css +58 -0
- data/app/assets/stylesheets/railspress/application.css +44 -13
- data/app/controllers/railspress/admin/base_controller.rb +6 -3
- data/app/controllers/railspress/admin/categories_controller.rb +1 -1
- data/app/controllers/railspress/admin/cms_transfers_controller.rb +49 -0
- data/app/controllers/railspress/admin/content_element_versions_controller.rb +12 -0
- data/app/controllers/railspress/admin/content_elements_controller.rb +143 -0
- data/app/controllers/railspress/admin/content_groups_controller.rb +69 -0
- data/app/controllers/railspress/admin/dashboard_controller.rb +6 -0
- data/app/controllers/railspress/admin/entities_controller.rb +157 -0
- data/app/controllers/railspress/admin/exports_controller.rb +55 -0
- data/app/controllers/railspress/admin/focal_points_controller.rb +100 -0
- data/app/controllers/railspress/admin/imports_controller.rb +63 -0
- data/app/controllers/railspress/admin/posts_controller.rb +58 -4
- data/app/controllers/railspress/admin/prototypes_controller.rb +30 -0
- data/app/controllers/railspress/admin/tags_controller.rb +1 -1
- data/app/controllers/railspress/application_controller.rb +1 -0
- data/app/helpers/railspress/admin_helper.rb +733 -0
- data/app/helpers/railspress/application_helper.rb +23 -0
- data/app/helpers/railspress/cms_helper.rb +319 -0
- data/app/javascript/railspress/controllers/cms_inline_editor_controller.js +147 -0
- data/app/javascript/railspress/controllers/content_element_form_controller.js +15 -0
- data/app/javascript/railspress/controllers/crop_controller.js +224 -0
- data/app/javascript/railspress/controllers/dropzone_controller.js +261 -0
- data/app/javascript/railspress/controllers/focal_point_controller.js +124 -0
- data/app/javascript/railspress/controllers/image_section_controller.js +94 -0
- data/app/javascript/railspress/controllers/index.js +37 -0
- data/app/javascript/railspress/index.js +62 -0
- data/app/jobs/railspress/export_posts_job.rb +16 -0
- data/app/jobs/railspress/import_posts_job.rb +44 -0
- data/app/models/concerns/railspress/has_focal_point.rb +242 -0
- data/app/models/concerns/railspress/soft_deletable.rb +23 -0
- data/app/models/concerns/railspress/taggable.rb +23 -0
- data/app/models/railspress/content_element.rb +103 -0
- data/app/models/railspress/content_element_version.rb +32 -0
- data/app/models/railspress/content_group.rb +39 -0
- data/app/models/railspress/export.rb +67 -0
- data/app/models/railspress/focal_point.rb +70 -0
- data/app/models/railspress/import.rb +65 -0
- data/app/models/railspress/post.rb +102 -15
- data/app/models/railspress/post_export_processor.rb +162 -0
- data/app/models/railspress/post_import_processor.rb +382 -0
- data/app/models/railspress/tag.rb +10 -3
- data/app/models/railspress/tagging.rb +11 -0
- data/app/services/railspress/content_export_service.rb +122 -0
- data/app/services/railspress/content_import_service.rb +228 -0
- data/app/views/action_text/attachables/_remote_image.html.erb +8 -0
- data/app/views/active_storage/blobs/_blob.html.erb +1 -1
- data/app/views/layouts/railspress/admin.html.erb +3 -1
- data/app/views/railspress/admin/categories/index.html.erb +11 -15
- data/app/views/railspress/admin/cms_transfers/show.html.erb +167 -0
- data/app/views/railspress/admin/content_element_versions/show.html.erb +42 -0
- data/app/views/railspress/admin/content_elements/_form.html.erb +71 -0
- data/app/views/railspress/admin/content_elements/_inline_form.html.erb +32 -0
- data/app/views/railspress/admin/content_elements/_inline_form_frame.html.erb +6 -0
- data/app/views/railspress/admin/content_elements/edit.html.erb +6 -0
- data/app/views/railspress/admin/content_elements/index.html.erb +74 -0
- data/app/views/railspress/admin/content_elements/new.html.erb +6 -0
- data/app/views/railspress/admin/content_elements/show.html.erb +124 -0
- data/app/views/railspress/admin/content_groups/_form.html.erb +9 -0
- data/app/views/railspress/admin/content_groups/edit.html.erb +6 -0
- data/app/views/railspress/admin/content_groups/index.html.erb +42 -0
- data/app/views/railspress/admin/content_groups/new.html.erb +6 -0
- data/app/views/railspress/admin/content_groups/show.html.erb +92 -0
- data/app/views/railspress/admin/dashboard/index.html.erb +36 -1
- data/app/views/railspress/admin/entities/_form.html.erb +53 -0
- data/app/views/railspress/admin/entities/edit.html.erb +4 -0
- data/app/views/railspress/admin/entities/index.html.erb +74 -0
- data/app/views/railspress/admin/entities/new.html.erb +4 -0
- data/app/views/railspress/admin/entities/show.html.erb +117 -0
- data/app/views/railspress/admin/exports/show.html.erb +62 -0
- data/app/views/railspress/admin/imports/_instructions.html.erb +56 -0
- data/app/views/railspress/admin/imports/show.html.erb +137 -0
- data/app/views/railspress/admin/posts/_form.html.erb +102 -28
- data/app/views/railspress/admin/posts/_post_row.html.erb +40 -0
- data/app/views/railspress/admin/posts/index.html.erb +47 -36
- data/app/views/railspress/admin/posts/show.html.erb +55 -19
- data/app/views/railspress/admin/prototypes/image_section.html.erb +42 -0
- data/app/views/railspress/admin/shared/_dropzone.html.erb +84 -0
- data/app/views/railspress/admin/shared/_focal_point_editor.html.erb +102 -0
- data/app/views/railspress/admin/shared/_image_section.html.erb +159 -0
- data/app/views/railspress/admin/shared/_image_section_compact.html.erb +90 -0
- data/app/views/railspress/admin/shared/_image_section_editor.html.erb +171 -0
- data/app/views/railspress/admin/shared/_image_section_v2.html.erb +205 -0
- data/app/views/railspress/admin/shared/_sidebar.html.erb +73 -5
- data/app/views/railspress/admin/tags/index.html.erb +12 -16
- data/config/brakeman.ignore +18 -0
- data/config/importmap.rb +23 -0
- data/config/routes.rb +62 -1
- data/db/migrate/20241218000004_create_railspress_post_tags.rb +1 -1
- data/db/migrate/20241218000005_create_railspress_imports.rb +21 -0
- data/db/migrate/20241218000006_create_railspress_exports.rb +20 -0
- data/db/migrate/20241218000007_create_railspress_taggings.rb +20 -0
- data/db/migrate/20241218000008_drop_railspress_post_tags.rb +14 -0
- data/db/migrate/20241218000010_add_reading_time_to_railspress_posts.rb +5 -0
- data/db/migrate/20250105000002_create_railspress_focal_points.rb +20 -0
- data/db/migrate/20260206000001_create_railspress_content_groups.rb +18 -0
- data/db/migrate/20260206000002_create_railspress_content_elements.rb +21 -0
- data/db/migrate/20260206000003_create_railspress_content_element_versions.rb +20 -0
- data/db/migrate/20260207000001_add_unique_index_to_content_elements.rb +11 -0
- data/db/migrate/20260211112812_add_image_hint_to_railspress_content_elements.rb +7 -0
- data/db/migrate/20260211154040_add_required_to_railspress_content_elements.rb +5 -0
- data/lib/generators/railspress/entity/entity_generator.rb +89 -0
- data/lib/generators/railspress/entity/templates/migration.rb.tt +13 -0
- data/lib/generators/railspress/entity/templates/model.rb.tt +21 -0
- data/lib/generators/railspress/install/install_generator.rb +51 -40
- data/lib/generators/railspress/install/templates/initializer.rb +29 -0
- data/lib/railspress/engine.rb +38 -0
- data/lib/railspress/entity.rb +239 -0
- data/lib/railspress/version.rb +1 -1
- data/lib/railspress.rb +198 -8
- data/lib/tasks/railspress_tasks.rake +49 -4
- metadata +215 -21
- data/MIT-LICENSE +0 -20
- data/app/assets/stylesheets/railspress/admin.css +0 -1207
- data/app/models/railspress/post_tag.rb +0 -8
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railspress
|
|
4
|
+
# Stores field configuration for a single entity
|
|
5
|
+
#
|
|
6
|
+
# Stores class name as string for Rails reloader compatibility.
|
|
7
|
+
# Class is resolved lazily via constantize on each access, ensuring
|
|
8
|
+
# fresh class reference after code reload in development.
|
|
9
|
+
class EntityConfig
|
|
10
|
+
attr_writer :label
|
|
11
|
+
|
|
12
|
+
def initialize(model_class_name)
|
|
13
|
+
@model_class_name = model_class_name.to_s
|
|
14
|
+
@field_definitions = {}
|
|
15
|
+
@label = nil # Resolved lazily from model_class if not set
|
|
16
|
+
@types_resolved = false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Lazily resolve class - gets fresh class after Rails reload
|
|
20
|
+
def model_class
|
|
21
|
+
@model_class_name.constantize
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def add_field(name, options = {})
|
|
25
|
+
# Store the explicit type or mark for lazy detection
|
|
26
|
+
explicit_type = options[:as]
|
|
27
|
+
@field_definitions[name.to_sym] = {
|
|
28
|
+
type: explicit_type,
|
|
29
|
+
options: options.except(:as),
|
|
30
|
+
needs_detection: explicit_type.nil?
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Auto-wire virtual attributes for array types
|
|
34
|
+
if explicit_type.in?([ :list, :lines ])
|
|
35
|
+
define_array_accessors(name, explicit_type)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Access fields with resolved types (lazy resolution)
|
|
40
|
+
def fields
|
|
41
|
+
resolve_field_types! unless @types_resolved
|
|
42
|
+
@field_definitions
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def route_key
|
|
46
|
+
model_class.model_name.plural
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def param_key
|
|
50
|
+
model_class.model_name.param_key
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def singular_label
|
|
54
|
+
(label || model_class.model_name.human.pluralize).singularize
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def label
|
|
58
|
+
@label || model_class.model_name.human.pluralize
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# Auto-generate virtual attributes for :list and :lines fields
|
|
64
|
+
# This eliminates the need for a separate ArrayFields concern
|
|
65
|
+
def define_array_accessors(name, type)
|
|
66
|
+
separator = type == :list ? ", " : "\n"
|
|
67
|
+
delimiter = type == :list ? "," : /\r?\n/
|
|
68
|
+
dedupe = type == :list
|
|
69
|
+
|
|
70
|
+
model_class.class_eval do
|
|
71
|
+
# Nil guard - always return array
|
|
72
|
+
define_method(name) do
|
|
73
|
+
super() || []
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Virtual getter: array → string
|
|
77
|
+
define_method("#{name}_list") do
|
|
78
|
+
send(name).join(separator)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Virtual setter: string → array
|
|
82
|
+
define_method("#{name}_list=") do |value|
|
|
83
|
+
parsed = value.to_s.split(delimiter).map(&:strip).reject(&:blank?)
|
|
84
|
+
parsed = parsed.uniq if dedupe
|
|
85
|
+
send("#{name}=", parsed)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def resolve_field_types!
|
|
91
|
+
@field_definitions.each do |name, field|
|
|
92
|
+
if field[:needs_detection]
|
|
93
|
+
field[:type] = detect_type(name)
|
|
94
|
+
field.delete(:needs_detection)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
@types_resolved = true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def detect_type(name)
|
|
101
|
+
name_str = name.to_s
|
|
102
|
+
|
|
103
|
+
# Check for ActionText rich text (has_one :rich_text_#{name} association)
|
|
104
|
+
if model_class.reflect_on_association(:"rich_text_#{name_str}")
|
|
105
|
+
return :rich_text
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Check for ActiveStorage attachments
|
|
109
|
+
if model_class.respond_to?(:reflect_on_all_attachments)
|
|
110
|
+
attachment = model_class.reflect_on_all_attachments.find do |a|
|
|
111
|
+
a.name.to_s == name_str
|
|
112
|
+
end
|
|
113
|
+
if attachment
|
|
114
|
+
# Check if this attachment has focal point support
|
|
115
|
+
if attachment.macro == :has_one_attached &&
|
|
116
|
+
model_class.respond_to?(:focal_point_attachments) &&
|
|
117
|
+
model_class.focal_point_attachments.include?(name)
|
|
118
|
+
return :focal_point_image
|
|
119
|
+
end
|
|
120
|
+
return attachment.macro == :has_many_attached ? :attachments : :attachment
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Check column type from schema
|
|
125
|
+
column = model_class.columns_hash[name_str]
|
|
126
|
+
return :string unless column
|
|
127
|
+
|
|
128
|
+
case column.type
|
|
129
|
+
when :text then :text
|
|
130
|
+
when :integer then :integer
|
|
131
|
+
when :boolean then :boolean
|
|
132
|
+
when :datetime then :datetime
|
|
133
|
+
when :date then :date
|
|
134
|
+
when :decimal, :float then :decimal
|
|
135
|
+
else :string
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Concern to include in host models for CMS management
|
|
141
|
+
module Entity
|
|
142
|
+
extend ActiveSupport::Concern
|
|
143
|
+
|
|
144
|
+
included do
|
|
145
|
+
class_attribute :_railspress_config, instance_writer: false
|
|
146
|
+
# Store class name (string) for Rails reloader compatibility
|
|
147
|
+
self._railspress_config = EntityConfig.new(self.name)
|
|
148
|
+
|
|
149
|
+
# Default scopes available to all entities
|
|
150
|
+
scope :ordered, -> { order(created_at: :desc) }
|
|
151
|
+
scope :recent, -> { ordered.limit(10) }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Default pagination - can be overridden in model
|
|
155
|
+
PER_PAGE = 20
|
|
156
|
+
|
|
157
|
+
class_methods do
|
|
158
|
+
# Simple pagination for index views
|
|
159
|
+
def page(page_number)
|
|
160
|
+
page_number = [ page_number.to_i, 1 ].max
|
|
161
|
+
offset((page_number - 1) * per_page_count).limit(per_page_count)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Allow models to override PER_PAGE
|
|
165
|
+
def per_page_count
|
|
166
|
+
const_defined?(:PER_PAGE, false) ? const_get(:PER_PAGE) : Railspress::Entity::PER_PAGE
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Declare which fields should appear in the CMS
|
|
170
|
+
#
|
|
171
|
+
# @example Simple field list (types auto-detected from schema)
|
|
172
|
+
# railspress_fields :title, :description, :published
|
|
173
|
+
#
|
|
174
|
+
# @example With explicit type override
|
|
175
|
+
# railspress_fields :body, as: :rich_text
|
|
176
|
+
#
|
|
177
|
+
# @example Hash syntax for multiple typed fields
|
|
178
|
+
# railspress_fields title: :string, body: :rich_text
|
|
179
|
+
#
|
|
180
|
+
def railspress_fields(*names, **options)
|
|
181
|
+
# Handle positional args: railspress_fields :title, :description
|
|
182
|
+
if options[:as] && names.length == 1
|
|
183
|
+
# Single field with type: railspress_fields :body, as: :rich_text
|
|
184
|
+
_railspress_config.add_field(names.first, as: options[:as])
|
|
185
|
+
elsif names.any?
|
|
186
|
+
# Multiple fields, auto-detect types
|
|
187
|
+
names.each do |name|
|
|
188
|
+
_railspress_config.add_field(name)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Handle hash syntax: railspress_fields title: :string, body: :rich_text
|
|
193
|
+
options.except(:as).each do |name, type|
|
|
194
|
+
_railspress_config.add_field(name, as: type)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Set custom label for sidebar/headers
|
|
199
|
+
#
|
|
200
|
+
# @example
|
|
201
|
+
# railspress_label "Client Projects"
|
|
202
|
+
#
|
|
203
|
+
def railspress_label(label)
|
|
204
|
+
_railspress_config.label = label
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Access the entity configuration
|
|
208
|
+
def railspress_config
|
|
209
|
+
_railspress_config
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Columns to display in the admin index table
|
|
213
|
+
#
|
|
214
|
+
# Override hierarchy:
|
|
215
|
+
# 1. Model overrides this method directly (full control)
|
|
216
|
+
# 2. Model defines RAILSPRESS_INDEX_COLUMNS constant
|
|
217
|
+
# 3. Auto-detect from Railspress.default_index_columns
|
|
218
|
+
#
|
|
219
|
+
# @example Override with constant
|
|
220
|
+
# class Project < ApplicationRecord
|
|
221
|
+
# include Railspress::Entity
|
|
222
|
+
# RAILSPRESS_INDEX_COLUMNS = [:name, :client, :status, :created_at]
|
|
223
|
+
# end
|
|
224
|
+
#
|
|
225
|
+
# @example Override with method (for dynamic logic)
|
|
226
|
+
# def self.railspress_index_columns
|
|
227
|
+
# [:name, :client, admin? ? :budget : nil, :created_at].compact
|
|
228
|
+
# end
|
|
229
|
+
#
|
|
230
|
+
def railspress_index_columns
|
|
231
|
+
return self::RAILSPRESS_INDEX_COLUMNS if const_defined?(:RAILSPRESS_INDEX_COLUMNS, false)
|
|
232
|
+
|
|
233
|
+
Railspress.default_index_columns.select do |col|
|
|
234
|
+
new.respond_to?(col)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
data/lib/railspress/version.rb
CHANGED
data/lib/railspress.rb
CHANGED
|
@@ -1,25 +1,41 @@
|
|
|
1
1
|
require "railspress/version"
|
|
2
2
|
require "railspress/engine"
|
|
3
|
+
require "railspress/entity"
|
|
3
4
|
require "lexxy"
|
|
4
5
|
|
|
5
6
|
module Railspress
|
|
7
|
+
class ConfigurationError < StandardError; end
|
|
8
|
+
|
|
6
9
|
class Configuration
|
|
7
10
|
attr_accessor :author_class_name,
|
|
8
11
|
:current_author_method,
|
|
9
12
|
:current_author_proc,
|
|
10
13
|
:author_scope,
|
|
11
|
-
:author_display_method
|
|
14
|
+
:author_display_method,
|
|
15
|
+
:words_per_minute,
|
|
16
|
+
:blog_path,
|
|
17
|
+
:default_index_columns,
|
|
18
|
+
:post_image_variants,
|
|
19
|
+
:inline_editing_check
|
|
12
20
|
|
|
13
|
-
attr_reader :authors_enabled, :
|
|
21
|
+
attr_reader :authors_enabled, :post_images_enabled, :focal_points_enabled, :cms_enabled, :image_contexts
|
|
14
22
|
|
|
15
23
|
def initialize
|
|
16
24
|
@authors_enabled = false
|
|
17
|
-
@
|
|
25
|
+
@post_images_enabled = false
|
|
26
|
+
@focal_points_enabled = false
|
|
27
|
+
@cms_enabled = false
|
|
28
|
+
@image_contexts = default_image_contexts
|
|
29
|
+
@post_image_variants = {}
|
|
30
|
+
@inline_editing_check = nil
|
|
18
31
|
@author_class_name = "User"
|
|
19
32
|
@current_author_method = :current_user
|
|
20
33
|
@current_author_proc = nil
|
|
21
34
|
@author_scope = nil
|
|
22
35
|
@author_display_method = :name
|
|
36
|
+
@words_per_minute = 200
|
|
37
|
+
@blog_path = "/blog"
|
|
38
|
+
@default_index_columns = [ :id, :title, :name, :created_at ]
|
|
23
39
|
end
|
|
24
40
|
|
|
25
41
|
# Declarative setter: config.enable_authors
|
|
@@ -27,9 +43,137 @@ module Railspress
|
|
|
27
43
|
@authors_enabled = true
|
|
28
44
|
end
|
|
29
45
|
|
|
30
|
-
# Declarative setter: config.
|
|
31
|
-
def
|
|
32
|
-
@
|
|
46
|
+
# Declarative setter: config.enable_post_images
|
|
47
|
+
def enable_post_images
|
|
48
|
+
@post_images_enabled = true
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Declarative setter: config.enable_focal_points
|
|
52
|
+
def enable_focal_points
|
|
53
|
+
@focal_points_enabled = true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Declarative setter: config.enable_cms
|
|
57
|
+
def enable_cms
|
|
58
|
+
@cms_enabled = true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Validate configuration after the configure block completes.
|
|
62
|
+
# This keeps the configure block order-independent.
|
|
63
|
+
def validate!
|
|
64
|
+
if @inline_editing_check && !@cms_enabled
|
|
65
|
+
raise Railspress::ConfigurationError,
|
|
66
|
+
"Inline editing requires CMS. Add `config.enable_cms` to your initializer."
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Set custom image contexts
|
|
71
|
+
def image_contexts=(contexts)
|
|
72
|
+
@image_contexts = contexts.transform_keys(&:to_sym)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Add a single image context
|
|
76
|
+
def add_image_context(name, aspect:, label: nil, sizes: [])
|
|
77
|
+
@image_contexts[name.to_sym] = {
|
|
78
|
+
aspect: aspect,
|
|
79
|
+
label: label || name.to_s.humanize,
|
|
80
|
+
sizes: sizes
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Remove an image context
|
|
85
|
+
def remove_image_context(name)
|
|
86
|
+
@image_contexts.delete(name.to_sym)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Register a host model as a CMS-managed entity
|
|
90
|
+
#
|
|
91
|
+
# Accepts class, string, or symbol. String/symbol registration is preferred
|
|
92
|
+
# for Rails reloader compatibility in development.
|
|
93
|
+
#
|
|
94
|
+
# Registration is deferred until first access - this allows registration
|
|
95
|
+
# in initializers before models are loaded by Zeitwerk.
|
|
96
|
+
#
|
|
97
|
+
# @param identifier [Class, String, Symbol] The model to register
|
|
98
|
+
# @param options [Hash] Optional configuration
|
|
99
|
+
# @option options [String] :label Custom sidebar/header label
|
|
100
|
+
#
|
|
101
|
+
# @example String registration (preferred)
|
|
102
|
+
# config.register_entity "Project"
|
|
103
|
+
# config.register_entity "Portfolio", label: "Work Samples"
|
|
104
|
+
#
|
|
105
|
+
# @example Symbol registration
|
|
106
|
+
# config.register_entity :project
|
|
107
|
+
# config.register_entity :admin_portfolio, label: "Work Samples"
|
|
108
|
+
#
|
|
109
|
+
# @example Class registration (works but may have stale refs after reload)
|
|
110
|
+
# config.register_entity Project
|
|
111
|
+
#
|
|
112
|
+
def register_entity(identifier, options = {})
|
|
113
|
+
# Normalize to class name string
|
|
114
|
+
class_name = case identifier
|
|
115
|
+
when String then identifier
|
|
116
|
+
when Symbol then identifier.to_s.camelize
|
|
117
|
+
when Class then identifier.name
|
|
118
|
+
else
|
|
119
|
+
raise ArgumentError, "Expected String, Symbol, or Class, got #{identifier.class}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Compute route_key from class name (e.g., "Project" -> "projects")
|
|
123
|
+
route_key = class_name.underscore.pluralize
|
|
124
|
+
|
|
125
|
+
# Store just the class name and options - resolved fresh on each access
|
|
126
|
+
entity_registrations[route_key] = { class_name: class_name, options: options }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Entity registrations: route_key => { class_name:, options: }
|
|
130
|
+
# We store class names (strings), not config objects, so Rails reloading works
|
|
131
|
+
def entity_registrations
|
|
132
|
+
@entity_registrations ||= {}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Get all registered entities - resolves fresh on each call
|
|
136
|
+
def registered_entities
|
|
137
|
+
result = {}
|
|
138
|
+
entity_registrations.each do |route_key, registration|
|
|
139
|
+
config = resolve_entity(registration[:class_name], registration[:options])
|
|
140
|
+
result[route_key] = config if config
|
|
141
|
+
end
|
|
142
|
+
result
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Find entity config by route key - resolves fresh on each call
|
|
146
|
+
def entity_for(route_key)
|
|
147
|
+
registration = entity_registrations[route_key.to_s]
|
|
148
|
+
return nil unless registration
|
|
149
|
+
|
|
150
|
+
resolve_entity(registration[:class_name], registration[:options])
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Check if an entity is registered
|
|
154
|
+
def entity_registered?(route_key)
|
|
155
|
+
entity_registrations.key?(route_key.to_s)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
def default_image_contexts
|
|
161
|
+
{
|
|
162
|
+
hero: { aspect: [ 16, 9 ], label: "Hero", sizes: [ 1920, 1280 ] },
|
|
163
|
+
card: { aspect: [ 4, 3 ], label: "Card", sizes: [ 800, 400 ] },
|
|
164
|
+
thumb: { aspect: [ 1, 1 ], label: "Thumbnail", sizes: [ 200 ] }
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def resolve_entity(class_name, options)
|
|
169
|
+
klass = class_name.constantize
|
|
170
|
+
unless klass.included_modules.include?(Railspress::Entity)
|
|
171
|
+
raise ArgumentError, "#{class_name} must include Railspress::Entity"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
config = klass.railspress_config
|
|
175
|
+
config.label = options[:label] if options[:label]
|
|
176
|
+
config
|
|
33
177
|
end
|
|
34
178
|
end
|
|
35
179
|
|
|
@@ -40,6 +184,7 @@ module Railspress
|
|
|
40
184
|
|
|
41
185
|
def configure
|
|
42
186
|
yield(configuration)
|
|
187
|
+
configuration.validate!
|
|
43
188
|
end
|
|
44
189
|
|
|
45
190
|
def reset_configuration!
|
|
@@ -51,8 +196,20 @@ module Railspress
|
|
|
51
196
|
configuration.authors_enabled
|
|
52
197
|
end
|
|
53
198
|
|
|
54
|
-
def
|
|
55
|
-
configuration.
|
|
199
|
+
def post_images_enabled?
|
|
200
|
+
configuration.post_images_enabled
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def focal_points_enabled?
|
|
204
|
+
configuration.focal_points_enabled
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def cms_enabled?
|
|
208
|
+
configuration.cms_enabled
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def image_contexts
|
|
212
|
+
configuration.image_contexts
|
|
56
213
|
end
|
|
57
214
|
|
|
58
215
|
def author_class
|
|
@@ -81,5 +238,38 @@ module Railspress
|
|
|
81
238
|
def current_author_proc
|
|
82
239
|
configuration.current_author_proc
|
|
83
240
|
end
|
|
241
|
+
|
|
242
|
+
def words_per_minute
|
|
243
|
+
configuration.words_per_minute
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def blog_path
|
|
247
|
+
configuration.blog_path
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def default_index_columns
|
|
251
|
+
configuration.default_index_columns
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def post_image_variants
|
|
255
|
+
configuration.post_image_variants
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def inline_editing_check
|
|
259
|
+
configuration.inline_editing_check
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Entity registry convenience accessors
|
|
263
|
+
def registered_entities
|
|
264
|
+
configuration.registered_entities
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def entity_for(route_key)
|
|
268
|
+
configuration.entity_for(route_key)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def entity_registered?(route_key)
|
|
272
|
+
configuration.entity_registered?(route_key)
|
|
273
|
+
end
|
|
84
274
|
end
|
|
85
275
|
end
|
|
@@ -1,4 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
namespace :railspress do
|
|
2
|
+
desc "Migrate post_tags to polymorphic taggings table (one-time migration)"
|
|
3
|
+
task migrate_tags: :environment do
|
|
4
|
+
puts "Starting tag migration from post_tags to taggings..."
|
|
5
|
+
|
|
6
|
+
# Check if PostTag table exists
|
|
7
|
+
unless ActiveRecord::Base.connection.table_exists?(:railspress_post_tags)
|
|
8
|
+
puts "No post_tags table found. Nothing to migrate."
|
|
9
|
+
next
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
old_count = ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM railspress_post_tags").to_i
|
|
13
|
+
|
|
14
|
+
if old_count == 0
|
|
15
|
+
puts "No post_tags to migrate. Done!"
|
|
16
|
+
next
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
puts "Found #{old_count} post_tags to migrate"
|
|
20
|
+
|
|
21
|
+
ActiveRecord::Base.transaction do
|
|
22
|
+
# Use raw SQL to avoid dependency on PostTag model
|
|
23
|
+
results = ActiveRecord::Base.connection.select_all(
|
|
24
|
+
"SELECT post_id, tag_id FROM railspress_post_tags"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
results.each do |row|
|
|
28
|
+
Railspress::Tagging.find_or_create_by!(
|
|
29
|
+
tag_id: row["tag_id"],
|
|
30
|
+
taggable_type: "Railspress::Post",
|
|
31
|
+
taggable_id: row["post_id"]
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
new_count = Railspress::Tagging.where(taggable_type: "Railspress::Post").count
|
|
36
|
+
|
|
37
|
+
if new_count != old_count
|
|
38
|
+
raise "Count mismatch! Expected #{old_count}, got #{new_count}. Rolling back."
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
puts "Successfully migrated #{new_count} taggings"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
puts "\nMigration complete!"
|
|
45
|
+
puts "You can now safely drop the post_tags table."
|
|
46
|
+
puts "Run the drop migration or execute:"
|
|
47
|
+
puts " rails generate migration DropRailspressPostTags"
|
|
48
|
+
end
|
|
49
|
+
end
|