populate-me 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
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
+