populate-me 0.12.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/.gitignore +9 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +655 -0
- data/Rakefile +14 -0
- data/example/config.ru +100 -0
- data/lib/populate_me.rb +2 -0
- data/lib/populate_me/admin.rb +157 -0
- data/lib/populate_me/admin/__assets__/css/asmselect.css +63 -0
- data/lib/populate_me/admin/__assets__/css/jquery-ui.min.css +6 -0
- data/lib/populate_me/admin/__assets__/css/main.css +244 -0
- data/lib/populate_me/admin/__assets__/img/help/children.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/create.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/delete.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/edit.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/form.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/list.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/login.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/logout.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/menu.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/overview.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/save.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/sort.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/sublist.png +0 -0
- data/lib/populate_me/admin/__assets__/js/asmselect.js +412 -0
- data/lib/populate_me/admin/__assets__/js/columnav.js +87 -0
- data/lib/populate_me/admin/__assets__/js/jquery-ui.min.js +7 -0
- data/lib/populate_me/admin/__assets__/js/main.js +388 -0
- data/lib/populate_me/admin/__assets__/js/mustache.js +578 -0
- data/lib/populate_me/admin/__assets__/js/sortable.js +2 -0
- data/lib/populate_me/admin/views/help.erb +94 -0
- data/lib/populate_me/admin/views/page.erb +189 -0
- data/lib/populate_me/api.rb +124 -0
- data/lib/populate_me/attachment.rb +186 -0
- data/lib/populate_me/document.rb +192 -0
- data/lib/populate_me/document_mixins/admin_adapter.rb +149 -0
- data/lib/populate_me/document_mixins/callbacks.rb +125 -0
- data/lib/populate_me/document_mixins/outcasting.rb +83 -0
- data/lib/populate_me/document_mixins/persistence.rb +95 -0
- data/lib/populate_me/document_mixins/schema.rb +198 -0
- data/lib/populate_me/document_mixins/typecasting.rb +70 -0
- data/lib/populate_me/document_mixins/validation.rb +44 -0
- data/lib/populate_me/file_system_attachment.rb +40 -0
- data/lib/populate_me/grid_fs_attachment.rb +103 -0
- data/lib/populate_me/mongo.rb +160 -0
- data/lib/populate_me/s3_attachment.rb +120 -0
- data/lib/populate_me/variation.rb +38 -0
- data/lib/populate_me/version.rb +4 -0
- data/populate-me.gemspec +34 -0
- data/test/helper.rb +37 -0
- data/test/test_admin.rb +183 -0
- data/test/test_api.rb +246 -0
- data/test/test_attachment.rb +167 -0
- data/test/test_document.rb +128 -0
- data/test/test_document_admin_adapter.rb +221 -0
- data/test/test_document_callbacks.rb +151 -0
- data/test/test_document_outcasting.rb +247 -0
- data/test/test_document_persistence.rb +83 -0
- data/test/test_document_schema.rb +280 -0
- data/test/test_document_typecasting.rb +128 -0
- data/test/test_grid_fs_attachment.rb +239 -0
- data/test/test_mongo.rb +324 -0
- data/test/test_s3_attachment.rb +281 -0
- data/test/test_variation.rb +91 -0
- data/test/test_version.rb +11 -0
- metadata +294 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b2770ba89bcfa96ad56baafdf141c67891317044051f44b117a8e6b438f7c432
|
4
|
+
data.tar.gz: f076278f23d91e5c59c141b39698c34a9083c5861ba01f41a3692d587bc0b73b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f952a4160015147d54bab9a504f189613521e33342561e95ae33710cb2c3929a9a9512a96d6b0ed2812571a3faa7c972f3b550556884e44c9c0cd7df19922ed0
|
7
|
+
data.tar.gz: cd10b6ec3d88432f6255803150c6c1b129b0ffab6a6736d92243238976cfbf295413ea5a1cd9251029edfd798cdead3ac98fcbf51cd3f415a0011702ada41c5b
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2016 Mickael Riga
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
20
|
+
|
data/README.md
ADDED
@@ -0,0 +1,655 @@
|
|
1
|
+
Populate Me
|
2
|
+
===========
|
3
|
+
|
4
|
+
Overview
|
5
|
+
--------
|
6
|
+
|
7
|
+
`PopulateMe` is a modular system which provides an admin backend for any
|
8
|
+
Ruby/Rack web application. It is made with Sinatra but you can code your
|
9
|
+
frontend with any other Framework like Rails.
|
10
|
+
|
11
|
+
Table of contents
|
12
|
+
----------------
|
13
|
+
|
14
|
+
- [Overview](#overview)
|
15
|
+
- [Table of contents](#table-of-contents)
|
16
|
+
- [Documents](#documents)
|
17
|
+
- [Schema](#schema)
|
18
|
+
- [Relationships](#relationships)
|
19
|
+
- [Validations](#validations)
|
20
|
+
- [Callbacks](#callbacks)
|
21
|
+
- [Single Documents](#single-documents)
|
22
|
+
- [Mongo documents](#mongo-documents)
|
23
|
+
- [Admin](#admin)
|
24
|
+
- [Polymorphism](#polymorphism)
|
25
|
+
- [Customize Admin](#customize-admin)
|
26
|
+
- [API](#api)
|
27
|
+
|
28
|
+
Documents
|
29
|
+
---------
|
30
|
+
|
31
|
+
The `Document` class is a prototype. It contains all the code that is
|
32
|
+
not specific to a database system. When using this class, documents are
|
33
|
+
just kept in memory and therefore are lost when you restart the app.
|
34
|
+
|
35
|
+
Obviously, in a real application, you would not use this class, but
|
36
|
+
a persistent one instead. But since the purpose is to have a common
|
37
|
+
interface for any database system, then the following examples are
|
38
|
+
written using the basic `Document` class.
|
39
|
+
|
40
|
+
For the moment, `PopulateMe` only ships with a [MongoDB](#mongo-documents) document, but we hope there will be others in the future, including some for SQL databases.
|
41
|
+
|
42
|
+
### Schema
|
43
|
+
|
44
|
+
Here is an example of a document class:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
require 'populate_me/document'
|
48
|
+
|
49
|
+
class BlogArticle < PopulateMe::Document
|
50
|
+
|
51
|
+
field :title, default: 'New blog article', required: true
|
52
|
+
field :content, type: :text
|
53
|
+
field :created_on, type: :datetime, default: proc{Time.now}
|
54
|
+
field :published, type: :boolean
|
55
|
+
|
56
|
+
sort_by :created_on, :desc
|
57
|
+
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
Quite common so far.
|
62
|
+
The `field` method allows you to record anything about the field
|
63
|
+
itself, but here are the keys used by `PopulateMe`:
|
64
|
+
|
65
|
+
- `:type` Defines the type of field (please find the list of types below).
|
66
|
+
- `:form_field` Set to `false` if you do not want this field in the default form.
|
67
|
+
- `:label` What the label in the form says (defaults to a human-friendly version of the field name)
|
68
|
+
- `:wrap` Set it to false if you do not want the form field to be wrapped in a `div` with a label.
|
69
|
+
- `:default` Either a default value or a `proc` to run to get the default value.
|
70
|
+
- `:required` Set to true if you want the field to be marked as required in the form.
|
71
|
+
- `:only_for` List of polymorphic type values
|
72
|
+
|
73
|
+
As you can see, most of the options are made for you to tailor the form
|
74
|
+
which `PopulateMe` will generate for you in the admin.
|
75
|
+
|
76
|
+
Available types are:
|
77
|
+
|
78
|
+
- `:string` Short text.
|
79
|
+
- `:text` Multiline text.
|
80
|
+
- `:boolean` Which is `true` or `false`.
|
81
|
+
- `:select` Dropdown list of options (records a string).
|
82
|
+
|
83
|
+
A `:list` type exists as well for nested documents, but it is not
|
84
|
+
fully working yet.
|
85
|
+
|
86
|
+
The `field` method creates a getter and a setter for this particular field.
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
blog_article.published # Returns true or false
|
90
|
+
blog_article.published = true
|
91
|
+
```
|
92
|
+
|
93
|
+
### Relationships
|
94
|
+
|
95
|
+
In its simplest form, when using the modules convention, relationships
|
96
|
+
can be declared this way:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
class BlogArticle < PopulateMe::Document
|
100
|
+
|
101
|
+
field :title
|
102
|
+
|
103
|
+
relationship :comments
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
class BlogArticle::Comment < PopulateMe::Document
|
108
|
+
|
109
|
+
field :author
|
110
|
+
field :blog_article_id, type: :hidden
|
111
|
+
position_field scope: :blog_article_id
|
112
|
+
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
The `relationship` method creates 2 getters for this particular field,
|
117
|
+
one with the same name and one with `_first` at the end. Both are cached
|
118
|
+
so that the database is queried only once.
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
blog_article.comments # Returns all the comments for this article
|
122
|
+
blog_article.comments_first # Returns the first comment for this article
|
123
|
+
```
|
124
|
+
|
125
|
+
It uses the `PopulateMe::Document::admin_find` and
|
126
|
+
`PopulateMe::Document::admin_find_first` methods in the background,
|
127
|
+
so default sorting order is respected.
|
128
|
+
|
129
|
+
### Validations
|
130
|
+
|
131
|
+
In its simplest form, validations are done by overriding the `#validate` method and declaring errors with the `#error_on` method.
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
class Person < PopulateMe::Document
|
135
|
+
|
136
|
+
field :name
|
137
|
+
|
138
|
+
def validate
|
139
|
+
error_on(:name, 'Cannot be fake') if self.name=='John Doe'
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
If you don't use the `PopulateMe` interface and create a document
|
146
|
+
programmatically, here is what it could look like:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
person = Person.new(name: 'John Doe')
|
150
|
+
person.new? # returns true
|
151
|
+
person.save # fails
|
152
|
+
person.valid? # returns false
|
153
|
+
person.errors # returns { name: ['Cannot be fake'] }
|
154
|
+
```
|
155
|
+
|
156
|
+
### Callbacks
|
157
|
+
|
158
|
+
There are some classic hooks which trigger the callbacks you declare.
|
159
|
+
Here is a basic example:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
require 'populate_me/document'
|
163
|
+
|
164
|
+
class Person < PopulateMe::Document
|
165
|
+
|
166
|
+
field :firstname
|
167
|
+
field :lastname
|
168
|
+
field :fullname, form_field: false
|
169
|
+
|
170
|
+
before :save do
|
171
|
+
self.fullname = "#{self.firstname} #{self.lastname}"
|
172
|
+
end
|
173
|
+
|
174
|
+
after :delete, :goodbye
|
175
|
+
|
176
|
+
def goodbye
|
177
|
+
puts "So long and thanks for all the fish"
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
```
|
182
|
+
|
183
|
+
First you can note that the field option `form_field: false` makes it a field
|
184
|
+
that does not appear in the form. This is generally the case for fields that
|
185
|
+
are generated from other fields.
|
186
|
+
|
187
|
+
Anyway, here we define a callback which `PopulateMe` runs each time a document
|
188
|
+
is saved. And with the second one, you can see that we can pass the name of
|
189
|
+
a method instead of a block.
|
190
|
+
|
191
|
+
The list of hooks is quite common but here it is as a reminder:
|
192
|
+
|
193
|
+
- `before :validate`
|
194
|
+
- `after :validate`
|
195
|
+
- `before :create`
|
196
|
+
- `after :create`
|
197
|
+
- `before :update`
|
198
|
+
- `after :update`
|
199
|
+
- `before :save` (both create or update)
|
200
|
+
- `after :save` (both create or update)
|
201
|
+
- `before :delete`
|
202
|
+
- `after :delete`
|
203
|
+
|
204
|
+
Now you can register many callbacks for the same hook. They will be chained in
|
205
|
+
the order you register them. However, if for any reason you need to register a
|
206
|
+
callback and make sure it runs before the others, you can add `prepend: true`.
|
207
|
+
|
208
|
+
```ruby
|
209
|
+
before :save, prepend: true do
|
210
|
+
puts 'Shotgun !!!'
|
211
|
+
end
|
212
|
+
```
|
213
|
+
|
214
|
+
If you want to go even further and create your own hooks, this is very easy.
|
215
|
+
You can create a hook like this:
|
216
|
+
|
217
|
+
```ruby
|
218
|
+
document.exec_callback(:my_hook)
|
219
|
+
```
|
220
|
+
|
221
|
+
And you would then register a callback like this:
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
register_callback :my_hook do
|
225
|
+
# Do something...
|
226
|
+
end
|
227
|
+
```
|
228
|
+
|
229
|
+
You can use `before` and `after` as well. In fact this:
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
after :lunch do
|
233
|
+
# Do something...
|
234
|
+
end
|
235
|
+
```
|
236
|
+
|
237
|
+
Is equivalent to:
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
register_callback :after_lunch do
|
241
|
+
# Do something...
|
242
|
+
end
|
243
|
+
```
|
244
|
+
|
245
|
+
### Single Documents
|
246
|
+
|
247
|
+
Sometimes you want a collection with only one document, like for recording
|
248
|
+
settings for example. In this case you can use the `::is_unique` class method.
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
require 'populate_me/document'
|
252
|
+
|
253
|
+
class GeneralWebsiteSettings < PopulateMe::Document
|
254
|
+
field :main_meta_title
|
255
|
+
field :main_meta_description
|
256
|
+
field :google_analytics_ref
|
257
|
+
end
|
258
|
+
|
259
|
+
GeneralWebsiteSettings.is_unique
|
260
|
+
```
|
261
|
+
|
262
|
+
It just creates the document if it does not exist yet with the ID `unique`.
|
263
|
+
If you want a different ID, you can pass it as an argument.
|
264
|
+
|
265
|
+
Just make sure that if you have fields with `required: true`, they also have
|
266
|
+
a `:default` value. Otherwise the creation of the document will fail because it
|
267
|
+
is not `self.valid?`.
|
268
|
+
|
269
|
+
### Mongo Documents
|
270
|
+
|
271
|
+
Note: the current version works with the mongo driver version 2
|
272
|
+
|
273
|
+
Now let's declare a real document class which can persist on a database,
|
274
|
+
the `MongoDB` kind of document. The first thing we need to clarify is the
|
275
|
+
setup. Here is a classic setup:
|
276
|
+
|
277
|
+
```ruby
|
278
|
+
# lib/db.rb
|
279
|
+
require 'mongo'
|
280
|
+
require 'populate_me/mongo'
|
281
|
+
|
282
|
+
client = Mongo::Client.new([ '127.0.0.1:27017' ], :database => 'your-database-name')
|
283
|
+
|
284
|
+
PopulateMe::Mongo.set :db, client.database
|
285
|
+
|
286
|
+
require 'person'
|
287
|
+
```
|
288
|
+
|
289
|
+
Then the document is pretty much the same as the prototype except that it
|
290
|
+
subclasses `PopulateMe::Mongo` instead.
|
291
|
+
|
292
|
+
```ruby
|
293
|
+
# lib/person.rb
|
294
|
+
require 'populate_me/mongo'
|
295
|
+
|
296
|
+
class Person < PopulateMe::Mongo
|
297
|
+
field :firstname
|
298
|
+
field :lastname
|
299
|
+
end
|
300
|
+
```
|
301
|
+
|
302
|
+
As you can see in setup, you can define inheritable settings on
|
303
|
+
`PopulateMe::Mongo`, meaning that any subclass after this will have the `:db`
|
304
|
+
and you can set it only once.
|
305
|
+
|
306
|
+
Nevertheless it is obviously possible to set a different `:db` for each class.
|
307
|
+
|
308
|
+
```ruby
|
309
|
+
# lib/person.rb
|
310
|
+
require 'populate_me/mongo'
|
311
|
+
|
312
|
+
class Person < PopulateMe::Mongo
|
313
|
+
|
314
|
+
set :db, $my_db
|
315
|
+
|
316
|
+
field :firstname
|
317
|
+
field :lastname
|
318
|
+
|
319
|
+
end
|
320
|
+
```
|
321
|
+
|
322
|
+
This is particularly useful if you keep a type of documents in a different
|
323
|
+
location for example. Otherwise it is more convenient to set it once
|
324
|
+
and for all.
|
325
|
+
|
326
|
+
You can also set `:collection_name`, but in most cases you would let `PopulateMe`
|
327
|
+
defaults it to the dasherized class name. So `BlogArticle::Comment` would be
|
328
|
+
in the collection called `blog-article--comment`.
|
329
|
+
|
330
|
+
Whatever you choose, you will have access to the collection object with the
|
331
|
+
`::collection` class method. Which allows you to do anything the driver does.
|
332
|
+
|
333
|
+
```ruby
|
334
|
+
first_pedro = Person.collection.find({ 'firstname' => 'Pedro' }).first
|
335
|
+
mcs = Person.collection.find({ 'lastname' => /^Mc/i })
|
336
|
+
```
|
337
|
+
|
338
|
+
Although since these are methods from the driver, `first_pedro` returns a hash,
|
339
|
+
and `mcs` returns a `Mongo::Collection::View`. If you want document object, you can use
|
340
|
+
the `::cast` class method which takes a block in the class context/scope and
|
341
|
+
casts either a single hash into a full featured document, or casts the items of
|
342
|
+
an array (or anything which responds to `:map`).
|
343
|
+
|
344
|
+
```ruby
|
345
|
+
first_pedro = Person.cast{ collection.find_one({ 'firstname' => 'Pedro' }) }
|
346
|
+
mcs = Person.cast{ collection.find({ 'lastname' => /^Mc/i }) }
|
347
|
+
first_pedro.class # returns Person
|
348
|
+
mcs[0].class # returns Person
|
349
|
+
```
|
350
|
+
|
351
|
+
Admin
|
352
|
+
-----
|
353
|
+
|
354
|
+
A basic admin would look like this:
|
355
|
+
|
356
|
+
```ruby
|
357
|
+
# lib/admin.rb
|
358
|
+
require "populate_me/admin"
|
359
|
+
|
360
|
+
class Admin < PopulateMe::Admin
|
361
|
+
# Since we are in lib we use this to move
|
362
|
+
# the root one level up.
|
363
|
+
# Not mandatory but useful if you plan to have
|
364
|
+
# custom views in the main views folder
|
365
|
+
set :root, ::File.expand_path('../..', __FILE__)
|
366
|
+
# Only if you use Rack::Cerberus for authentication
|
367
|
+
# you can pass the settings
|
368
|
+
set :cerberus, {company_name: 'Nintendo'}
|
369
|
+
# Build menu and sub-menus
|
370
|
+
set :menu, [
|
371
|
+
['Settings', '/admin/form/settings/unique'],
|
372
|
+
['Articles', '/admin/list/article'],
|
373
|
+
['Staff', [
|
374
|
+
['Designers', '/admin/list/staff-member?filter[job]=Designer'],
|
375
|
+
['Developers', '/admin/list/staff-member?filter[job]=Developer'],
|
376
|
+
]]
|
377
|
+
]
|
378
|
+
end
|
379
|
+
```
|
380
|
+
|
381
|
+
So the main thing you need is to define your menu. Then mount it in
|
382
|
+
your `config.ru` whereever you want.
|
383
|
+
|
384
|
+
```ruby
|
385
|
+
# config.ru
|
386
|
+
require 'admin'
|
387
|
+
|
388
|
+
map '/admin' do
|
389
|
+
run Admin
|
390
|
+
end
|
391
|
+
```
|
392
|
+
|
393
|
+
Most of the URLs in your menu will probably be for the admin itself and use
|
394
|
+
the admin URL patterns, but this is not mandatory. A link to an external page
|
395
|
+
would load in a new tab. Whereas admin URLs create columns in the `PopulateMe`
|
396
|
+
user interface. Many things are possible with these patterns but here are
|
397
|
+
the main ones:
|
398
|
+
|
399
|
+
- `/:path_to_admin/list/:dasherized_document_class` This gives you the list of
|
400
|
+
documents from the desired class. They are ordered as specified by `sort_by`.
|
401
|
+
You can also filter like in the example to get only specific documents.
|
402
|
+
- `:path_to_admin/form/:dasherized_document_class/:id` You would rarely use this
|
403
|
+
one which directly opens the form of a specific document, since all this is
|
404
|
+
generally accessed from the list page. It doesn't need to be coded. The only is
|
405
|
+
probably for [single documents](#single-documents) because they are not part of
|
406
|
+
a list. The ID would then be litterally `unique`, or whatever ID you declared
|
407
|
+
instead.
|
408
|
+
|
409
|
+
### Polymorphism
|
410
|
+
|
411
|
+
You can use the schema to set a Document class as polymorphic. The consequence
|
412
|
+
is that the admin will make you choose a type before creating a new document.
|
413
|
+
And then the form will only display the fields applicable to this polymorphic
|
414
|
+
type. And once created, it will only show relationships applicable to its
|
415
|
+
polymorphic type. You can do this with the `:only_for` option.
|
416
|
+
|
417
|
+
Here is an example of a document that can either be a title with a paragraph, or
|
418
|
+
a title with a set of images:
|
419
|
+
|
420
|
+
```ruby
|
421
|
+
# lib/models/box.rb
|
422
|
+
|
423
|
+
require 'populate_me/document'
|
424
|
+
|
425
|
+
class Box < PopulateMe::Document
|
426
|
+
|
427
|
+
field :title
|
428
|
+
field :paragraph, type: :text, only_for: 'Paragraph'
|
429
|
+
relationship :images, only_for: 'Image slider'
|
430
|
+
position_field
|
431
|
+
|
432
|
+
end
|
433
|
+
```
|
434
|
+
|
435
|
+
In this case, when you create a `Box` with the polymorphic type `Paragraph`, the
|
436
|
+
form will have a field for `:paragraph` but no relationship for images. And if
|
437
|
+
you create a `Box` with the polymorphic type `Image slider`, it will be the
|
438
|
+
opposite.
|
439
|
+
|
440
|
+
The option `:only_for` can also be an `Array`. Actually, when inspecting the
|
441
|
+
`fields`, you'll see that even when you pass a `String`, it will be put inside
|
442
|
+
an `Array`.
|
443
|
+
|
444
|
+
```ruby
|
445
|
+
Box.fields[:paragraph][:only_for] # => ['Paragraph']
|
446
|
+
```
|
447
|
+
|
448
|
+
A hidden field is automatically created called `:polymorphic_type`, therefore
|
449
|
+
it is a method you can call to get or set the `:polymorphic_type`.
|
450
|
+
|
451
|
+
```ruby
|
452
|
+
box = Box.new polymorphic_type: 'Paragraph'
|
453
|
+
box.polymorphic_type # => 'Paragraph'
|
454
|
+
```
|
455
|
+
|
456
|
+
One of the information that the field contains is all the `:values` the field
|
457
|
+
can have.
|
458
|
+
|
459
|
+
```ruby
|
460
|
+
Box.fields[:polymorphic_type][:values] # => ['Paragraph', 'Image slider']
|
461
|
+
```
|
462
|
+
|
463
|
+
They are in the order they are declared in the fields. If you want to just set
|
464
|
+
this list yourself or any other option attached to the `:polimorphic_type` field
|
465
|
+
you can do so with the `Document::polymorphic` class method.
|
466
|
+
|
467
|
+
```ruby
|
468
|
+
# lib/models/box.rb
|
469
|
+
|
470
|
+
require 'populate_me/document'
|
471
|
+
|
472
|
+
class Box < PopulateMe::Document
|
473
|
+
|
474
|
+
polymorphic values: ['Image slider', 'Paragraph']
|
475
|
+
field :title
|
476
|
+
field :paragraph, type: :text, only_for: 'Paragraph'
|
477
|
+
relationship :images, only_for: 'Image slider'
|
478
|
+
position_field
|
479
|
+
|
480
|
+
end
|
481
|
+
```
|
482
|
+
|
483
|
+
If each polymorphic type has a lot of fields and/or relationships, you can use the
|
484
|
+
`Document::only_for` class method which sets the `:only_for` option for
|
485
|
+
everything inside the block.
|
486
|
+
|
487
|
+
```ruby
|
488
|
+
# lib/models/media.rb
|
489
|
+
|
490
|
+
require 'populate_me/document'
|
491
|
+
|
492
|
+
class Media < PopulateMe::Document
|
493
|
+
|
494
|
+
field :title
|
495
|
+
only_for 'Book' do
|
496
|
+
field :author
|
497
|
+
field :publisher
|
498
|
+
relationship :chapters
|
499
|
+
end
|
500
|
+
only_for 'Movie' do
|
501
|
+
field :script_writer
|
502
|
+
field :director
|
503
|
+
relationship :scenes
|
504
|
+
relationship :actors
|
505
|
+
end
|
506
|
+
position_field
|
507
|
+
|
508
|
+
end
|
509
|
+
```
|
510
|
+
|
511
|
+
It is worth noting that this implementation of polymorphism is supposed to work
|
512
|
+
with fixed schema databases, and therefore all fields and relationship exist for
|
513
|
+
each document. In our case, books would still have a `#director` method. The
|
514
|
+
difference is only cosmetic and mainly allows you to have forms that are less
|
515
|
+
crowded in the admin.
|
516
|
+
|
517
|
+
To mitigate this, a few methods are there to help you. There is a predicate for
|
518
|
+
knowing if a class is polymorphic.
|
519
|
+
|
520
|
+
```ruby
|
521
|
+
Media.polymorphic? # => true
|
522
|
+
```
|
523
|
+
|
524
|
+
For each document, you can inspect its polymorphic type or check if a field or
|
525
|
+
relationship is applicable.
|
526
|
+
|
527
|
+
```ruby
|
528
|
+
book = Media.new polymorphic_type: 'Book', title: 'Hot Water Music', author: 'Charles Bukowski'
|
529
|
+
book.polymorphic_type # => 'Book'
|
530
|
+
book.field_applicable? :author # => true
|
531
|
+
book.relationship_applicable? :actors # => false
|
532
|
+
```
|
533
|
+
|
534
|
+
### Customize Admin
|
535
|
+
|
536
|
+
You can customize the admin with a few settings.
|
537
|
+
The main ones are for adding CSS and javascript.
|
538
|
+
There are 2 settings for this: `:custom_css_url` and `:custom_js_url`.
|
539
|
+
|
540
|
+
```ruby
|
541
|
+
# lib/admin.rb
|
542
|
+
require "populate_me/admin"
|
543
|
+
|
544
|
+
class Admin < PopulateMe::Admin
|
545
|
+
|
546
|
+
set :custom_css_url, '/css/admin.css'
|
547
|
+
set :custom_js_url, '/js/admin.js'
|
548
|
+
|
549
|
+
set :root, ::File.expand_path('../..', __FILE__)
|
550
|
+
|
551
|
+
set :menu, [
|
552
|
+
['Settings', '/admin/form/settings/unique'],
|
553
|
+
['Articles', '/admin/list/article'],
|
554
|
+
['Staff', [
|
555
|
+
['Designers', '/admin/list/staff-member?filter[job]=Designer'],
|
556
|
+
['Developers', '/admin/list/staff-member?filter[job]=Developer'],
|
557
|
+
]]
|
558
|
+
]
|
559
|
+
|
560
|
+
end
|
561
|
+
```
|
562
|
+
|
563
|
+
Inside the javascript file, you can use many functions and variables that
|
564
|
+
are under the `PopulateMe` namespace. See source code to know more about it.
|
565
|
+
Some are callbacks like `PopulateMe.custom_init_column` which allows you to
|
566
|
+
bind events when a column was created.
|
567
|
+
|
568
|
+
```javascript
|
569
|
+
# /js/admin.js
|
570
|
+
|
571
|
+
$(function() {
|
572
|
+
|
573
|
+
$('body').bind('change', 'select.special', function(event) {
|
574
|
+
alert('Changed!');
|
575
|
+
});
|
576
|
+
|
577
|
+
PopulateMe.custom_init_column = function(column) {
|
578
|
+
$('select.special', column).css({color: 'orange'});
|
579
|
+
}
|
580
|
+
|
581
|
+
});
|
582
|
+
```
|
583
|
+
|
584
|
+
The other thing you might want to do is adding mustache templates.
|
585
|
+
You can do this with the setting `:custom_templates_view`.
|
586
|
+
|
587
|
+
```ruby
|
588
|
+
# lib/admin.rb
|
589
|
+
require "populate_me/admin"
|
590
|
+
|
591
|
+
class Admin < PopulateMe::Admin
|
592
|
+
|
593
|
+
set :custom_templates_view, :custom_templates
|
594
|
+
|
595
|
+
# ...
|
596
|
+
|
597
|
+
end
|
598
|
+
```
|
599
|
+
|
600
|
+
Let's say we want to be able to set the size of the preview for attachments,
|
601
|
+
as opposed to the default value of 150. We would put this in the view:
|
602
|
+
|
603
|
+
```eruby
|
604
|
+
<script id="template-attachment-field-custom" type="x-tmpl-mustache">
|
605
|
+
{{#url}}
|
606
|
+
<img src='{{url}}{{cache_buster}}' alt='Preview' width='{{attachment_preview_width}}' />
|
607
|
+
<button class='attachment-deleter'>x</button>
|
608
|
+
<br />
|
609
|
+
{{/url}}
|
610
|
+
<input type='file' name='{{input_name}}' {{#max_size}}data-max-size='{{max_size}}'{{/max_size}} {{{build_input_atrributes}}} />
|
611
|
+
</script>
|
612
|
+
```
|
613
|
+
|
614
|
+
This is the default template except we've replace `150` with the mustache
|
615
|
+
variable `attachment_preview_width`. Everything that you set on the schema
|
616
|
+
is available in the template, so you can set both the custom template name
|
617
|
+
and the width variable in the hash passed to `field` when doing your schema.
|
618
|
+
The template name is the ID of the script tag.
|
619
|
+
|
620
|
+
```ruby
|
621
|
+
# /lib/blog_post.rb
|
622
|
+
require 'populate_me/document'
|
623
|
+
|
624
|
+
class BlogPost < PopulateMe::Document
|
625
|
+
|
626
|
+
field :title
|
627
|
+
field :image, type: :attachment, custom_template: 'template-attachment-field-custom', attachment_preview_width: 200, variations: [
|
628
|
+
PopulateMe::Variation.new_image_magick_job(:thumb, :gif, "-resize '300x'")
|
629
|
+
]
|
630
|
+
|
631
|
+
# ...
|
632
|
+
|
633
|
+
end
|
634
|
+
```
|
635
|
+
|
636
|
+
|
637
|
+
API
|
638
|
+
---
|
639
|
+
|
640
|
+
In a normal use, you most likely don't have anything to do with the `API` module.
|
641
|
+
It is just another middleware automatically mounted under `/api` on your `Admin`.
|
642
|
+
So if your `Admin` path is `/admin`, then your `API` path is `/admin/api`.
|
643
|
+
|
644
|
+
The purpose of the `API` module is to provide all the path patterns for creating,
|
645
|
+
deleting and updating documents. The interface does all the job for you. But if
|
646
|
+
you end up building your all custom interface, you probably want to [have a look
|
647
|
+
at the implementation](lib/populate_me/api.rb).
|
648
|
+
|
649
|
+
Another aspect of the `API` is that it relies on document methods. So if you
|
650
|
+
want to create a subclass of `Document`, make sure that you override everything
|
651
|
+
that the `API` or the `Admin` may need.
|
652
|
+
|
653
|
+
This module is derived from a Gem I did called [rack-backend-api](https://github.com/mig-hub/backend-api). It is not maintained any more since `PopulateMe` is the evolution
|
654
|
+
of this Gem.
|
655
|
+
|