robin_cms 0.1.2 → 0.1.4
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 +4 -0
- data/lib/robin_cms/cms.rb +56 -59
- data/lib/robin_cms/collection_library.rb +70 -0
- data/lib/robin_cms/configuration-schema.json +2 -0
- data/lib/robin_cms/configuration.rb +2 -16
- data/lib/robin_cms/data_library.rb +58 -0
- data/lib/robin_cms/editable.rb +40 -0
- data/lib/robin_cms/item.rb +89 -19
- data/lib/robin_cms/library.rb +94 -0
- data/lib/robin_cms/queryable.rb +9 -7
- data/lib/robin_cms/version.rb +1 -1
- data/lib/robin_cms/views/delete_dialog.erb +1 -1
- data/lib/robin_cms/views/filter_form.erb +2 -2
- data/lib/robin_cms/views/hidden_field.erb +1 -1
- data/lib/robin_cms/views/input_field.erb +1 -1
- data/lib/robin_cms/views/layout.erb +6 -3
- data/lib/robin_cms/views/library.erb +9 -9
- data/lib/robin_cms/views/library_actions.erb +4 -1
- data/lib/robin_cms/views/library_item.erb +11 -16
- data/lib/robin_cms/views/richtext_field.erb +1 -1
- data/lib/robin_cms/views/select_field.erb +2 -2
- data/lib/robin_cms/views/stylesheet.css.erb +443 -0
- data/lib/robin_cms.rb +6 -5
- metadata +9 -9
- data/lib/robin_cms/collection_item.rb +0 -82
- data/lib/robin_cms/data_item.rb +0 -87
- data/lib/robin_cms/itemable.rb +0 -124
- data/lib/robin_cms/static_item.rb +0 -92
- data/lib/robin_cms/views/style.erb +0 -441
- /data/lib/robin_cms/views/{logo.erb → logo.svg.erb} +0 -0
- /data/lib/robin_cms/views/{new_tab.erb → new_tab.svg.erb} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3a49ee1ef9d638a300e82352bba65071dc6dfffecfb4cb409fbf8cbb8fbdd69
|
|
4
|
+
data.tar.gz: 928887dfc693144ebaaf9f2233a08b161faa7146ab7a037501098052d046fe74
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 17bcd5f6cc1d057c6740fb3d7cf393432cd823956fcf4e78421b085cfc1ec3ef382c40b776b5f570cfb295af6cae71fd8a0711e5accc118641509bc519947bd9
|
|
7
|
+
data.tar.gz: e7f2f736f7535dce94edbe15a41c24201959e1231d0827c542ec3b4560215d1285e49f1d94321a57bdca50c1cfd85015f8a11f74cbd952b967081e7f5b3291a3
|
data/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Robin CMS
|
|
2
2
|
|
|
3
|
+
> [!IMPORTANT]
|
|
4
|
+
> This software is currently in beta. There may be bugs and breaking changes.
|
|
5
|
+
> If you find a bug, I'd love to hear about it.
|
|
6
|
+
|
|
3
7
|

