macron-cms 3.0.0 → 5.0.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/README.md +338 -25
- data/app/assets/stylesheets/macron/cms/application.css +1 -1
- data/app/controllers/concerns/macron/cms/api_serialization.rb +79 -0
- data/app/controllers/concerns/macron/cms/slug_redirectable.rb +27 -0
- data/app/controllers/macron/cms/api/v1/article_contents_controller.rb +221 -0
- data/app/controllers/macron/cms/api/v1/articles_controller.rb +140 -66
- data/app/controllers/macron/cms/api/v1/content_types_controller.rb +3 -1
- data/app/controllers/macron/cms/article_contents_controller.rb +44 -10
- data/app/controllers/macron/cms/articles_controller.rb +4 -3
- data/app/controllers/macron/cms/content_types_controller.rb +2 -0
- data/app/controllers/macron/cms/dashboard_controller.rb +1 -1
- data/app/helpers/macron/cms/content_helper.rb +1 -1
- data/app/helpers/macron/cms/fields_helper.rb +14 -0
- data/app/models/macron/cms/article.rb +45 -15
- data/app/models/macron/cms/article_content.rb +24 -0
- data/app/models/macron/cms/content.rb +27 -0
- data/app/services/macron/cms/inline_asset_attacher.rb +70 -0
- data/app/services/macron/cms/remote_asset_attacher.rb +16 -6
- data/app/views/macron/cms/articles/_content_blocks_section.html.erb +494 -199
- data/app/views/macron/cms/articles/_form.html.erb +10 -2
- data/app/views/macron/cms/articles/_table.html.erb +3 -3
- data/app/views/macron/cms/articles/show.html.erb +3 -1
- data/app/views/macron/cms/content_types/index.html.erb +30 -0
- data/app/views/macron/cms/content_types/show.html.erb +34 -0
- data/config/routes.rb +12 -1
- data/db/migrate/20261029000001_add_convertible_to_to_macron_cms_contents.rb +5 -0
- data/db/migrate/20261029120001_add_featured_image_to_macron_cms_contents.rb +5 -0
- data/db/migrate/20261029120002_add_featured_image_blob_to_macron_cms_articles.rb +8 -0
- data/db/migrate/20261029140000_create_friendly_id_slugs.rb +26 -0
- data/db/migrate/20261029150000_remove_name_from_macron_cms_articles.rb +6 -0
- data/db/migrate/20261029160000_add_published_at_to_macron_cms_articles.rb +14 -0
- data/db/seeds.rb +3 -2
- data/lib/macron/cms/version.rb +1 -1
- data/lib/tasks/macron/cms_tasks.rake +6 -0
- metadata +11 -1
|
@@ -6,6 +6,8 @@ module Macron
|
|
|
6
6
|
has_many :article_contents, class_name: "Macron::Cms::ArticleContent", dependent: :destroy
|
|
7
7
|
has_many :articles, through: :article_contents
|
|
8
8
|
|
|
9
|
+
scope :featured_image_capable, -> { where(featured_image: true) }
|
|
10
|
+
|
|
9
11
|
def normalized_fields
|
|
10
12
|
raw = if fields.is_a?(Array)
|
|
11
13
|
fields
|
|
@@ -29,6 +31,31 @@ module Macron
|
|
|
29
31
|
def field_names
|
|
30
32
|
normalized_fields.map { |f| f["name"] }
|
|
31
33
|
end
|
|
34
|
+
|
|
35
|
+
def normalized_convertible_to
|
|
36
|
+
Array(convertible_to).filter_map do |entry|
|
|
37
|
+
target_name = entry.to_s.strip
|
|
38
|
+
target_name if target_name.present? && target_name != name
|
|
39
|
+
end.uniq
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def convertible_to?(target)
|
|
43
|
+
target_name = target.is_a?(Content) ? target.name : target.to_s.strip
|
|
44
|
+
normalized_convertible_to.include?(target_name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def convertible_targets(available_contents = nil)
|
|
48
|
+
names = normalized_convertible_to
|
|
49
|
+
return [] if names.empty?
|
|
50
|
+
|
|
51
|
+
indexed = if available_contents
|
|
52
|
+
available_contents.index_by(&:name)
|
|
53
|
+
else
|
|
54
|
+
Content.where(name: names).index_by(&:name)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
names.filter_map { |n| indexed[n] }
|
|
58
|
+
end
|
|
32
59
|
end
|
|
33
60
|
end
|
|
34
61
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require "base64"
|
|
2
|
+
require "stringio"
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Macron
|
|
6
|
+
module Cms
|
|
7
|
+
class InlineAssetAttacher
|
|
8
|
+
MAX_FILE_SIZE = 25.megabytes
|
|
9
|
+
|
|
10
|
+
def self.attach(article_content:, upload_data:, replace: true)
|
|
11
|
+
new(record: article_content, association: :files, upload_data: upload_data, replace: replace).call
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.attach_background(article:, upload_data:, replace: true)
|
|
15
|
+
new(record: article, association: :background_image, upload_data: upload_data, replace: replace).call
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(record:, association:, upload_data:, replace: true)
|
|
19
|
+
@record = record
|
|
20
|
+
@association = association
|
|
21
|
+
@upload_data = upload_data.respond_to?(:to_unsafe_h) ? upload_data.to_unsafe_h : upload_data.to_h
|
|
22
|
+
@replace = replace
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call
|
|
26
|
+
return false if @upload_data.blank?
|
|
27
|
+
|
|
28
|
+
data_str = (@upload_data["data_base64"] || @upload_data[:data_base64] || @upload_data["data"] || @upload_data[:data]).to_s.strip
|
|
29
|
+
return false if data_str.blank?
|
|
30
|
+
|
|
31
|
+
# Strip optional data URI scheme prefix: data:image/png;base64,...
|
|
32
|
+
raw_base64 = data_str.sub(/\Adata:[^;]+;base64,/, "")
|
|
33
|
+
binary_data = Base64.decode64(raw_base64)
|
|
34
|
+
|
|
35
|
+
if binary_data.bytesize > MAX_FILE_SIZE
|
|
36
|
+
raise ArgumentError, "Uploaded file exceeds maximum size of #{MAX_FILE_SIZE} bytes"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
filename = (@upload_data["filename"] || @upload_data[:filename]).to_s.strip
|
|
40
|
+
content_type = (@upload_data["content_type"] || @upload_data[:content_type]).to_s.strip
|
|
41
|
+
|
|
42
|
+
if filename.blank?
|
|
43
|
+
ext = Rack::Mime::MIME_TYPES.invert[content_type] || ".bin"
|
|
44
|
+
filename = "upload_#{SecureRandom.hex(6)}#{ext}"
|
|
45
|
+
end
|
|
46
|
+
content_type = "application/octet-stream" if content_type.blank?
|
|
47
|
+
|
|
48
|
+
io = StringIO.new(binary_data)
|
|
49
|
+
|
|
50
|
+
target_assoc = @record.public_send(@association)
|
|
51
|
+
|
|
52
|
+
if @replace
|
|
53
|
+
if target_assoc.respond_to?(:purge) && target_assoc.attached?
|
|
54
|
+
target_assoc.purge
|
|
55
|
+
elsif target_assoc.respond_to?(:attached?) && target_assoc.attached?
|
|
56
|
+
target_assoc.purge
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
target_assoc.attach(
|
|
61
|
+
io: io,
|
|
62
|
+
filename: filename,
|
|
63
|
+
content_type: content_type
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
true
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -9,11 +9,17 @@ module Macron
|
|
|
9
9
|
OPEN_TIMEOUT = 5
|
|
10
10
|
|
|
11
11
|
def self.attach(article_content:, url:, replace: true)
|
|
12
|
-
new(
|
|
12
|
+
new(record: article_content, association: :files, url: url, replace: replace).call
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def
|
|
16
|
-
|
|
15
|
+
def self.attach_background(article:, url:, replace: true)
|
|
16
|
+
new(record: article, association: :background_image, url: url, replace: replace).call
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(record: nil, association: :files, article_content: nil, url:, replace: true)
|
|
20
|
+
@record = record || article_content
|
|
21
|
+
@association = association
|
|
22
|
+
@article_content = article_content || (@record.is_a?(ArticleContent) ? @record : nil)
|
|
17
23
|
@url = url.to_s.strip
|
|
18
24
|
@replace = replace
|
|
19
25
|
end
|
|
@@ -51,13 +57,15 @@ module Macron
|
|
|
51
57
|
filename = derive_filename(uri, downloaded_io)
|
|
52
58
|
content_type = downloaded_io.content_type || "application/octet-stream"
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
target_assoc = @record.public_send(@association)
|
|
61
|
+
|
|
62
|
+
if @replace && target_assoc.respond_to?(:attached?) && target_assoc.attached?
|
|
63
|
+
target_assoc.purge
|
|
56
64
|
end
|
|
57
65
|
|
|
58
66
|
downloaded_io.rewind if downloaded_io.respond_to?(:rewind)
|
|
59
67
|
|
|
60
|
-
|
|
68
|
+
target_assoc.attach(
|
|
61
69
|
io: downloaded_io,
|
|
62
70
|
filename: filename,
|
|
63
71
|
content_type: content_type
|
|
@@ -77,6 +85,8 @@ module Macron
|
|
|
77
85
|
end
|
|
78
86
|
|
|
79
87
|
def fallback_to_payload_url
|
|
88
|
+
return unless @article_content
|
|
89
|
+
|
|
80
90
|
current_payload = (@article_content.payload || {}).dup
|
|
81
91
|
current_payload["upload"] = @url
|
|
82
92
|
@article_content.payload = current_payload
|