robin_cms 0.1.7 → 0.3.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 +8 -3
- data/lib/robin_cms/cms.rb +34 -20
- data/lib/robin_cms/configuration.rb +61 -16
- data/lib/robin_cms/configuration_schema.rb +109 -0
- data/lib/robin_cms/data_library.rb +10 -3
- data/lib/robin_cms/helpers.rb +46 -10
- data/lib/robin_cms/item.rb +65 -24
- data/lib/robin_cms/library.rb +26 -1
- data/lib/robin_cms/singular_library.rb +68 -0
- data/lib/robin_cms/version.rb +1 -1
- data/lib/robin_cms/views/change_password.erb +1 -1
- data/lib/robin_cms/views/image_field.erb +0 -1
- data/lib/robin_cms/views/layout.erb +10 -1
- data/lib/robin_cms/views/library.erb +4 -5
- data/lib/robin_cms/views/library_actions.erb +3 -1
- data/lib/robin_cms/views/library_item.erb +3 -2
- data/lib/robin_cms/views/new_tab.svg.erb +0 -1
- data/lib/robin_cms/views/profile.erb +1 -1
- data/lib/robin_cms/views/richtext_field.erb +88 -2
- data/lib/robin_cms/views/select_field.erb +1 -1
- data/lib/robin_cms/views/stylesheet.css.erb +2 -2
- data/lib/robin_cms/views/trix.css.erb +75 -0
- data/lib/robin_cms.rb +5 -0
- metadata +6 -4
- data/lib/robin_cms/configuration-schema.json +0 -120
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 26f13be1b3e97bce69e705daf56e969916467591b7c30b2877c2ce29e07e62d3
|
|
4
|
+
data.tar.gz: 7f34fbee635184285025e757e3c52ef8e25467255ed4a90647203bbfcd656756
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a420e205f0f06d34817b5cb1424f513051cddb7a665dcddb9d707c091088969e7031bef154542347691fe109a612f58741c229f622380281e4289a2bcfa1a268
|
|
7
|
+
data.tar.gz: 7d1e1bbd2b23d02c02a7dcca004c35015663bb2ece865f7d4ebfabbcd96e2897bc95bd758bd64145cb70d5ef2f7819717d195417f7121254080773b1f70a9f43
|
data/README.md
CHANGED
|
@@ -72,7 +72,8 @@ standalone CMS for any Static Site Generator, you can embed it in a dynamic
|
|
|
72
72
|
Sinatra app, or you can use it as a Jekyll plugin. If you're already using
|
|
73
73
|
Jekyll as your SSG, this is the simplest approach.
|
|
74
74
|
|
|
75
|
-
See the [documentation](
|
|
75
|
+
See the [documentation](https://robincms.org/docs.html) for further information
|
|
76
|
+
on usage.
|
|
76
77
|
|
|
77
78
|
## Configuring
|
|
78
79
|
|
|
@@ -100,8 +101,8 @@ schema for your content. An example YAML file looks like this:
|
|
|
100
101
|
- { label: Title, id: title, type: input }
|
|
101
102
|
- { label: Author, id: author_name, type: input }
|
|
102
103
|
|
|
103
|
-
See the [docs](
|
|
104
|
-
also find some examples [here](./examples).
|
|
104
|
+
See the [docs](https://robincms.org/docs.html) for more details on
|
|
105
|
+
configuration. You can also find some examples [here](./examples).
|
|
105
106
|
|
|
106
107
|
## Testing
|
|
107
108
|
|
|
@@ -109,6 +110,10 @@ Unit test are written in RSpec. To run them:
|
|
|
109
110
|
|
|
110
111
|
rake test
|
|
111
112
|
|
|
113
|
+
## Screenshot
|
|
114
|
+
|
|
115
|
+

|
|
116
|
+
|
|
112
117
|
## Roadmap
|
|
113
118
|
|
|
114
119
|
* [ ] Support markdown content
|
data/lib/robin_cms/cms.rb
CHANGED
|
@@ -37,7 +37,7 @@ module RobinCMS
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
get "/" do
|
|
40
|
-
redirect to(
|
|
40
|
+
redirect to(home)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
get "/login" do
|
|
@@ -47,7 +47,7 @@ module RobinCMS
|
|
|
47
47
|
post "/login" do
|
|
48
48
|
authenticate!(params[:username], params[:password])
|
|
49
49
|
session[:auth_user] = params[:username]
|
|
50
|
-
redirect to(
|
|
50
|
+
redirect to(home)
|
|
51
51
|
rescue AuthenticationError
|
|
52
52
|
flash[:error] = "Incorrect username or password"
|
|
53
53
|
redirect to("/login")
|
|
@@ -90,6 +90,11 @@ module RobinCMS
|
|
|
90
90
|
published: params[:published],
|
|
91
91
|
q: params[:q]
|
|
92
92
|
)
|
|
93
|
+
|
|
94
|
+
if @library.singular?
|
|
95
|
+
redirect to("/libraries/#{params[:kind]}/item/0")
|
|
96
|
+
end
|
|
97
|
+
|
|
93
98
|
erb :library
|
|
94
99
|
end
|
|
95
100
|
|
|
@@ -154,6 +159,21 @@ module RobinCMS
|
|
|
154
159
|
redirect to("/libraries/#{params[:kind]}")
|
|
155
160
|
end
|
|
156
161
|
|
|
162
|
+
# Handle uploading attachments from the trix editor. Responds with the path
|
|
163
|
+
# of the uploaded file in JSON format.
|
|
164
|
+
post "/libraries/:kind/attachment" do
|
|
165
|
+
static_path = @library.create_static(
|
|
166
|
+
params[:key],
|
|
167
|
+
params[:file][:tempfile].to_path
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
{ :url => "/" + static_path }.to_json
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
post "/libraries/:kind/attachment/delete" do
|
|
174
|
+
@library.delete_static(params[:key])
|
|
175
|
+
end
|
|
176
|
+
|
|
157
177
|
post "/publish" do
|
|
158
178
|
system(@config[:build_command])
|
|
159
179
|
if $?.exitstatus == 0
|
|
@@ -162,27 +182,21 @@ module RobinCMS
|
|
|
162
182
|
flash[:error_dialog] = "There was an error publishing the site."
|
|
163
183
|
end
|
|
164
184
|
|
|
165
|
-
redirect to(
|
|
185
|
+
redirect to(home)
|
|
166
186
|
end
|
|
167
187
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
# the app is mounted at i.e. the virtual location.
|
|
178
|
-
uri = env["PATH_INFO"]
|
|
179
|
-
|
|
180
|
-
library = @config[:libraries].find do |c|
|
|
181
|
-
uri.start_with?("/#{c[:static_location]}")
|
|
182
|
-
end
|
|
188
|
+
# Explicitly serve static files for previewing in the CMS. This is
|
|
189
|
+
# necessary because until you publish the site, static files are not copied
|
|
190
|
+
# to the site output, and therefore requests for the file will return a
|
|
191
|
+
# 404. Note that if using it as a Jekyll plugin, this is not needed as the
|
|
192
|
+
# files will be served by Jekyll's dev server.
|
|
193
|
+
#
|
|
194
|
+
# This route needs to be defined last since it catches a wide range of URLs.
|
|
195
|
+
get "/:static_location/:key" do
|
|
196
|
+
static_locations = @config[:libraries].map { |l| l[:static_location] }.uniq
|
|
183
197
|
|
|
184
|
-
if
|
|
185
|
-
send_file(File.join(
|
|
198
|
+
if static_locations.include?(params[:static_location])
|
|
199
|
+
send_file(File.join(params[:static_location], params[:key]))
|
|
186
200
|
else
|
|
187
201
|
pass
|
|
188
202
|
end
|
|
@@ -10,9 +10,10 @@ module RobinCMS
|
|
|
10
10
|
|
|
11
11
|
COLLECTION_DEFAULTS = {
|
|
12
12
|
type: "collection",
|
|
13
|
-
location: "
|
|
14
|
-
static_location: "
|
|
13
|
+
location: "_posts",
|
|
14
|
+
static_location: "uploads",
|
|
15
15
|
filetype: "html",
|
|
16
|
+
pattern: ":year-:month-:day-:title",
|
|
16
17
|
description: "",
|
|
17
18
|
can_create: true,
|
|
18
19
|
can_delete: true,
|
|
@@ -21,8 +22,8 @@ module RobinCMS
|
|
|
21
22
|
|
|
22
23
|
DATA_DEFAULTS = {
|
|
23
24
|
type: "data",
|
|
24
|
-
location: "
|
|
25
|
-
static_location: "
|
|
25
|
+
location: "_data",
|
|
26
|
+
static_location: "uploads",
|
|
26
27
|
filetype: "yml",
|
|
27
28
|
description: "",
|
|
28
29
|
can_create: true,
|
|
@@ -30,6 +31,17 @@ module RobinCMS
|
|
|
30
31
|
fields: []
|
|
31
32
|
}.freeze
|
|
32
33
|
|
|
34
|
+
SINGULAR_DEFAULTS = {
|
|
35
|
+
type: "singular",
|
|
36
|
+
location: "_data",
|
|
37
|
+
static_location: "uploads",
|
|
38
|
+
filetype: "yml",
|
|
39
|
+
description: "",
|
|
40
|
+
can_create: false,
|
|
41
|
+
can_delete: false,
|
|
42
|
+
fields: []
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
33
45
|
FIELD_DEFAULTS = {
|
|
34
46
|
type: "text",
|
|
35
47
|
default: nil,
|
|
@@ -54,11 +66,10 @@ module RobinCMS
|
|
|
54
66
|
type: "hidden"
|
|
55
67
|
}].freeze
|
|
56
68
|
|
|
57
|
-
|
|
69
|
+
AUTOMATIC_COLLECTION_FIELDS = [{
|
|
58
70
|
label: "Published",
|
|
59
71
|
id: "published",
|
|
60
72
|
type: "select",
|
|
61
|
-
default: false,
|
|
62
73
|
options: [{
|
|
63
74
|
label: "Draft",
|
|
64
75
|
value: false
|
|
@@ -72,6 +83,14 @@ module RobinCMS
|
|
|
72
83
|
order: 2
|
|
73
84
|
}].freeze
|
|
74
85
|
|
|
86
|
+
AUTOMATIC_SINGULAR_FIELDS = [{
|
|
87
|
+
label: "Title",
|
|
88
|
+
id: "title",
|
|
89
|
+
type: "hidden",
|
|
90
|
+
required: true,
|
|
91
|
+
order: 1
|
|
92
|
+
}]
|
|
93
|
+
|
|
75
94
|
AUTOMATIC_IMAGE_FIELDS = [{
|
|
76
95
|
id: "image_src",
|
|
77
96
|
type: "hidden"
|
|
@@ -94,9 +113,13 @@ module RobinCMS
|
|
|
94
113
|
YAML.load_file(config_file, symbolize_names: true)
|
|
95
114
|
end
|
|
96
115
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
116
|
+
schema = ConfigurationSchema.schema
|
|
117
|
+
errors = JSON::Validator.fully_validate(schema, data)
|
|
118
|
+
|
|
119
|
+
unless errors.empty?
|
|
120
|
+
puts errors
|
|
121
|
+
raise ValidationError, "There is an error in your configuration file"
|
|
122
|
+
end
|
|
100
123
|
|
|
101
124
|
config = from_h(DEFAULTS).merge_config(data).merge_automatic_fields
|
|
102
125
|
config.validate!
|
|
@@ -115,15 +138,24 @@ module RobinCMS
|
|
|
115
138
|
library[:type].nil? || library[:type] == "collection"
|
|
116
139
|
end
|
|
117
140
|
|
|
118
|
-
def
|
|
119
|
-
library[:
|
|
141
|
+
def data?(library)
|
|
142
|
+
library[:type] == "data"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def singular?(library)
|
|
146
|
+
library[:type] == "singular"
|
|
120
147
|
end
|
|
121
148
|
|
|
122
149
|
def merge_config(config)
|
|
123
150
|
merge(config, {
|
|
124
151
|
libraries: config[:libraries].map do |library|
|
|
125
|
-
library_defaults =
|
|
126
|
-
|
|
152
|
+
library_defaults = if collection?(library)
|
|
153
|
+
COLLECTION_DEFAULTS
|
|
154
|
+
elsif data?(library)
|
|
155
|
+
DATA_DEFAULTS
|
|
156
|
+
elsif singular?(library)
|
|
157
|
+
SINGULAR_DEFAULTS
|
|
158
|
+
end
|
|
127
159
|
|
|
128
160
|
library_defaults.merge(library, {
|
|
129
161
|
fields: library[:fields].map do |field|
|
|
@@ -141,11 +173,22 @@ module RobinCMS
|
|
|
141
173
|
library[:fields].append(*AUTOMATIC_FIELDS)
|
|
142
174
|
|
|
143
175
|
if collection?(library)
|
|
144
|
-
library[:fields].append(*
|
|
176
|
+
library[:fields].append(*AUTOMATIC_COLLECTION_FIELDS)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Replace the automatic title field with a hidden one. The title is not
|
|
180
|
+
# used for singular fields, but every item is required to have one, so
|
|
181
|
+
# hide it in the UI.
|
|
182
|
+
if singular?(library)
|
|
183
|
+
# Using prepend ensures this one stays when we call uniq! below.
|
|
184
|
+
library[:fields].prepend(*AUTOMATIC_SINGULAR_FIELDS)
|
|
145
185
|
end
|
|
146
186
|
|
|
147
|
-
|
|
148
|
-
|
|
187
|
+
# Ensure the automatic image fields are inserted directly after the
|
|
188
|
+
# declared image field.
|
|
189
|
+
image_index = library[:fields].find_index { |f| f[:type] == "image" }
|
|
190
|
+
if image_index
|
|
191
|
+
library[:fields].insert(image_index + 1, *AUTOMATIC_IMAGE_FIELDS)
|
|
149
192
|
end
|
|
150
193
|
|
|
151
194
|
library[:fields].uniq! { |f| f[:id] }
|
|
@@ -156,6 +199,8 @@ module RobinCMS
|
|
|
156
199
|
|
|
157
200
|
# This method provides custom validation which can't be expressed (or is
|
|
158
201
|
# too difficult to express) using the Json Schema standard.
|
|
202
|
+
#
|
|
203
|
+
# TODO: validate that options are given if the field type is select.
|
|
159
204
|
def validate!
|
|
160
205
|
self[:libraries].each do |library|
|
|
161
206
|
next if library[:fields].nil?
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal => true
|
|
2
|
+
|
|
3
|
+
module RobinCMS
|
|
4
|
+
module ConfigurationSchema
|
|
5
|
+
def self.field_schema(kind)
|
|
6
|
+
field_types = [
|
|
7
|
+
"text",
|
|
8
|
+
"date",
|
|
9
|
+
"hidden",
|
|
10
|
+
"number",
|
|
11
|
+
"color",
|
|
12
|
+
"email",
|
|
13
|
+
"url",
|
|
14
|
+
"select",
|
|
15
|
+
"image"
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
if kind == "collection"
|
|
19
|
+
field_types << "richtext"
|
|
20
|
+
field_types << "array"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
"type" => "object",
|
|
25
|
+
"required" => ["id", "label"],
|
|
26
|
+
"properties" => {
|
|
27
|
+
"id" => { "type" => "string" },
|
|
28
|
+
"label" => { "type" => "string" },
|
|
29
|
+
"type" => { "type" => "string", "enum" => field_types },
|
|
30
|
+
"default" => { "type" => "string" },
|
|
31
|
+
"required" => { "type" => "boolean" },
|
|
32
|
+
"readonly" => { "type" => "boolean" },
|
|
33
|
+
"option" => {
|
|
34
|
+
"type" => "array",
|
|
35
|
+
"items" => {
|
|
36
|
+
"type" => "object",
|
|
37
|
+
"required" => ["label", "value"],
|
|
38
|
+
"properties" => {
|
|
39
|
+
"label" => { "type" => "string" },
|
|
40
|
+
"value" => { "type" => "string" }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"dimensions" => { "type" => "string", "pattern" => "^[0-9]+x[0-9]+$" },
|
|
45
|
+
"filetype" => { "type" => "string", "enum" => ["png", "jpg"] },
|
|
46
|
+
"description" => { "type" => "string" },
|
|
47
|
+
"order" => { "type" => "number" }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.library_schema(kind)
|
|
53
|
+
filetypes = case kind
|
|
54
|
+
when "collection"
|
|
55
|
+
["html"]
|
|
56
|
+
when "data", "singular"
|
|
57
|
+
["yml"]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
{
|
|
61
|
+
"type" => "object",
|
|
62
|
+
"required" => ["id", "label", "label_singular"],
|
|
63
|
+
"properties" => {
|
|
64
|
+
"id" => { "type" => "string" },
|
|
65
|
+
"type" => { "type" => "string", "const" => kind },
|
|
66
|
+
"filetype" => { "type" => "string", "enum" => filetypes },
|
|
67
|
+
"label" => { "type" => "string" },
|
|
68
|
+
"label_singular" => { "type" => "string" },
|
|
69
|
+
"location" => { "type" => "string" },
|
|
70
|
+
"static_location" => { "type" => "string" },
|
|
71
|
+
"description" => { "type" => "string" },
|
|
72
|
+
"can_create" => { "type" => "boolean" },
|
|
73
|
+
"can_delete" => { "type" => "boolean" },
|
|
74
|
+
"pattern" => { "type" => "string" },
|
|
75
|
+
"display_name_pattern" => { "type" => "string" },
|
|
76
|
+
"fields" => { "type" => "array", "items" => field_schema(kind) }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.schema
|
|
82
|
+
{
|
|
83
|
+
"$schema" => "http://json-schema.org/draft-06/schema#",
|
|
84
|
+
"title" => "RobinCMS configuration schema",
|
|
85
|
+
"type" => "object",
|
|
86
|
+
"required" => ["url", "libraries"],
|
|
87
|
+
"properties" => {
|
|
88
|
+
"title" => { "type" => "string" },
|
|
89
|
+
"url" => { "type" => "string" },
|
|
90
|
+
"build_command" => { "type" => "string" },
|
|
91
|
+
"accent_color" => {
|
|
92
|
+
"type" => "string",
|
|
93
|
+
"pattern" => '^\\#{1}[a-fA-F0-9]{6}'
|
|
94
|
+
},
|
|
95
|
+
"libraries" => {
|
|
96
|
+
"type" => "array",
|
|
97
|
+
"items" => {
|
|
98
|
+
"anyOf" => [
|
|
99
|
+
library_schema("collection"),
|
|
100
|
+
library_schema("data"),
|
|
101
|
+
library_schema("singular")
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -14,10 +14,11 @@ module RobinCMS
|
|
|
14
14
|
def all
|
|
15
15
|
return [] unless File.exist?(filepath)
|
|
16
16
|
|
|
17
|
-
items = YAML.load_file(filepath, symbolize_names: true)
|
|
17
|
+
items = YAML.load_file(filepath, symbolize_names: true, permitted_classes: [Date])
|
|
18
18
|
|
|
19
19
|
return [] unless items
|
|
20
20
|
|
|
21
|
+
# Use the index of the item as it's id.
|
|
21
22
|
items.each_with_index.map do |attrs, i|
|
|
22
23
|
Item.new(i, attrs, **@schema)
|
|
23
24
|
end
|
|
@@ -29,11 +30,17 @@ module RobinCMS
|
|
|
29
30
|
|
|
30
31
|
item.update_timestamps!
|
|
31
32
|
|
|
33
|
+
# The item's id is it's index in the YAML array. If the id is nil, we are
|
|
34
|
+
# creating a new item; append it to the end of the array. If the id is
|
|
35
|
+
# not nil, we are editing an item; swap out the item at the index
|
|
36
|
+
# corresponding to it's id.
|
|
32
37
|
all_items = YAML.load_file(filepath) || []
|
|
33
38
|
if item.id.nil?
|
|
34
|
-
|
|
39
|
+
# Data items don't have a published state.
|
|
40
|
+
all_items.append(item.frontmatter(ignore_published: true))
|
|
35
41
|
else
|
|
36
|
-
|
|
42
|
+
# As above.
|
|
43
|
+
all_items[item.id.to_i] = item.frontmatter(ignore_published: true)
|
|
37
44
|
end
|
|
38
45
|
|
|
39
46
|
serialized = YAML.dump(all_items, stringify_names: true)
|
data/lib/robin_cms/helpers.rb
CHANGED
|
@@ -37,44 +37,64 @@ module RobinCMS
|
|
|
37
37
|
URI.decode_www_form(request.query_string).to_h
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
def home
|
|
41
|
+
"/libraries/#{@config[:libraries].first[:id]}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def path(addr)
|
|
45
|
+
url(addr, absolute = false)
|
|
46
|
+
end
|
|
47
|
+
|
|
40
48
|
def root_url
|
|
41
|
-
|
|
49
|
+
path("/")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def home_url
|
|
53
|
+
path(home)
|
|
42
54
|
end
|
|
43
55
|
|
|
44
56
|
def profile_url
|
|
45
|
-
|
|
57
|
+
path("/profile")
|
|
46
58
|
end
|
|
47
59
|
|
|
48
60
|
def logout_url
|
|
49
|
-
|
|
61
|
+
path("/logout")
|
|
50
62
|
end
|
|
51
63
|
|
|
52
64
|
def change_password_url
|
|
53
|
-
|
|
65
|
+
path("/change-password")
|
|
54
66
|
end
|
|
55
67
|
|
|
56
68
|
def library_url(library)
|
|
57
|
-
|
|
69
|
+
path("/libraries/#{library[:id]}")
|
|
58
70
|
end
|
|
59
71
|
|
|
60
72
|
def library_item_url(library, item = nil)
|
|
61
73
|
if item
|
|
62
|
-
|
|
74
|
+
path("/libraries/#{library[:id]}/item/#{item.id}")
|
|
63
75
|
else
|
|
64
|
-
|
|
76
|
+
path("/libraries/#{library[:id]}/item")
|
|
65
77
|
end
|
|
66
78
|
end
|
|
67
79
|
|
|
68
80
|
def delete_library_item_url(library, item)
|
|
69
|
-
|
|
81
|
+
path("/libraries/#{library[:id]}/item/#{item.id}/delete")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def library_attachment_url(library)
|
|
85
|
+
path("/libraries/#{library[:id]}/attachment")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def delete_library_attachment_url(library)
|
|
89
|
+
path("/libraries/#{library[:id]}/attachment/delete")
|
|
70
90
|
end
|
|
71
91
|
|
|
72
92
|
def item_image_url(item)
|
|
73
|
-
|
|
93
|
+
path(item.image_src)
|
|
74
94
|
end
|
|
75
95
|
|
|
76
96
|
def publish_url
|
|
77
|
-
|
|
97
|
+
path("/publish")
|
|
78
98
|
end
|
|
79
99
|
|
|
80
100
|
def current_page?(library)
|
|
@@ -90,6 +110,22 @@ module RobinCMS
|
|
|
90
110
|
"__#{prefix}_#{id}"
|
|
91
111
|
end
|
|
92
112
|
|
|
113
|
+
# The attachments for Trix need to be served from the admin site root
|
|
114
|
+
# (usually "/admin") in order for the catch-all route handler to be able
|
|
115
|
+
# to serve them.
|
|
116
|
+
#
|
|
117
|
+
# TODO: You can set a "preview URL" for Trix (see
|
|
118
|
+
# https://github.com/basecamp/trix#previewing-attached-files). But I
|
|
119
|
+
# couldn't get it to work.
|
|
120
|
+
def escape_for_trix(value)
|
|
121
|
+
value = value
|
|
122
|
+
.sub(""href":"/uploads", ""href":"#{root_url}uploads")
|
|
123
|
+
.sub(""url":"/uploads", ""url":"#{root_url}uploads")
|
|
124
|
+
.sub("href=\"/uploads", "href=\"#{root_url}uploads")
|
|
125
|
+
.sub("url=\"/uploads", "url=\"#{root_url}uploads")
|
|
126
|
+
CGI::escapeHTML(value)
|
|
127
|
+
end
|
|
128
|
+
|
|
93
129
|
def flash
|
|
94
130
|
@flash ||= Flash.new(session ? session[:flash] : {})
|
|
95
131
|
end
|
data/lib/robin_cms/item.rb
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RobinCMS
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
# The in-memory data model for all items.
|
|
5
|
+
#
|
|
6
|
+
# The Item objects model the domain, while the Library objects model disk
|
|
7
|
+
# IO/persistence. Since the distinction between Data and Collection items is
|
|
8
|
+
# purely a distinction of how they are written to disk, this is abstracted
|
|
9
|
+
# entirely by the Library objects and therefore Item's don't care what type
|
|
10
|
+
# they are.
|
|
11
|
+
#
|
|
12
|
+
# There are two boundaries for serialization/deserialization of items. One is
|
|
13
|
+
# to/from the file used to store the data on disk. This happens in the
|
|
14
|
+
# find_one, all, and write methods of the Library classes. The other is
|
|
15
|
+
# to/from the html representation of the item. This happens in the
|
|
16
|
+
# attributes= and field_value_or_default methods in this class.
|
|
17
|
+
class Item attr_reader :id, :attributes, :schema
|
|
6
18
|
|
|
7
19
|
DATETIME_FORMAT = "%Y-%m-%d"
|
|
8
20
|
|
|
@@ -17,15 +29,49 @@ module RobinCMS
|
|
|
17
29
|
@id = id
|
|
18
30
|
@schema = schema
|
|
19
31
|
|
|
20
|
-
# Be sure to use the setter here
|
|
32
|
+
# Be sure to use the setter here.
|
|
21
33
|
self.attributes = attrs
|
|
22
34
|
end
|
|
23
35
|
|
|
24
36
|
def attributes=(attributes)
|
|
25
37
|
@attributes = attributes.to_h.transform_keys(&:to_sym)
|
|
26
38
|
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
# Parse array fields.
|
|
40
|
+
@schema[:fields].each do |f|
|
|
41
|
+
next unless f[:type] == "array"
|
|
42
|
+
|
|
43
|
+
field_id = f[:id].to_sym
|
|
44
|
+
if @attributes[field_id] && !@attributes[field_id].is_a?(Array)
|
|
45
|
+
@attributes[field_id] = @attributes[field_id].split(",").map(&:strip)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Parse published status. The logic here is a bit weird, but basically an
|
|
50
|
+
# item without a 'published' key is considered published. Draft times are
|
|
51
|
+
# marked with 'published: false'. This is in line with how it works in
|
|
52
|
+
# Jekyll.
|
|
53
|
+
if blank?
|
|
54
|
+
@attributes[:published] = false # New items always default to draft.
|
|
55
|
+
elsif attributes.has_key?(:published)
|
|
56
|
+
@attributes[:published] = attributes[:published].to_s != "false"
|
|
57
|
+
else
|
|
58
|
+
@attributes[:published] = true
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def field_value_or_default(field_id)
|
|
63
|
+
field = @schema[:fields].find { |f| f[:id] == field_id }
|
|
64
|
+
value = @attributes[field[:id].to_sym]
|
|
65
|
+
|
|
66
|
+
# Explicitly check for nil here because a field might have a value of
|
|
67
|
+
# false.
|
|
68
|
+
value = field[:default] if value.nil?
|
|
69
|
+
|
|
70
|
+
case field[:type]
|
|
71
|
+
when "array"
|
|
72
|
+
value&.join(",")
|
|
73
|
+
else
|
|
74
|
+
value.to_s
|
|
29
75
|
end
|
|
30
76
|
end
|
|
31
77
|
|
|
@@ -34,11 +80,11 @@ module RobinCMS
|
|
|
34
80
|
end
|
|
35
81
|
|
|
36
82
|
def published?
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
83
|
+
@attributes[:published]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def blank?
|
|
87
|
+
@id.nil?
|
|
42
88
|
end
|
|
43
89
|
|
|
44
90
|
def published_label
|
|
@@ -73,8 +119,8 @@ module RobinCMS
|
|
|
73
119
|
def display_name
|
|
74
120
|
return @attributes[:title] unless @schema[:display_name_pattern]
|
|
75
121
|
|
|
76
|
-
@schema[:display_name_pattern].
|
|
77
|
-
@attributes.each { |key, value| name.gsub!(":#{key}", value) }
|
|
122
|
+
@schema[:display_name_pattern].dup.tap do |name|
|
|
123
|
+
@attributes.each { |key, value| name.gsub!(":#{key}", value.to_s) }
|
|
78
124
|
end
|
|
79
125
|
end
|
|
80
126
|
|
|
@@ -86,12 +132,17 @@ module RobinCMS
|
|
|
86
132
|
@attributes[:image_alt]
|
|
87
133
|
end
|
|
88
134
|
|
|
135
|
+
def can_delete?
|
|
136
|
+
@id && @schema[:can_delete]
|
|
137
|
+
end
|
|
138
|
+
|
|
89
139
|
# Get the frontmatter as a hash with stringified keys.
|
|
90
|
-
def frontmatter
|
|
140
|
+
def frontmatter(ignore_published: false)
|
|
91
141
|
frontmatter = @attributes.clone
|
|
92
142
|
FRONTMATTER_IGNORE_KEYS.each { |key| frontmatter.delete(key) }
|
|
93
143
|
|
|
94
|
-
|
|
144
|
+
# See comment in the attributes= method.
|
|
145
|
+
if ignore_published || published?
|
|
95
146
|
frontmatter.delete(:published)
|
|
96
147
|
else
|
|
97
148
|
frontmatter[:published] = false
|
|
@@ -103,15 +154,5 @@ module RobinCMS
|
|
|
103
154
|
# to support earlier versions of Ruby.
|
|
104
155
|
frontmatter.to_h.transform_keys(&:to_s)
|
|
105
156
|
end
|
|
106
|
-
|
|
107
|
-
def field_value_or_default(field_id)
|
|
108
|
-
field = @schema[:fields].find { |f| f[:id] == field_id }
|
|
109
|
-
value = @attributes[field[:id].to_sym] || field[:default]
|
|
110
|
-
value.to_s
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def can_delete?
|
|
114
|
-
@id && @schema[:can_delete]
|
|
115
|
-
end
|
|
116
157
|
end
|
|
117
158
|
end
|