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.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +20 -0
  5. data/README.md +655 -0
  6. data/Rakefile +14 -0
  7. data/example/config.ru +100 -0
  8. data/lib/populate_me.rb +2 -0
  9. data/lib/populate_me/admin.rb +157 -0
  10. data/lib/populate_me/admin/__assets__/css/asmselect.css +63 -0
  11. data/lib/populate_me/admin/__assets__/css/jquery-ui.min.css +6 -0
  12. data/lib/populate_me/admin/__assets__/css/main.css +244 -0
  13. data/lib/populate_me/admin/__assets__/img/help/children.png +0 -0
  14. data/lib/populate_me/admin/__assets__/img/help/create.png +0 -0
  15. data/lib/populate_me/admin/__assets__/img/help/delete.png +0 -0
  16. data/lib/populate_me/admin/__assets__/img/help/edit.png +0 -0
  17. data/lib/populate_me/admin/__assets__/img/help/form.png +0 -0
  18. data/lib/populate_me/admin/__assets__/img/help/list.png +0 -0
  19. data/lib/populate_me/admin/__assets__/img/help/login.png +0 -0
  20. data/lib/populate_me/admin/__assets__/img/help/logout.png +0 -0
  21. data/lib/populate_me/admin/__assets__/img/help/menu.png +0 -0
  22. data/lib/populate_me/admin/__assets__/img/help/overview.png +0 -0
  23. data/lib/populate_me/admin/__assets__/img/help/save.png +0 -0
  24. data/lib/populate_me/admin/__assets__/img/help/sort.png +0 -0
  25. data/lib/populate_me/admin/__assets__/img/help/sublist.png +0 -0
  26. data/lib/populate_me/admin/__assets__/js/asmselect.js +412 -0
  27. data/lib/populate_me/admin/__assets__/js/columnav.js +87 -0
  28. data/lib/populate_me/admin/__assets__/js/jquery-ui.min.js +7 -0
  29. data/lib/populate_me/admin/__assets__/js/main.js +388 -0
  30. data/lib/populate_me/admin/__assets__/js/mustache.js +578 -0
  31. data/lib/populate_me/admin/__assets__/js/sortable.js +2 -0
  32. data/lib/populate_me/admin/views/help.erb +94 -0
  33. data/lib/populate_me/admin/views/page.erb +189 -0
  34. data/lib/populate_me/api.rb +124 -0
  35. data/lib/populate_me/attachment.rb +186 -0
  36. data/lib/populate_me/document.rb +192 -0
  37. data/lib/populate_me/document_mixins/admin_adapter.rb +149 -0
  38. data/lib/populate_me/document_mixins/callbacks.rb +125 -0
  39. data/lib/populate_me/document_mixins/outcasting.rb +83 -0
  40. data/lib/populate_me/document_mixins/persistence.rb +95 -0
  41. data/lib/populate_me/document_mixins/schema.rb +198 -0
  42. data/lib/populate_me/document_mixins/typecasting.rb +70 -0
  43. data/lib/populate_me/document_mixins/validation.rb +44 -0
  44. data/lib/populate_me/file_system_attachment.rb +40 -0
  45. data/lib/populate_me/grid_fs_attachment.rb +103 -0
  46. data/lib/populate_me/mongo.rb +160 -0
  47. data/lib/populate_me/s3_attachment.rb +120 -0
  48. data/lib/populate_me/variation.rb +38 -0
  49. data/lib/populate_me/version.rb +4 -0
  50. data/populate-me.gemspec +34 -0
  51. data/test/helper.rb +37 -0
  52. data/test/test_admin.rb +183 -0
  53. data/test/test_api.rb +246 -0
  54. data/test/test_attachment.rb +167 -0
  55. data/test/test_document.rb +128 -0
  56. data/test/test_document_admin_adapter.rb +221 -0
  57. data/test/test_document_callbacks.rb +151 -0
  58. data/test/test_document_outcasting.rb +247 -0
  59. data/test/test_document_persistence.rb +83 -0
  60. data/test/test_document_schema.rb +280 -0
  61. data/test/test_document_typecasting.rb +128 -0
  62. data/test/test_grid_fs_attachment.rb +239 -0
  63. data/test/test_mongo.rb +324 -0
  64. data/test/test_s3_attachment.rb +281 -0
  65. data/test/test_variation.rb +91 -0
  66. data/test/test_version.rb +11 -0
  67. metadata +294 -0
