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,4 @@
1
+ module PopulateMe
2
+ VERSION = '0.12.0'
3
+ end
4
+
@@ -0,0 +1,34 @@
1
+ require File.join(File.dirname(__FILE__), 'lib/populate_me/version')
2
+
3
+ Gem::Specification.new do |s|
4
+
5
+ s.authors = ["Mickael Riga"]
6
+ s.email = ["mig@mypeplum.com"]
7
+ s.homepage = "https://github.com/mig-hub/populate-me"
8
+ s.licenses = ['MIT']
9
+
10
+ s.name = 'populate-me'
11
+ s.version = PopulateMe::VERSION
12
+ s.summary = "PopulateMe is an admin system for web applications."
13
+ s.description = "PopulateMe is an admin system for managing structured content of web applications. It is built on top of the Sinatra framework, but can be used along any framework using Rack."
14
+
15
+ s.platform = Gem::Platform::RUBY
16
+ s.files = `git ls-files`.split("\n").sort
17
+ s.test_files = s.files.grep(/^test\//)
18
+ s.require_paths = ['lib']
19
+
20
+ s.add_dependency 'web-utils', '~> 0'
21
+ s.add_dependency 'sinatra', '~> 2'
22
+ s.add_dependency 'json', '~> 2.1'
23
+
24
+ s.add_development_dependency 'bundler', '~> 1.13'
25
+ s.add_development_dependency 'minitest', '~> 5.8'
26
+ s.add_development_dependency 'rack-test', '~> 0.6'
27
+ s.add_development_dependency 'rack-cerberus', '~> 1.0'
28
+ s.add_development_dependency 'mongo', '~> 2.0'
29
+ s.add_development_dependency 'rack-grid-serve', '~> 0.0.8'
30
+ s.add_development_dependency 'aws-sdk-s3', '~> 1'
31
+ s.add_development_dependency 'racksh', '~> 1.0'
32
+ s.add_development_dependency 'rake', '>= 12.3.3'
33
+ end
34
+
@@ -0,0 +1,37 @@
1
+ ENV['RACK_ENV'] = 'test'
2
+
3
+ require 'minitest/autorun'
4
+ require 'rack/test'
5
+
6
+ module Minitest::Assertions
7
+ def assert_json response
8
+ assert response.content_type=="application/json", "Expected #{response.inspect} to be a JSON response"
9
+ end
10
+ def assert_for_view json, view_name, title=nil
11
+ assert json['template']==view_name, "Expected #{json.inspect} to have 'template' set to '#{view_name}'"
12
+ unless title.nil?
13
+ assert json['page_title']==title, "Expected #{json.inspect} to have 'page_title' set to '#{title}'"
14
+ end
15
+ end
16
+
17
+ def assert_receive obj, meth, retval=nil, args=[]
18
+ mocked_meth = Minitest::Mock.new
19
+ mocked_meth.expect(:call, retval, args)
20
+ obj.stub meth, mocked_meth do
21
+ yield
22
+ end
23
+ assert mocked_meth.verify, "Expected #{obj.inspect} to receive :#{meth}"
24
+ end
25
+ def refute_receive obj, meth
26
+ proof = nil
27
+ obj.stub meth, proc{proof = :received} do
28
+ yield
29
+ assert(proof!=:received, "Expected #{obj.inspect} not to receive :#{meth}")
30
+ end
31
+ end
32
+ end
33
+
34
+ class Minitest::Spec
35
+ include Rack::Test::Methods
36
+ end
37
+
@@ -0,0 +1,183 @@
1
+ require 'helper'
2
+ require 'populate_me/admin'
3
+
4
+ class Admin < PopulateMe::Admin
5
+ enable :sessions
6
+ set :menu, [
7
+ ['Home Details', '/admin/form/home-details/0'],
8
+ ['Project Page', [
9
+ ['Project Page Intro', '/admin/form/project-page-intro/0'],
10
+ ['Projects', '/admin/list/project'],
11
+ ['Checks', [
12
+ ['Check 1', '/check/1'],
13
+ ['Check 2', '/check/2']
14
+ ]]
15
+ ]]
16
+ ]
17
+ end
18
+
19
+ class AdminWithCerberusPass < Admin
20
+ def self.cerberus_pass
21
+ '123'
22
+ end
23
+ end
24
+
25
+ class AdminCerberusNotAvailable < AdminWithCerberusPass
26
+ def self.cerberus_available?
27
+ false
28
+ end
29
+ end
30
+
31
+ class AdminCerberusDisabled < AdminWithCerberusPass
32
+ disable :cerberus
33
+ end
34
+
35
+ describe PopulateMe::Admin do
36
+
37
+ parallelize_me!
38
+
39
+ let(:app) { ::Admin.new }
40
+
41
+ let(:settings) { app.settings }
42
+
43
+ let(:json) { JSON.parse(last_response.body) }
44
+
45
+ describe 'Settings' do
46
+ it 'Sets paths based on the subclass file path' do
47
+ assert_equal __FILE__, settings.app_file
48
+ end
49
+ it 'Has a default value for the page title tag' do
50
+ assert_equal 'Populate Me', settings.meta_title
51
+ end
52
+ it 'Has a default index_path' do
53
+ assert_equal '/menu', settings.index_path
54
+ end
55
+ it 'Has cerberus enabled by default' do
56
+ assert settings.cerberus?
57
+ end
58
+ describe 'when ENV CERBERUS_PASS is not set' do
59
+ it 'Does not have cerberus_active' do
60
+ refute settings.cerberus_active
61
+ end
62
+ end
63
+ describe 'when ENV CERBERUS_PASS is set' do
64
+ let(:app) { AdminWithCerberusPass.new }
65
+ it 'Has cerberus_active' do
66
+ assert settings.cerberus_active
67
+ end
68
+ end
69
+ describe 'when ENV CERBERUS_PASS is set but gem not loaded' do
70
+ let(:app) { AdminCerberusNotAvailable.new }
71
+ it 'Does not have cerberus_active' do
72
+ refute settings.cerberus_active
73
+ end
74
+ end
75
+ describe 'when ENV CERBERUS_PASS is set but cerberus is disabled' do
76
+ let(:app) { AdminCerberusDisabled.new }
77
+ it 'Does not have cerberus_active' do
78
+ refute settings.cerberus_active
79
+ end
80
+ end
81
+ describe 'when Cerberus is active' do
82
+ let(:app) { AdminWithCerberusPass.new }
83
+ it 'Sets logout_path to /logout' do
84
+ assert_equal '/logout', settings.logout_path
85
+ end
86
+ end
87
+ describe 'when Cerberus is not active' do
88
+ it 'Sets logout_path to false' do
89
+ refute settings.logout_path
90
+ end
91
+ end
92
+ end
93
+
94
+ describe 'Middlewares' do
95
+
96
+ it 'Has API middleware mounted on /api' do
97
+ get '/api'
98
+ assert_predicate last_response, :ok?
99
+ assert_json last_response
100
+ assert json['success']
101
+ end
102
+
103
+ it 'Has assets available on /__assets__' do
104
+ get('/__assets__/css/main.css')
105
+ assert_predicate last_response, :ok?
106
+ assert_equal 'text/css', last_response.content_type
107
+ end
108
+
109
+ describe 'when cerberus is active' do
110
+ let(:app) { AdminWithCerberusPass.new }
111
+ it 'Uses Cerberus for authentication' do
112
+ get '/'
113
+ assert_equal 401, last_response.status
114
+ end
115
+ end
116
+ describe 'when cerberus is inactive' do
117
+ it 'Does not use Cerberus' do
118
+ get '/'
119
+ assert_predicate last_response, :ok?
120
+ end
121
+ end
122
+
123
+ end
124
+
125
+ describe 'Handlers' do
126
+
127
+ let(:help_item) {
128
+ { 'title' => '?', 'href' => '/help', 'new_page' => false }
129
+ }
130
+
131
+ describe '/menu' do
132
+
133
+ describe 'when url is root' do
134
+ it 'Returns the correct info' do
135
+ get '/menu'
136
+ assert_predicate last_response, :ok?
137
+ assert_json last_response
138
+ assert_for_view json, 'template_menu', 'Menu'
139
+ expected_h = {
140
+ 'title' => 'Home Details',
141
+ 'href' => '/admin/form/home-details/0',
142
+ 'new_page' => false
143
+ }
144
+ assert_equal expected_h, json['items'][0]
145
+ expected_h = {
146
+ 'title' => 'Project Page',
147
+ 'href' => '/menu/project-page',
148
+ 'new_page' => false
149
+ }
150
+ assert_equal expected_h, json['items'][1]
151
+ end
152
+ it 'Adds help link' do
153
+ get '/menu'
154
+ assert_equal 3, json['items'].size
155
+ assert_equal(help_item, json['items'].last)
156
+ end
157
+ end
158
+ describe 'when url is nested' do
159
+ it 'Returns the correct info' do
160
+ get '/menu/project-page/checks'
161
+ assert_predicate last_response, :ok?
162
+ assert_json last_response
163
+ assert_for_view json, 'template_menu', 'Checks'
164
+ assert_equal 2, json['items'].size
165
+ expected_h = {
166
+ 'title' => 'Check 1',
167
+ 'href' => '/check/1',
168
+ 'new_page' => false
169
+ }
170
+ assert_equal expected_h, json['items'][0]
171
+ end
172
+ it 'Does not add help link' do
173
+ get '/menu/project-page/checks'
174
+ refute_equal(help_item, json['items'].last)
175
+ end
176
+ end
177
+
178
+ end
179
+
180
+ end
181
+
182
+ end
183
+
@@ -0,0 +1,246 @@
1
+ require 'helper'
2
+
3
+ require 'populate_me/document'
4
+ class Band < PopulateMe::Document
5
+ attr_accessor :name, :awsome, :position
6
+ def members; @members ||= []; end
7
+ def validate
8
+ error_on(:name,"WTF") if self.name=='ZZ Top'
9
+ end
10
+ end
11
+ class Band::Member < PopulateMe::Document
12
+ attr_accessor :name
13
+ end
14
+
15
+ require 'populate_me/api'
16
+
17
+ describe 'PopulateMe::API' do
18
+
19
+ # This middleware has the CRUD interface for
20
+ # managing documents through a JSON-based API
21
+ #
22
+ # The API needs the Document class to implement these methods:
23
+ # - Class.admin_get
24
+ # - Needs to be able to accept the ID as a string
25
+ # - The class is responsible for conversion
26
+ # - So a mix of different classes of IDs is not possible
27
+ # - instance.to_h
28
+ # - instance.save
29
+ # - instance.delete
30
+
31
+ parallelize_me!
32
+
33
+ let(:app) {
34
+ PopulateMe::API.new
35
+ }
36
+
37
+ let(:json) {
38
+ JSON.parse(last_response.body)
39
+ }
40
+
41
+ def assert_not_found
42
+ assert_json last_response
43
+ assert_equal 404, last_response.status
44
+ assert_equal 'pass', last_response.headers['X-Cascade']
45
+ refute json['success']
46
+ assert_equal 'Not Found', json['message']
47
+ end
48
+
49
+ def assert_successful_creation
50
+ assert_json last_response
51
+ assert_equal 201, last_response.status
52
+ assert json['success']
53
+ assert_equal 'Created Successfully', json['message']
54
+ end
55
+
56
+ def assert_successful_sorting
57
+ assert_json last_response
58
+ assert_predicate last_response, :ok?
59
+ assert json['success']
60
+ assert_equal 'Sorted Successfully', json['message']
61
+ end
62
+
63
+ def assert_invalid_instance
64
+ assert_json last_response
65
+ assert_equal 400, last_response.status
66
+ refute json['success']
67
+ assert_equal 'Invalid Document', json['message']
68
+ end
69
+
70
+ def assert_successful_instance
71
+ assert_json last_response
72
+ assert_predicate last_response, :ok?
73
+ assert json['success']
74
+ end
75
+
76
+ def assert_successful_update
77
+ assert_json last_response
78
+ assert_predicate last_response, :ok?
79
+ assert json['success']
80
+ assert_equal 'Updated Successfully', json['message']
81
+ end
82
+
83
+ def assert_successful_deletion
84
+ assert_json last_response
85
+ assert_predicate last_response, :ok?
86
+ assert json['success']
87
+ assert_equal 'Deleted Successfully', json['message']
88
+ end
89
+
90
+ describe 'GET /version' do
91
+ it 'Returns the PopulateMe version' do
92
+ get('/version')
93
+ assert_json last_response
94
+ assert_predicate last_response, :ok?
95
+ assert json['success']
96
+ assert_equal PopulateMe::VERSION, json['version']
97
+ end
98
+ end
99
+
100
+ describe 'POST /:model' do
101
+
102
+ it 'Creates successfully' do
103
+ post('/band', {data: {id: 'neurosis', name: 'Neurosis'}})
104
+ assert_successful_creation
105
+ assert_equal 'Neurosis', json['data']['name']
106
+ end
107
+
108
+ it 'Typecasts before creating' do
109
+ post('/band', {data: {name: 'Arcade Fire', awsome: 'true'}})
110
+ assert_successful_creation
111
+ assert json['data']['awsome']
112
+ end
113
+
114
+ it 'Can create a doc even if no data is sent' do
115
+ post '/band'
116
+ assert_successful_creation
117
+ end
118
+
119
+ it 'Fails if the doc is invalid' do
120
+ post('/band', {data: {id: 'invalid_doc_post', name: 'ZZ Top'}})
121
+ assert_invalid_instance
122
+ assert_equal({'name'=>['WTF']}, json['data'])
123
+ assert_nil Band.admin_get('invalid_doc_post')
124
+ end
125
+
126
+ it 'Redirects if destination is given' do
127
+ post '/band', {'_destination'=>'http://example.org/anywhere'}
128
+ assert_equal 302, last_response.status
129
+ assert_equal 'http://example.org/anywhere', last_response.header['Location']
130
+ end
131
+
132
+ end
133
+
134
+ describe 'PUT /:model' do
135
+
136
+ it 'Can set indexes for sorting' do
137
+ post('/band', {data: {id: 'sortable1', name: 'Sortable 1'}})
138
+ post('/band', {data: {id: 'sortable2', name: 'Sortable 2'}})
139
+ post('/band', {data: {id: 'sortable3', name: 'Sortable 3'}})
140
+ put '/band', {
141
+ 'action'=>'sort',
142
+ 'field'=>'position',
143
+ 'ids'=> ['sortable2','sortable3','sortable1']
144
+ }
145
+ assert_successful_sorting
146
+ assert_equal 0, Band.admin_get('sortable2').position
147
+ assert_equal 1, Band.admin_get('sortable3').position
148
+ assert_equal 2, Band.admin_get('sortable1').position
149
+ end
150
+
151
+ it 'Redirects after sorting if destination is given' do
152
+ post('/band', {data: {id: 'redirectsortable1', name: 'Redirect Sortable 1'}})
153
+ post('/band', {data: {id: 'redirectsortable2', name: 'Redirect Sortable 2'}})
154
+ post('/band', {data: {id: 'redirectsortable3', name: 'Redirect Sortable 3'}})
155
+ put '/band', {
156
+ 'action'=>'sort',
157
+ 'field'=>'position',
158
+ 'ids'=> ['redirectsortable2','redirectsortable3','redirectsortable1'],
159
+ '_destination'=>'http://example.org/anywhere'
160
+ }
161
+ assert_equal 302, last_response.status
162
+ assert_equal 'http://example.org/anywhere', last_response.header['Location']
163
+ end
164
+
165
+ end
166
+
167
+ describe 'GET /:model/:id' do
168
+ it 'Sends a not-found when the model is not a class' do
169
+ get('/wizz/42')
170
+ assert_not_found
171
+ end
172
+ it 'Sends not-found when the model is a class but not a model' do
173
+ get('/string/42')
174
+ assert_not_found
175
+ end
176
+ it 'Sends not-found when the id is not provided' do
177
+ get('/band/')
178
+ assert_not_found
179
+ end
180
+ it 'Sends not-found when the instance does not exist' do
181
+ get('/band/666')
182
+ assert_not_found
183
+ end
184
+ it 'Sends the instance if it exists' do
185
+ post('/band', {data: {id: 'sendable', name: 'Morphine'}})
186
+ get('/band/sendable')
187
+ assert_successful_instance
188
+ assert_equal Band.admin_get('sendable').to_h, json['data']
189
+ end
190
+ end
191
+
192
+ describe 'PUT /:model/:id' do
193
+ it 'Sends not-found if the instance does not exist' do
194
+ put('/band/666')
195
+ assert_not_found
196
+ end
197
+ it 'Fails if the document is invalid' do
198
+ post('/band', {data: {id: 'invalid_doc_put', name: 'Valid here'}})
199
+ put('/band/invalid_doc_put', {data: {name: 'ZZ Top'}})
200
+ assert_invalid_instance
201
+ assert_equal({'name'=>['WTF']}, json['data'])
202
+ refute_equal 'ZZ Top', Band.admin_get('invalid_doc_put').name
203
+ end
204
+ it 'Updates documents' do
205
+ post('/band', {data: {id: 'updatable', name: 'Updatable'}})
206
+ put('/band/updatable', {data: {awsome: 'yes'}})
207
+ assert_successful_update
208
+ obj = Band.admin_get('updatable')
209
+ assert_equal 'yes', obj.awsome
210
+ assert_equal 'Updatable', obj.name
211
+ end
212
+ # it 'Updates nested documents' do
213
+ # obj = Band.admin_get('3')
214
+ # put('/band/3', {data: {members: [
215
+ # {id: obj.members[0].id, _class: 'Band::Member', name: 'Joey Ramone'},
216
+ # {id: obj.members[1].id, _class: 'Band::Member'},
217
+ # ]}})
218
+ # assert_successful_update
219
+ # obj = Band.admin_get('3')
220
+ # assert_equal 'yes', obj.awsome
221
+ # assert_equal 'The Ramones', obj.name
222
+ # assert_equal 2, obj.members.size
223
+ # assert_equal 'Joey Ramone', obj.members[0].name
224
+ # assert_equal 'Deedee Ramone', obj.members[1].name
225
+ # end
226
+ end
227
+
228
+ describe 'DELETE /:model/:id' do
229
+ it 'Sends not-found if the instance does not exist' do
230
+ delete('/band/666')
231
+ assert_not_found
232
+ end
233
+ it 'Returns a deletion response when the instance exists' do
234
+ post('/band', {data: {id: 'deletable', name: '1D'}})
235
+ delete('/band/deletable')
236
+ assert_successful_deletion
237
+ assert_instance_of Hash, json['data']
238
+ end
239
+ it 'Redirects if destination is given' do
240
+ delete('/band/2', {'_destination'=>'http://example.org/anywhere'})
241
+ assert_equal 302, last_response.status
242
+ assert_equal 'http://example.org/anywhere', last_response.header['Location']
243
+ end
244
+ end
245
+ end
246
+