|
|
4
8
|
|
|
5
9
|
Robin CMS is a minimalist flat-file CMS built with Ruby and Sinatra. It is
|
data/lib/robin_cms/cms.rb
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module RobinCMS
|
|
4
4
|
class CMS < Sinatra::Base
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
set :session_secret, ENV.fetch("SESSION_SECRET", SecureRandom.hex(64))
|
|
5
|
+
include Helpers
|
|
6
|
+
include Auth
|
|
8
7
|
|
|
9
8
|
attr_accessor :config
|
|
10
9
|
|
|
@@ -14,10 +13,9 @@ module RobinCMS
|
|
|
14
13
|
@config = Configuration.parse(**opts)
|
|
15
14
|
end
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
end
|
|
16
|
+
set :logging, true
|
|
17
|
+
set :sessions, true
|
|
18
|
+
set :session_secret, ENV.fetch("SESSION_SECRET", SecureRandom.hex(64))
|
|
21
19
|
|
|
22
20
|
before do
|
|
23
21
|
if %w[/login /logout].include?(request.path_info)
|
|
@@ -27,13 +25,18 @@ module RobinCMS
|
|
|
27
25
|
redirect to("/login") unless session[:auth_user]
|
|
28
26
|
end
|
|
29
27
|
|
|
30
|
-
before
|
|
28
|
+
before /\/libraries\/(.*).*/ do
|
|
31
29
|
kind = params[:captures].first.split("/").first
|
|
30
|
+
library_schema = @config.find_library(kind)
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
@sorted_fields = @config.sorted_fields(kind)
|
|
32
|
+
halt 404 unless library_schema
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
@library = case library_schema[:type]
|
|
35
|
+
when "collection"
|
|
36
|
+
CollectionLibrary.new(library_schema)
|
|
37
|
+
when "data"
|
|
38
|
+
DataLibrary.new(library_schema)
|
|
39
|
+
end
|
|
37
40
|
end
|
|
38
41
|
|
|
39
42
|
after do
|
|
@@ -50,7 +53,6 @@ module RobinCMS
|
|
|
50
53
|
|
|
51
54
|
post "/login" do
|
|
52
55
|
authenticate!(params[:username], params[:password])
|
|
53
|
-
|
|
54
56
|
session[:auth_user] = params[:username]
|
|
55
57
|
redirect to(home_page)
|
|
56
58
|
rescue AuthenticationError
|
|
@@ -80,11 +82,9 @@ module RobinCMS
|
|
|
80
82
|
end
|
|
81
83
|
|
|
82
84
|
authenticate!(session[:auth_user], params[:old_password])
|
|
83
|
-
|
|
84
85
|
update_credentials(params[:new_password])
|
|
85
86
|
session[:auth_user] = nil
|
|
86
87
|
flash[:success] = "Password updated succesfully. Please log in with your new credentials."
|
|
87
|
-
|
|
88
88
|
redirect to("/login")
|
|
89
89
|
rescue AuthenticationError
|
|
90
90
|
flash[:error] = "Incorrect username or password"
|
|
@@ -92,75 +92,72 @@ module RobinCMS
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
get "/libraries/:kind" do
|
|
95
|
-
@items =
|
|
95
|
+
@items = @library.query(
|
|
96
96
|
sort: params[:sort],
|
|
97
97
|
published: params[:published],
|
|
98
|
-
q: params[:q]
|
|
99
|
-
|
|
98
|
+
q: params[:q]
|
|
99
|
+
)
|
|
100
100
|
erb :library
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
get "/libraries/:kind/item" do
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
else
|
|
108
|
-
@item = Item.blank(@library)
|
|
109
|
-
end
|
|
104
|
+
@item = @library.blank
|
|
105
|
+
erb :library_item
|
|
106
|
+
end
|
|
110
107
|
|
|
108
|
+
get "/libraries/:kind/item/:id" do
|
|
109
|
+
@item = @library.find_one(params[:id])
|
|
110
|
+
halt 404 unless @item
|
|
111
111
|
erb :library_item
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
post "/libraries/:kind/item" do
|
|
115
|
-
if params[:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
tempfile = params[:image][:tempfile]
|
|
122
|
-
|
|
123
|
-
StaticItem.delete_if_exists!(@library, @item.attributes[:image_src])
|
|
124
|
-
StaticItem.create!(@library, filename, tempfile).then do |s|
|
|
125
|
-
params[:image_src] = "/" + s.filepath
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
@item.attributes = params
|
|
130
|
-
@item.update!
|
|
131
|
-
else
|
|
132
|
-
if params[:image]
|
|
133
|
-
filename = params[:image][:filename]
|
|
134
|
-
tempfile = params[:image][:tempfile]
|
|
135
|
-
|
|
136
|
-
StaticItem.create!(@library, filename, tempfile).then do |s|
|
|
137
|
-
params[:image_src] = "/" + s.filepath
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
Item.create!(@library, params)
|
|
115
|
+
if params[:image]
|
|
116
|
+
static_path = @library.create_static(
|
|
117
|
+
params[:image][:filename],
|
|
118
|
+
params[:image][:tempfile].to_path
|
|
119
|
+
)
|
|
120
|
+
params[:image_src] = "/" + static_path
|
|
142
121
|
end
|
|
143
122
|
|
|
123
|
+
@library.create(params)
|
|
144
124
|
redirect to("/libraries/#{params[:kind]}")
|
|
145
125
|
rescue ItemExistsError
|
|
146
126
|
flash[:error] = "An item with the same name already exists"
|
|
147
127
|
redirect to("/libraries/#{params[:kind]}/item")
|
|
148
128
|
end
|
|
149
129
|
|
|
150
|
-
post "/libraries/:kind/item
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
halt 404 unless @item
|
|
130
|
+
post "/libraries/:kind/item/:id" do
|
|
131
|
+
@item = @library.find_one(params[:id])
|
|
132
|
+
halt 404 unless @item
|
|
154
133
|
|
|
155
|
-
|
|
156
|
-
|
|
134
|
+
if params[:image]
|
|
135
|
+
# If there is an existing image for this item, delete it to prevent
|
|
136
|
+
# accumulation of old files.
|
|
137
|
+
if @item.attributes[:image_src] && !@item.attributes[:image_src].empty?
|
|
138
|
+
@library.delete_static(@item.attributes[:image_src])
|
|
157
139
|
end
|
|
140
|
+
static_path = @library.create_static(
|
|
141
|
+
params[:image][:filename],
|
|
142
|
+
params[:image][:tempfile].to_path
|
|
143
|
+
)
|
|
144
|
+
params[:image_src] = "/" + static_path
|
|
145
|
+
end
|
|
158
146
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
147
|
+
@item.attributes = params
|
|
148
|
+
@library.write(@item)
|
|
149
|
+
redirect to("/libraries/#{params[:kind]}")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
post "/libraries/:kind/item/:id/delete" do
|
|
153
|
+
@item = @library.find_one(params[:id])
|
|
154
|
+
halt 404 unless @item
|
|
155
|
+
|
|
156
|
+
if @item.attributes[:image_src]
|
|
157
|
+
@library.delete_static(@item.attributes[:image_src])
|
|
162
158
|
end
|
|
163
159
|
|
|
160
|
+
@library.delete(@item.id)
|
|
164
161
|
redirect to("/libraries/#{params[:kind]}")
|
|
165
162
|
end
|
|
166
163
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobinCMS
|
|
4
|
+
class CollectionLibrary < Library
|
|
5
|
+
def create(attributes)
|
|
6
|
+
id = make_slug(attributes[:title], @schema[:pattern])
|
|
7
|
+
item = Item.new(id, attributes, **@schema)
|
|
8
|
+
|
|
9
|
+
if File.exist?(filepath(id))
|
|
10
|
+
raise ItemExistsError, "An item with the same name already exists"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
write(item)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def find_one(id)
|
|
17
|
+
path = filepath(id)
|
|
18
|
+
|
|
19
|
+
return unless File.exist?(path)
|
|
20
|
+
|
|
21
|
+
deserialize(path)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def all
|
|
25
|
+
Dir.glob(filepath("*"))
|
|
26
|
+
.filter { |f| File.file?(f) }
|
|
27
|
+
.map { |path| deserialize(path) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def write(item)
|
|
31
|
+
path = filepath(item.id)
|
|
32
|
+
|
|
33
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
34
|
+
FileUtils.touch(path)
|
|
35
|
+
|
|
36
|
+
item.update_timestamps!
|
|
37
|
+
|
|
38
|
+
serialized = YAML.dump(item.frontmatter, stringify_names: true)
|
|
39
|
+
if item.attributes[:content]
|
|
40
|
+
serialized << "---\n"
|
|
41
|
+
serialized << item.attributes[:content]
|
|
42
|
+
end
|
|
43
|
+
File.write(path, serialized)
|
|
44
|
+
|
|
45
|
+
path
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def delete(id)
|
|
49
|
+
File.delete(filepath(id))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def filepath(id)
|
|
55
|
+
File.join(@schema[:location], "#{id}.#{@schema[:filetype]}")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def deserialize(path)
|
|
59
|
+
raw = File.read(path)
|
|
60
|
+
id = File.basename(path, File.extname(path))
|
|
61
|
+
|
|
62
|
+
_, frontmatter, content = raw.split("---")
|
|
63
|
+
|
|
64
|
+
attributes = YAML.load(frontmatter, symbolize_names: true)
|
|
65
|
+
attributes[:content] = content.strip
|
|
66
|
+
|
|
67
|
+
Item.new(id, attributes, **@schema)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"can_create": { "type": "boolean" },
|
|
51
51
|
"can_delete": { "type": "boolean" },
|
|
52
52
|
"pattern": { "type": "string" },
|
|
53
|
+
"display_name_pattern": { "type": "string" },
|
|
53
54
|
"fields": {
|
|
54
55
|
"type": "array",
|
|
55
56
|
"items": {
|
|
@@ -89,6 +90,7 @@
|
|
|
89
90
|
"can_create": { "type": "boolean" },
|
|
90
91
|
"can_delete": { "type": "boolean" },
|
|
91
92
|
"pattern": { "type": "string" },
|
|
93
|
+
"display_name_pattern": { "type": "string" },
|
|
92
94
|
"fields": {
|
|
93
95
|
"type": "array",
|
|
94
96
|
"items": {
|
|
@@ -102,27 +102,13 @@ module RobinCMS
|
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
def find_library(kind)
|
|
105
|
-
self[:libraries].find { |
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def sorted_fields(kind)
|
|
109
|
-
find_library(kind)
|
|
110
|
-
&.dig(:fields)
|
|
111
|
-
&.sort_by { |field| field[:order] || Float::INFINITY }
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def drafts_enabled?(library)
|
|
115
|
-
collection?(library)
|
|
105
|
+
self[:libraries].find { |library| library[:id] == kind }
|
|
116
106
|
end
|
|
117
107
|
|
|
118
108
|
def collection?(library)
|
|
119
109
|
library[:type].nil? || library[:type] == "collection"
|
|
120
110
|
end
|
|
121
111
|
|
|
122
|
-
def data?(library)
|
|
123
|
-
!collection?(library)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
112
|
def has_image?(library)
|
|
127
113
|
library[:fields].find { |f| f[:type] == "image" }
|
|
128
114
|
end
|
|
@@ -148,7 +134,7 @@ module RobinCMS
|
|
|
148
134
|
config[:libraries].each do |library|
|
|
149
135
|
library[:fields].append(*AUTOMATIC_FIELDS)
|
|
150
136
|
|
|
151
|
-
if
|
|
137
|
+
if collection?(library)
|
|
152
138
|
library[:fields].append(*AUTOMATIC_DRAFT_FIELDS)
|
|
153
139
|
end
|
|
154
140
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobinCMS
|
|
4
|
+
class DataLibrary < Library
|
|
5
|
+
def create(attributes)
|
|
6
|
+
item = Item.new(nil, attributes, **@schema)
|
|
7
|
+
write(item)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def find_one(id)
|
|
11
|
+
all[id.to_i]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def all
|
|
15
|
+
return [] unless File.exist?(filepath)
|
|
16
|
+
|
|
17
|
+
items = YAML.load_file(filepath, symbolize_names: true)
|
|
18
|
+
|
|
19
|
+
return [] unless items
|
|
20
|
+
|
|
21
|
+
items.each_with_index.map do |attrs, i|
|
|
22
|
+
Item.new(i, attrs, **@schema)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def write(item)
|
|
27
|
+
FileUtils.mkdir_p(File.dirname(filepath))
|
|
28
|
+
FileUtils.touch(filepath)
|
|
29
|
+
|
|
30
|
+
item.update_timestamps!
|
|
31
|
+
|
|
32
|
+
all_items = YAML.load_file(filepath) || []
|
|
33
|
+
if item.id.nil?
|
|
34
|
+
all_items.append(item.frontmatter)
|
|
35
|
+
else
|
|
36
|
+
all_items[item.id.to_i] = item.frontmatter
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
serialized = YAML.dump(all_items, stringify_names: true)
|
|
40
|
+
File.write(filepath, serialized)
|
|
41
|
+
|
|
42
|
+
filepath
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def delete(id)
|
|
46
|
+
all_items = YAML.load_file(filepath)
|
|
47
|
+
all_items[id.to_i] = nil
|
|
48
|
+
serialized = YAML.dump(all_items.compact, stringify_names: true)
|
|
49
|
+
File.write(filepath, serialized)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def filepath
|
|
55
|
+
File.join(@schema[:location], "#{@schema[:id]}.#{@schema[:filetype]}")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobinCMS
|
|
4
|
+
# This module provides methods for editing image files.
|
|
5
|
+
# No methods are required in the base class in order to use this mixin.
|
|
6
|
+
module Editable
|
|
7
|
+
def resize_image(filepath, to:)
|
|
8
|
+
# The mogrify command edits images in place. For more info, see
|
|
9
|
+
# mogrify(1).
|
|
10
|
+
system("mogrify -resize #{to} #{filepath}")
|
|
11
|
+
if $?.exitstatus != 0
|
|
12
|
+
raise ConversionError, "Could not resize image #{filepath}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def convert_image(filepath, to:)
|
|
17
|
+
system("mogrify -format #{to} #{filepath}")
|
|
18
|
+
if $?.exitstatus != 0
|
|
19
|
+
raise ConversionError, "Could not format image #{filepath}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The file created by the mogrify command will be the same as the
|
|
23
|
+
# original but with the new file extension.
|
|
24
|
+
converted = filepath.sub(/#{File.extname(filepath)}$/, ".#{to}")
|
|
25
|
+
|
|
26
|
+
if converted == filepath
|
|
27
|
+
# If the converted file has the same filename as the original (i.e. no
|
|
28
|
+
# conversion happened), there is nothing more to do.
|
|
29
|
+
filepath
|
|
30
|
+
else
|
|
31
|
+
# Otherwise, copy the contents of the mogrify output file to a new
|
|
32
|
+
# tempfile and delete the mogrify output.
|
|
33
|
+
tempfile = Tempfile.new(["robin_cms" ".#{to}"])
|
|
34
|
+
IO.copy_stream(converted, tempfile)
|
|
35
|
+
File.delete(converted)
|
|
36
|
+
tempfile
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/robin_cms/item.rb
CHANGED
|
@@ -1,34 +1,104 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RobinCMS
|
|
4
|
-
# This is a wrapper class for providing a uniform interface to both the
|
|
5
|
-
# DataItem class and the CollectionItem class. It's sole job is to dispatch
|
|
6
|
-
# methods to the appropriate class based on the settings of the library.
|
|
7
4
|
class Item
|
|
8
|
-
|
|
5
|
+
attr_reader :id, :attributes
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
DATETIME_FORMAT = "%Y-%m-%d"
|
|
8
|
+
|
|
9
|
+
# The keys which we don't want to serialize.
|
|
10
|
+
FRONTMATTER_IGNORE_KEYS = [:id, :content, :image, :captures].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(id, attrs = {}, **opts)
|
|
13
|
+
if !attrs.empty? && !attrs.has_key?(:title)
|
|
14
|
+
raise TypeError, "Missing required field `title'"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
@id = id
|
|
18
|
+
@opts = opts
|
|
19
|
+
|
|
20
|
+
# Be sure to use the setter here so the keys get converted to symbols.
|
|
21
|
+
self.attributes = attrs
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def attributes=(attributes)
|
|
25
|
+
@attributes = attributes.to_h.transform_keys(&:to_sym)
|
|
26
|
+
|
|
27
|
+
if attributes.has_key?(:published)
|
|
28
|
+
@attributes[:published] = attributes[:published].to_s == "true"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def inspect
|
|
33
|
+
"<#{self.class} id=\"#{id}\">"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def published?
|
|
37
|
+
if @attributes.has_key?(:published)
|
|
38
|
+
@attributes[:published]
|
|
39
|
+
else
|
|
40
|
+
true
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def published_label
|
|
45
|
+
if published?
|
|
46
|
+
"Published"
|
|
47
|
+
else
|
|
48
|
+
"Draft"
|
|
14
49
|
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def created_at
|
|
53
|
+
return unless @attributes[:created_at]
|
|
54
|
+
|
|
55
|
+
Time.parse(@attributes[:created_at])
|
|
56
|
+
end
|
|
15
57
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
library = args.first
|
|
58
|
+
def updated_at
|
|
59
|
+
return unless @attributes[:updated_at]
|
|
19
60
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
61
|
+
Time.parse(@attributes[:updated_at])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def update_timestamps!
|
|
65
|
+
timestamp = Time.now.strftime(DATETIME_FORMAT)
|
|
66
|
+
|
|
67
|
+
if !@attributes.has_key?(:created_at) || @attributes[:created_at].empty?
|
|
68
|
+
@attributes[:created_at] = timestamp
|
|
69
|
+
end
|
|
70
|
+
@attributes[:updated_at] = timestamp
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def display_name
|
|
74
|
+
return @attributes[:title] unless @opts[:display_name_pattern]
|
|
75
|
+
|
|
76
|
+
@opts[:display_name_pattern].clone.tap do |name|
|
|
77
|
+
@attributes.each { |key, value| name.gsub!(":#{key}", value) }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get the frontmatter as a hash with stringified keys.
|
|
82
|
+
def frontmatter
|
|
83
|
+
frontmatter = @attributes.clone
|
|
84
|
+
FRONTMATTER_IGNORE_KEYS.each { |key| frontmatter.delete(key) }
|
|
85
|
+
|
|
86
|
+
if published?
|
|
87
|
+
frontmatter.delete(:published)
|
|
25
88
|
else
|
|
26
|
-
|
|
89
|
+
frontmatter[:published] = false
|
|
27
90
|
end
|
|
91
|
+
|
|
92
|
+
# The Psych module (for which YAML is an alias) has a
|
|
93
|
+
# stringify_names option which does exactly this. However it was
|
|
94
|
+
# only introduced in Ruby 3.4. Using transform_keys is a workaround
|
|
95
|
+
# to support earlier versions of Ruby.
|
|
96
|
+
frontmatter.to_h.transform_keys(&:to_s)
|
|
28
97
|
end
|
|
29
98
|
|
|
30
|
-
def
|
|
31
|
-
|
|
99
|
+
def field_value_or_default(field)
|
|
100
|
+
value = @attributes[field[:id].to_sym] || field[:default]
|
|
101
|
+
value.to_s
|
|
32
102
|
end
|
|
33
103
|
end
|
|
34
104
|
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobinCMS
|
|
4
|
+
class Library
|
|
5
|
+
include Editable
|
|
6
|
+
include Sluggable
|
|
7
|
+
include Queryable
|
|
8
|
+
|
|
9
|
+
attr_reader :schema
|
|
10
|
+
|
|
11
|
+
def initialize(schema)
|
|
12
|
+
@schema = schema.freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def blank
|
|
16
|
+
Item.new(nil, **@schema)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create(attributes)
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def find_one(id)
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def all
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Writes the specified item to disk.
|
|
32
|
+
#
|
|
33
|
+
# Returns the path where the item was written.
|
|
34
|
+
def write(item)
|
|
35
|
+
raise NotImplementedError
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Delete the item specified by +id+.
|
|
39
|
+
def delete(id)
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def create_static(filename, tempfile)
|
|
44
|
+
path = File.join(@schema[:static_location], File.basename(make_slug(filename)))
|
|
45
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
46
|
+
|
|
47
|
+
if File.exist?(path)
|
|
48
|
+
raise ItemExistsError, "An item with the same name already exists"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
image_field = @schema[:fields].find { |f| f[:type] == "image" }
|
|
52
|
+
dimensions = image_field&.dig(:dimensions)
|
|
53
|
+
filetype = image_field&.dig(:filetype)
|
|
54
|
+
|
|
55
|
+
resize_image(tempfile, to: dimensions) if dimensions
|
|
56
|
+
|
|
57
|
+
# Only convert if the specified filetype is different to the actual
|
|
58
|
+
# filetype. Unlike resize_image which modifies the image in-pace, this
|
|
59
|
+
# function necessarily creates a new file, so we need to ensure we
|
|
60
|
+
# update the path with the new file extension.
|
|
61
|
+
if filetype && File.extname(tempfile) != ".#{filetype}"
|
|
62
|
+
tempfile = convert_image(tempfile, to: filetype)
|
|
63
|
+
path = File.join(File.dirname(path), "#{File.basename(path, '.*')}.#{filetype}")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
FileUtils.cp(tempfile, path)
|
|
67
|
+
path
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def delete_static(filename)
|
|
71
|
+
return if filename.nil? || filename.empty?
|
|
72
|
+
path = File.join(@schema[:static_location], File.basename(filename))
|
|
73
|
+
return unless File.exist?(path)
|
|
74
|
+
|
|
75
|
+
File.delete(path)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def drafts_enabled?
|
|
79
|
+
@schema[:type].nil? || @schema[:type] == "collection"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def sorted_fields
|
|
83
|
+
@schema[:fields].sort_by { |field| field[:order] || Float::INFINITY }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def [](key)
|
|
87
|
+
@schema[key]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def kind
|
|
91
|
+
@schema[:id]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/robin_cms/queryable.rb
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RobinCMS
|
|
4
|
+
# This module provides methods for querying items.
|
|
5
|
+
#
|
|
6
|
+
# In order to use this as a mixin, you only need to implement an +all+
|
|
7
|
+
# method in the base class. This method should take no parameters, and
|
|
8
|
+
# return all items.
|
|
4
9
|
module Queryable
|
|
5
|
-
def
|
|
10
|
+
def query(**kwargs)
|
|
6
11
|
by_published = lambda do |i|
|
|
7
12
|
case kwargs[:published]
|
|
8
13
|
when nil, ""
|
|
@@ -34,14 +39,11 @@ module RobinCMS
|
|
|
34
39
|
end * sort_direction
|
|
35
40
|
end
|
|
36
41
|
|
|
37
|
-
all(
|
|
38
|
-
.filter(&by_search)
|
|
39
|
-
.filter(&by_published)
|
|
40
|
-
.sort(&by_field)
|
|
42
|
+
all.filter(&by_search).filter(&by_published).sort(&by_field)
|
|
41
43
|
end
|
|
42
44
|
|
|
43
|
-
def count
|
|
44
|
-
all
|
|
45
|
+
def count
|
|
46
|
+
all.size
|
|
45
47
|
end
|
|
46
48
|
end
|
|
47
49
|
end
|
data/lib/robin_cms/version.rb
CHANGED