@@ -0,0 +1,14 @@
1
+ require 'rake/testtask'
2
+
3
+ task :default => :test
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << "test"
7
+ t.pattern = 'test/test_*.rb'
8
+ unless ENV['TESTONLY'].nil?
9
+ t.pattern = t.pattern.sub(/\*/, ENV['TESTONLY'])
10
+ end
11
+ t.options = '--pride'
12
+ t.warning = false
13
+ end
14
+
@@ -0,0 +1,100 @@
1
+ # We use this example for running real tests on top of unit tests.
2
+ # This is not meant to be an example to learn how PopulateMe would work.
3
+ # Real examples will come later and this file will be removed.
4
+
5
+ $:.unshift File.expand_path('../../lib', __FILE__)
6
+
7
+ # Models ##########
8
+
9
+ require 'populate_me/document'
10
+
11
+ # MONGO = Mongo::Client.new([ '127.0.0.1:27017' ], :database => 'blog-populate-me-test')
12
+ # DB = MONGO.database
13
+ # PopulateMe::Mongo.set :db, DB
14
+
15
+ require 'populate_me/attachment'
16
+ PopulateMe::Document.set :default_attachment_class, PopulateMe::Attachment
17
+ # require 'populate_me/file_system_attachment'
18
+ # PopulateMe::Document.set :default_attachment_class, PopulateMe::FileSystemAttachment
19
+ #
20
+ # require 'populate_me/grid_fs_attachment'
21
+ # PopulateMe::Mongo.set :default_attachment_class, PopulateMe::GridFS
22
+ # PopulateMe::GridFS.set :db, DB
23
+
24
+ # require 'populate_me/s3_attachment'
25
+ # s3_resource = Aws::S3::Resource.new
26
+ # s3_bucket = s3_resource.bucket(ENV['BUCKET'])
27
+ # PopulateMe::Document.set :default_attachment_class, PopulateMe::S3Attachment
28
+ # PopulateMe::S3Attachment.set :bucket, s3_bucket
29
+
30
+
31
+ class BlogPost < PopulateMe::Document
32
+ field :title, required: true
33
+ field :thumbnail, type: :attachment, max_size: 600*1024, variations: [
34
+ PopulateMe::Variation.new_image_magick_job(:populate_me_thumb, :jpg, "-resize '400x225^' -gravity center -extent 400x225")
35
+ ]
36
+ field :content, type: :text
37
+ field :authors, type: :list
38
+ field :published, type: :boolean
39
+ field :date_now, type: :date, input_attributes: {type: :date}
40
+ field :datetime_now, type: :datetime, input_attributes: {type: :datetime}
41
+ relationship :comments
42
+ def validate
43
+ error_on(:content,'Cannot be blank') if WebUtils.blank?(self.content)
44
+ end
45
+
46
+ end
47
+ class BlogPost::Author < PopulateMe::Document
48
+ # nested
49
+ field :name
50
+ def validate
51
+ error_on(:name, 'Cannot be shit') if self.name=='shit'
52
+ end
53
+ end
54
+ class BlogPost::Comment < PopulateMe::Document
55
+ # not nested
56
+ field :author, default: 'Anonymous'
57
+ field :content, type: :text
58
+ field :blog_post_id, type: :hidden
59
+ position_field scope: :blog_post_id
60
+ end
61
+
62
+ class Article < PopulateMe::Document
63
+ field :title
64
+ field :content, type: :text
65
+ field :yes_or_no, type: :select, select_options: [:yes,:no]
66
+ field :tags, type: :select, multiple: true, select_options: ['art','sport','science']
67
+ field :price, type: :price
68
+ position_field
69
+
70
+ relationship :sections
71
+
72
+ after :save do
73
+ puts self.inspect
74
+ end
75
+ end
76
+
77
+ class Article::Section < PopulateMe::Document
78
+
79
+ field :short, only_for: 'Nice Short'
80
+ field :long, only_for: 'Nice Long'
81
+ field :article_id, type: :hidden
82
+ position_field scope: :article_id
83
+
84
+ end
85
+
86
+ # Admin ##########
87
+
88
+ require "populate_me/admin"
89
+ class Admin < PopulateMe::Admin
90
+ set :menu, [
91
+ ['Blog Posts', '/list/blog-post'],
92
+ ['Articles', '/list/article'],
93
+ ]
94
+ end
95
+
96
+ use PopulateMe::Attachment::Middleware
97
+ # use PopulateMe::FileSystemAttachment::Middleware
98
+ # use PopulateMe::GridFS::Middleware
99
+ run Admin
100
+
@@ -0,0 +1,2 @@
1
+ require 'populate_me/api'
2
+
@@ -0,0 +1,157 @@
1
+ require 'populate_me/api'
2
+ require 'json'
3
+ require 'tilt/erb'
4
+ begin
5
+ require "rack/cerberus"
6
+ rescue LoadError
7
+ puts "Cerberus not loaded."
8
+ end
9
+
10
+ class PopulateMe::Admin < Sinatra::Base
11
+
12
+ # Settings
13
+ set :app_file, nil # Need to be set when subclassed
14
+ set :show_exceptions, false
15
+ set :meta_title, 'Populate Me'
16
+ set :index_path, '/menu'
17
+ enable :cerberus
18
+ set :cerberus_active, Proc.new{
19
+ cerberus_available? &&
20
+ !!cerberus_pass &&
21
+ settings.cerberus?
22
+ }
23
+ set :logout_path, Proc.new{ settings.cerberus_active ? '/logout' : false }
24
+
25
+ # Load API helpers
26
+ helpers PopulateMe::API::Helpers
27
+ helpers do
28
+
29
+ def user_name
30
+ return 'Anonymous' if session.nil?||session[:populate_me_user].nil?
31
+ session[:populate_me_user]
32
+ end
33
+
34
+ def help_img desc, filename
35
+ "<img src='#{request.script_name}/__assets__/img/help/#{filename}' alt='#{desc}' />"
36
+ end
37
+
38
+ end
39
+
40
+ # Make all templates in admin/views accessible with their basename
41
+ Dir["#{File.expand_path('../admin/views',__FILE__)}/*.erb"].each do |f|
42
+ template File.basename(f,'.erb').to_sym do
43
+ File.read(f)
44
+ end
45
+ end
46
+
47
+ before do
48
+ content_type :json
49
+ end
50
+
51
+ get '/?' do
52
+ content_type :html
53
+ erb :page, layout: false
54
+ end
55
+
56
+ get '/menu/?*?' do
57
+ page_title = 'Menu'
58
+ current_level = settings.menu.dup
59
+ levels = params[:splat][0].split('/')
60
+ levels.each do |l|
61
+ page_title, current_level = current_level.find{|item| slugify(item[0])==l}
62
+ end
63
+ items = current_level.map do |l|
64
+ href = l[1].is_a?(String) ? l[1] : "#{request.script_name}/menu#{levels.map{|level|'/'+level}.join}/#{slugify(l[0])}"
65
+ { title: l[0], href: href, new_page: (not href.start_with?('/')) }
66
+ end
67
+ if request.path_info=='/menu'
68
+ items.push({title: '?', href: "#{request.script_name}/help", new_page: false})
69
+ end
70
+ {
71
+ template: 'template_menu',
72
+ page_title: page_title,
73
+ items: items
74
+ }.to_json
75
+ end
76
+
77
+ get '/list/:class_name' do
78
+ @model_class = resolve_model_class params[:class_name]
79
+ @model_class.to_admin_list(request: request, params: params).to_json
80
+ end
81
+
82
+ get '/form/?:class_name?/?:id?' do
83
+ @model_class = resolve_model_class params[:class_name]
84
+ if params[:id].nil?
85
+ @model_instance = @model_class.new.set_defaults
86
+ @model_instance.set_from_hash(params[:data], typecast: true) unless params[:data].nil?
87
+ else
88
+ @model_instance = resolve_model_instance @model_class, params[:id]
89
+ end
90
+ @model_instance.to_admin_form(
91
+ request: request,
92
+ params: params,
93
+ nested: params[:nested]=='true',
94
+ input_name_prefix: params[:input_name_prefix]
95
+ ).to_json
96
+ end
97
+
98
+ get '/help' do
99
+ content_type :html
100
+ erb :help, layout: false
101
+ end
102
+
103
+ not_found do
104
+ response.headers['X-Cascade'] = 'pass'
105
+ {'success'=>false,'message'=>'Not Found'}.to_json
106
+ end
107
+
108
+ error do
109
+ puts
110
+ puts env['sinatra.error'].inspect
111
+ puts
112
+ {'success'=>false,'message'=>env['sinatra.error'].message}.to_json
113
+ end
114
+
115
+ class << self
116
+
117
+ def cerberus_pass
118
+ # Method = overridable = testable
119
+ ENV['CERBERUS_PASS']
120
+ end
121
+ def cerberus_available?
122
+ # Method = overridable = testable
123
+ Rack.const_defined?(:Cerberus)
124
+ end
125
+
126
+ private
127
+
128
+ def setup_default_middleware builder
129
+ # Override the Sinatra method
130
+ super builder
131
+ setup_populate_me_middleware builder
132
+ end
133
+
134
+ def setup_populate_me_middleware builder
135
+ # Authentication
136
+ setup_cerberus builder
137
+ # Mount assets on /__assets__
138
+ builder.use Rack::Static, :urls=>['/__assets__'], :root=>File.expand_path('../admin',__FILE__)
139
+ # Mount the API on /api
140
+ builder.use Rack::Builder do
141
+ map('/api'){ run PopulateMe::API }
142
+ end
143
+ end
144
+
145
+ def setup_cerberus builder
146
+ return unless settings.cerberus_active
147
+ cerberus_settings = settings.cerberus==true ? {} : settings.cerberus
148
+ cerberus_settings[:session_key] = 'populate_me_user'
149
+ builder.use Rack::Cerberus, cerberus_settings do |user,pass,req|
150
+ pass==cerberus_pass
151
+ end
152
+ end
153
+
154
+ end
155
+
156
+ end
157
+
@@ -0,0 +1,63 @@
1
+ .asmContainer {
2
+ /* container that surrounds entire asmSelect widget */
3
+ }
4
+
5
+ .asmSelect {
6
+ /* the newly created regular 'select' */
7
+ display: inline;
8
+ }
9
+
10
+ .asmOptionDisabled {
11
+ /* disabled options in new select */
12
+ color: #999;
13
+ }
14
+
15
+ .asmHighlight {
16
+ /* the highlight span */
17
+ padding: 0;
18
+ margin: 0 0 0 1em;
19
+ }
20
+
21
+ .asmList {
22
+ /* html list that contains selected items */
23
+ margin: 0.25em 0 1em 0;
24
+ position: relative;
25
+ display: block;
26
+ padding-left: 0;
27
+ list-style: none;
28
+ }
29
+
30
+ .asmListItem {
31
+ /* li item from the html list above */
32
+ position: relative;
33
+ margin-left: 0;
34
+ padding-left: 0;
35
+ list-style: none;
36
+ background: #ddd;
37
+ border: 1px solid #bbb;
38
+ width: 100%;
39
+ margin: 0 0 -1px 0;
40
+ line-height: 1em;
41
+ }
42
+
43
+ .asmListItem:hover {
44
+ background-color: #e5e5e5;
45
+ }
46
+
47
+ .asmListItemLabel {
48
+ /* this is a span that surrounds the text in the item, except for the remove link */
49
+ padding: 5px;
50
+ display: block;
51
+ }
52
+
53
+ .asmListSortable .asmListItemLabel {
54
+ cursor: move;
55
+ }
56
+
57
+ .asmListItemRemove {
58
+ /* the remove link in each list item */
59
+ position: absolute;
60
+ right: 0;
61
+ top: 0;
62
+ padding: 5px;
63
+ }
@@ -0,0 +1,6 @@
1
+ /*! jQuery UI - v1.12.1 - 2018-02-23
2
+ * http://jqueryui.com
3
+ * Includes: sortable.css
4
+ * Copyright jQuery Foundation and other contributors; Licensed MIT */
5
+
6
+ .ui-sortable-handle{-ms-touch-action:none;touch-action:none}
@@ -0,0 +1,244 @@
1
+ /* General */
2
+
3
+ html, body {
4
+ margin: 0;
5
+ padding: 0;
6
+ color: #002b36;
7
+ background-color: #eee8d5;
8
+ height: 100%;
9
+ font-family: 'Avenir', sans-serif;
10
+ }
11
+
12
+ body {
13
+ line-height: 1.5em;
14
+ }
15
+
16
+ #finder {
17
+ height: 100%;
18
+ }
19
+
20
+ .column {
21
+ background: #fdf6e3;
22
+ height: 100%;
23
+ min-width: 250px;
24
+ overflow: auto;
25
+ box-shadow: 0px 0px 10px #586e75;
26
+ }
27
+
28
+ .column > h1, .column > ol,
29
+ .column > form, .column > p { /* Column padding */
30
+ margin-left: 20px;
31
+ margin-right: 20px;
32
+ }
33
+
34
+ .column > h1,
35
+ .column > .text-page h1 {
36
+ font-size: 1.5em;
37
+ font-weight: normal;
38
+ margin-top: 1em;
39
+ margin-bottom: 1.5em;
40
+ }
41
+
42
+ .column > .text-page {
43
+ padding: 0 20px 20px 20px;
44
+ overflow: hidden;
45
+ width: 50%; width: 50vw;
46
+ max-width: 600px;
47
+ background-color: #ffffff;
48
+ }
49
+ .column > .text-page h2 {
50
+ font-size: 1.5em;
51
+ }
52
+ .column > .text-page img {
53
+ width: 100%;
54
+ }
55
+ .column > .text-page a {
56
+ color: #268bd2;
57
+ }
58
+ .column > .text-page a:hover {
59
+ text-decoration: underline;
60
+ }
61
+ .column > .text-page .frame {
62
+ padding: 1em;
63
+ border: 1px solid #002b36;
64
+ }
65
+
66
+ ol {
67
+ list-style: none;
68
+ margin: 0;
69
+ padding: 0;
70
+ }
71
+
72
+ a {
73
+ color: #002b36;
74
+ text-decoration: none;
75
+ /* border-bottom: 1px solid #002b36; */
76
+ }
77
+ a:hover, a.selected {
78
+ /* Blue */
79
+ color: #268bd2;
80
+ /* Green/Blue */
81
+ /* color: #2aa198; */
82
+ }
83
+
84
+ button { cursor: pointer; }
85
+
86
+ /* Lists */
87
+
88
+ .new-document-btn-wrap > * {
89
+ vertical-align: middle;
90
+ }
91
+
92
+ .new-document-btn, .new-nested-document-btn {
93
+ border: 0;
94
+ font-size: 1.615em;
95
+ margin-bottom: 1em;
96
+ }
97
+
98
+ button.admin-delete, button.admin-delete-nested, .handle-button {
99
+ display: inline-block; vertical-align: middle;
100
+ font-size: 1em; line-height: 1em;
101
+ width: auto; height: auto;
102
+ margin: 0;
103
+ padding: 0.5em 0.5em 0.5em 0;
104
+ border: 0px;
105
+ }
106
+ button.admin-delete, button.admin-delete-nested {
107
+ color: #dc322f;
108
+ background-color: transparent;
109
+ }
110
+ button.admin-delete:focus, button.admin-delete-nested:focus, .handle-button:focus {
111
+ outline: 0;
112
+ }
113
+ .handle {
114
+ cursor: move;
115
+ cursor: -moz-grab;
116
+ cursor: -webkit-grab;
117
+ cursor: grab;
118
+ }
119
+ .handle:active {
120
+ cursor: -moz-grabbing;
121
+ cursor: -webkit-grabbing;
122
+ cursor: grabbing;
123
+ }
124
+
125
+ [type=submit] {
126
+ border: 0px;
127
+ cursor: pointer;
128
+ color: #fdf6e3;
129
+ background-color: #859900;
130
+ }
131
+ [type=submit]:hover {
132
+ background-color: #268bd2;
133
+ }
134
+
135
+ .documents, .nested-documents {
136
+ -webkit-touch-callout: none;
137
+ -webkit-user-select: none;
138
+ -khtml-user-select: none;
139
+ -moz-user-select: none;
140
+ -ms-user-select: none;
141
+ user-select: none;
142
+ }
143
+ .documents.grid {
144
+ width: 90%; width: 90vw;
145
+ }
146
+ .documents > li, .nested-documents > li {
147
+ display: block;
148
+ margin-bottom: 1em;
149
+ }
150
+ .documents.grid > li {
151
+ display: inline-block;
152
+ vertical-align: top;
153
+ width: 200px;
154
+ margin-right: 1em;
155
+ }
156
+ .documents.grid > li > a {
157
+ word-break: break-word;
158
+ }
159
+ .documents.grid > li img {
160
+ width: 200px;
161
+ }
162
+ .documents > li:hover, .nested-documents > li:hover {
163
+ background-color: #eee8d5;
164
+ outline: 0.5em solid #eee8d5;
165
+ }
166
+ .nested-documents > li > header {
167
+ background: #002b36;
168
+ height: 1.5em;
169
+ margin-bottom: 1em;
170
+ }
171
+ .documents .sortable-placeholder, .nested-documents .sortable-placeholder {
172
+ border: 1px dashed #839496;
173
+ background-color: #eee8d5;
174
+ }
175
+ .local-menu { font-size: 0.7em; }
176
+
177
+ /* Forms */
178
+
179
+ form {
180
+ margin-bottom: 2em;
181
+ }
182
+
183
+ .field {
184
+ margin-bottom: 1em;
185
+ }
186
+
187
+ input[type=text], input[type=email],
188
+ textarea {
189
+ border: 0px;
190
+ color: #839496;
191
+ background: #ffffff;
192
+ width: 400px;
193
+ padding: 0.5em;
194
+ font-size: 1em;
195
+ }
196
+ input[type=text]:focus, input[type=email]:focus,
197
+ textarea:focus {
198
+ /* color: #002b36; */
199
+ }
200
+
201
+ textarea { height: 6em; resize: vertical; }
202
+ textarea.oneline { height: 1em; resize: none; }
203
+
204
+ fieldset {
205
+ border: 0;
206
+ padding-left: 3em; padding-right: 0;
207
+ }
208
+
209
+ .invalid > label {
210
+ color: #dc322f;
211
+ }
212
+
213
+ /* asmSelect */
214
+
215
+ .asmListItem {
216
+ background-color: #ffffff;
217
+ border: 1px solid #fdf6e3;
218
+ }
219
+ .asmListItem:hover { background-color: #eee8d5; }
220
+ .asmListItemLabel { padding-right: 2em; }
221
+ .asmListSortable .asmListItemLabel {
222
+ cursor: move;
223
+ cursor: -moz-grab;
224
+ cursor: -webkit-grab;
225
+ cursor: grab;
226
+ }
227
+ .asmListSortable .asmListItemLabel:active {
228
+ cursor: -moz-grabbing;
229
+ cursor: -webkit-grabbing;
230
+ cursor: grabbing;
231
+ }
232
+ .asmListItemRemove, .asmListItemRemove:hover { color: #dc322f; }
233
+
234
+
235
+ /\
236
+ / *\
237
+ / # \
238
+ / * \
239
+ / # \
240
+ / # * \
241
+ / * # \
242
+ /______________\
243
+
244
+