rear 0.2.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 (121) hide show
  1. checksums.yaml +15 -0
  2. data/.travis.yml +7 -0
  3. data/CHANGELOG.md +7 -0
  4. data/Gemfile +20 -0
  5. data/LICENSE +19 -0
  6. data/README.md +101 -0
  7. data/Rakefile +79 -0
  8. data/assets/api.js +307 -0
  9. data/assets/bootstrap-datetimepicker/css/bootstrap-datetimepicker.min.css +8 -0
  10. data/assets/bootstrap-datetimepicker/js/bootstrap-datetimepicker.min.js +26 -0
  11. data/assets/bootstrap/css/bootstrap-responsive.min.css +9 -0
  12. data/assets/bootstrap/css/bootstrap.min.css +9 -0
  13. data/assets/bootstrap/img/glyphicons-halflings-white.png +0 -0
  14. data/assets/bootstrap/img/glyphicons-halflings.png +0 -0
  15. data/assets/bootstrap/js/bootstrap.min.js +6 -0
  16. data/assets/jquery.js +5 -0
  17. data/assets/noty/jquery.noty.js +520 -0
  18. data/assets/noty/layouts/top.js +34 -0
  19. data/assets/noty/layouts/topRight.js +43 -0
  20. data/assets/noty/promise.js +432 -0
  21. data/assets/noty/themes/default.js +156 -0
  22. data/assets/select2-bootstrap.css +86 -0
  23. data/assets/select2/select2-spinner.gif +0 -0
  24. data/assets/select2/select2.css +652 -0
  25. data/assets/select2/select2.min.js +22 -0
  26. data/assets/select2/select2.png +0 -0
  27. data/assets/select2/select2x2.png +0 -0
  28. data/assets/ui.css +75 -0
  29. data/assets/xhr.js +4 -0
  30. data/bin/rear +65 -0
  31. data/docs/Assocs.md +100 -0
  32. data/docs/Columns.md +404 -0
  33. data/docs/Deploy.md +62 -0
  34. data/docs/FileManager.md +75 -0
  35. data/docs/Filters.md +341 -0
  36. data/docs/Setup.md +201 -0
  37. data/lib/rear.rb +13 -0
  38. data/lib/rear/actions.rb +98 -0
  39. data/lib/rear/constants.rb +61 -0
  40. data/lib/rear/controller_setup.rb +249 -0
  41. data/lib/rear/helpers.rb +17 -0
  42. data/lib/rear/helpers/class.rb +46 -0
  43. data/lib/rear/helpers/columns.rb +68 -0
  44. data/lib/rear/helpers/filters.rb +147 -0
  45. data/lib/rear/helpers/generic.rb +73 -0
  46. data/lib/rear/helpers/order.rb +47 -0
  47. data/lib/rear/helpers/pager.rb +35 -0
  48. data/lib/rear/helpers/render.rb +37 -0
  49. data/lib/rear/home_controller.rb +10 -0
  50. data/lib/rear/input.rb +341 -0
  51. data/lib/rear/orm.rb +73 -0
  52. data/lib/rear/rear.rb +74 -0
  53. data/lib/rear/setup.rb +9 -0
  54. data/lib/rear/setup/associations.rb +33 -0
  55. data/lib/rear/setup/columns.rb +109 -0
  56. data/lib/rear/setup/filters.rb +314 -0
  57. data/lib/rear/setup/generic.rb +59 -0
  58. data/lib/rear/setup/menu.rb +39 -0
  59. data/lib/rear/templates/editor/ace.slim +7 -0
  60. data/lib/rear/templates/editor/assocs.slim +10 -0
  61. data/lib/rear/templates/editor/boolean.slim +5 -0
  62. data/lib/rear/templates/editor/bulk_edit.slim +75 -0
  63. data/lib/rear/templates/editor/checkbox.slim +5 -0
  64. data/lib/rear/templates/editor/ckeditor.slim +7 -0
  65. data/lib/rear/templates/editor/date.slim +9 -0
  66. data/lib/rear/templates/editor/datetime.slim +9 -0
  67. data/lib/rear/templates/editor/layout.slim +103 -0
  68. data/lib/rear/templates/editor/password.slim +1 -0
  69. data/lib/rear/templates/editor/radio.slim +5 -0
  70. data/lib/rear/templates/editor/select.slim +3 -0
  71. data/lib/rear/templates/editor/string.slim +1 -0
  72. data/lib/rear/templates/editor/text.slim +1 -0
  73. data/lib/rear/templates/editor/time.slim +9 -0
  74. data/lib/rear/templates/error.slim +36 -0
  75. data/lib/rear/templates/filters/boolean.slim +10 -0
  76. data/lib/rear/templates/filters/checkbox.slim +15 -0
  77. data/lib/rear/templates/filters/date.slim +10 -0
  78. data/lib/rear/templates/filters/datetime.slim +10 -0
  79. data/lib/rear/templates/filters/layout.slim +26 -0
  80. data/lib/rear/templates/filters/radio.slim +14 -0
  81. data/lib/rear/templates/filters/select.slim +9 -0
  82. data/lib/rear/templates/filters/string.slim +3 -0
  83. data/lib/rear/templates/filters/text.slim +3 -0
  84. data/lib/rear/templates/filters/time.slim +10 -0
  85. data/lib/rear/templates/home.slim +0 -0
  86. data/lib/rear/templates/layout.slim +78 -0
  87. data/lib/rear/templates/pager.slim +22 -0
  88. data/lib/rear/templates/pane/ace.slim +2 -0
  89. data/lib/rear/templates/pane/assocs.slim +62 -0
  90. data/lib/rear/templates/pane/boolean.slim +2 -0
  91. data/lib/rear/templates/pane/checkbox.slim +5 -0
  92. data/lib/rear/templates/pane/ckeditor.slim +2 -0
  93. data/lib/rear/templates/pane/date.slim +2 -0
  94. data/lib/rear/templates/pane/datetime.slim +2 -0
  95. data/lib/rear/templates/pane/layout.slim +111 -0
  96. data/lib/rear/templates/pane/password.slim +2 -0
  97. data/lib/rear/templates/pane/quickview.slim +21 -0
  98. data/lib/rear/templates/pane/radio.slim +5 -0
  99. data/lib/rear/templates/pane/select.slim +5 -0
  100. data/lib/rear/templates/pane/string.slim +2 -0
  101. data/lib/rear/templates/pane/text.slim +2 -0
  102. data/lib/rear/templates/pane/time.slim +2 -0
  103. data/lib/rear/utils.rb +288 -0
  104. data/rear.gemspec +27 -0
  105. data/test/helpers.rb +33 -0
  106. data/test/models/ar.rb +52 -0
  107. data/test/models/dm.rb +53 -0
  108. data/test/models/sq.rb +58 -0
  109. data/test/setup.rb +4 -0
  110. data/test/templates/adhoc/book/editor/name.slim +1 -0
  111. data/test/templates/adhoc/book/editor/string.slim +1 -0
  112. data/test/templates/adhoc/book/pane/name.slim +1 -0
  113. data/test/templates/adhoc/book/pane/string.slim +1 -0
  114. data/test/templates/shared/shared-templates/editor/string.slim +1 -0
  115. data/test/templates/shared/shared-templates/pane/string.slim +1 -0
  116. data/test/test__assocs.rb +249 -0
  117. data/test/test__columns.rb +269 -0
  118. data/test/test__crud.rb +81 -0
  119. data/test/test__custom_templates.rb +147 -0
  120. data/test/test__filters.rb +228 -0
  121. metadata +220 -0
@@ -0,0 +1,5 @@
1
+ div *attrs(column, :pane)
2
+ - options.each_pair do |k,v|
3
+ - if active_options.include?(k)
4
+ = v
5
+ br
@@ -0,0 +1,2 @@
1
+ div *attrs(column, :pane)
2
+ = value
@@ -0,0 +1,2 @@
1
+ div *attrs(column, :pane)
2
+ = value
@@ -0,0 +1,2 @@
1
+ div *attrs(column, :pane)
2
+ = value.kind_of?(Time) ? value.strftime('%H:%M:%S') : value
data/lib/rear/utils.rb ADDED
@@ -0,0 +1,288 @@
1
+ module RearUtils
2
+ include RearConstants
3
+
4
+ # just define controller, do not set model
5
+ def initialize_model_controller model
6
+ return model.const_get(:RearController) if model.const_defined?(:RearController)
7
+ ctrl = model.const_set(:RearController, Class.new(E))
8
+ RearControllerSetup.init(ctrl)
9
+ ctrl.map EUtils.class_to_route(model)
10
+ ctrl.model model
11
+ ctrl
12
+ end
13
+ module_function :initialize_model_controller
14
+
15
+ def associated_model_controller model, ensure_mounted = false
16
+ ctrl = ObjectSpace.each_object(Class).find do |o|
17
+ EUtils.is_app?(o) && o.respond_to?(:model) && o.model == model
18
+ end
19
+ unless ctrl
20
+ ctrl = initialize_model_controller(model)
21
+ ctrl.label(false) # automatically generated controllers not shown in menu
22
+ end
23
+ ctrl.mounted? || ctrl.mount if ensure_mounted
24
+ ctrl
25
+ end
26
+ module_function :associated_model_controller
27
+
28
+ def extract_assocs model, *args
29
+ send('extract_%s_assocs' % orm(model), model, *args)
30
+ end
31
+ module_function :extract_assocs
32
+
33
+ def extract_associated_ar_model model, assoc
34
+ class_name = assoc.options[:class_name] ||
35
+ ActiveSupport::Inflector.camelize(ActiveSupport::Inflector.singularize(assoc.name.to_s))
36
+ if (ns = class_name.to_s.split('::')).size > 1
37
+ return ns.inject(Object) {|n,c| n.const_get c}
38
+ else
39
+ if (ns = model.name.split('::')).size > 1
40
+ ns.pop
41
+ model_namespace = ns.inject(Object) {|n,c| n.const_get c}
42
+ else
43
+ model_namespace = Object
44
+ end
45
+ return model_namespace.const_get(class_name)
46
+ end
47
+ nil
48
+ end
49
+ module_function :extract_associated_ar_model
50
+
51
+ def extract_ar_assocs model, any = false
52
+ model.reflect_on_all_associations.inject(ASSOCS__STRUCT.call) do |map, r|
53
+
54
+ target_model = extract_associated_ar_model(model, r) ||
55
+ raise(NameError, "Was unable to detect model for %s relation" % r)
56
+
57
+ target_pkey = r.options[:primary_key] || target_model.primary_key
58
+ if any || target_pkey # models without primary key not handled by default
59
+
60
+ target_pkey = target_pkey.to_sym if target_pkey
61
+ readonly = nil
62
+ belongs_to_keys = {source: nil, target: nil}
63
+
64
+ assoc_type = case r.macro
65
+ when :belongs_to
66
+ belongs_to_keys[:source] = (r.options[:foreign_key] || ActiveSupport::Inflector.foreign_key(r.name)).to_sym
67
+ belongs_to_keys[:target] = target_pkey
68
+ :belongs_to
69
+ when :has_one
70
+ readonly = true if r.options[:through]
71
+ :has_one
72
+ else
73
+ :has_many
74
+ end
75
+
76
+ map[assoc_type].update r.name => {
77
+ type: assoc_type,
78
+ name: r.name,
79
+ remote_model: target_model,
80
+ remote_pkey: target_pkey,
81
+ readonly: readonly,
82
+ dom_id: dom_id_generator(model, assoc_type, r.name),
83
+ belongs_to_keys: belongs_to_keys
84
+ }.freeze
85
+ end
86
+ map
87
+ end.freeze
88
+ end
89
+ module_function :extract_ar_assocs
90
+
91
+ def extract_dm_assocs model, any = false
92
+ model.relationships.entries.inject(ASSOCS__STRUCT.call) do |map, r|
93
+ _, target_pkey = extract_dm_columns(r.target_model)
94
+ if any || target_pkey # only models with a primary key are handled by default
95
+
96
+ readonly = nil
97
+ belongs_to_keys = {source: nil, target: nil}
98
+
99
+ assoc_type = case r.class.name.split('::')[2]
100
+ when 'ManyToOne'
101
+ belongs_to_keys[:source] = r.source_key.first.name.to_sym
102
+ belongs_to_keys[:target] = r.target_key.first.name.to_sym
103
+ :belongs_to
104
+ when 'OneToOne'
105
+ readonly = true if r.options[:through]
106
+ :has_one
107
+ else
108
+ :has_many
109
+ end
110
+
111
+ map[assoc_type].update r.name => {
112
+ type: assoc_type,
113
+ name: r.name,
114
+ remote_model: r.target_model,
115
+ remote_pkey: target_pkey,
116
+ readonly: readonly,
117
+ dom_id: dom_id_generator(model, assoc_type, r.name),
118
+ belongs_to_keys: belongs_to_keys
119
+ }.freeze
120
+ end
121
+ map
122
+ end.freeze
123
+ end
124
+ module_function :extract_dm_assocs
125
+
126
+ def extract_sq_assocs model, any = false
127
+ model.associations.inject(ASSOCS__STRUCT.call) do |map,an|
128
+ a = model.association_reflection(an)
129
+
130
+ target_model = extract_constant(a[:class_name])
131
+ _, target_pkey = extract_sq_columns(target_model)
132
+
133
+ if any || target_pkey # only models with a primary key are handled by default
134
+ readonly = nil
135
+ belongs_to_keys = {source: nil, target: nil}
136
+
137
+ assoc_type = case a[:type]
138
+ when :many_to_one
139
+ belongs_to_keys[:source] = a[:key]
140
+ belongs_to_keys[:target] = a[:primary_key] || target_pkey
141
+ :belongs_to
142
+ when :one_to_one
143
+ readonly = true if a[:through]
144
+ :has_one
145
+ else
146
+ :has_many
147
+ end
148
+
149
+ map[assoc_type].update a[:name] => {
150
+ type: assoc_type,
151
+ name: a[:name],
152
+ remote_model: target_model,
153
+ remote_pkey: target_pkey,
154
+ readonly: readonly,
155
+ dom_id: dom_id_generator(model, assoc_type, a[:name]),
156
+ belongs_to_keys: belongs_to_keys
157
+ }.freeze
158
+ end
159
+ map
160
+ end.freeze
161
+ end
162
+ module_function :extract_sq_assocs
163
+
164
+ def extract_columns model
165
+ send('extract_%s_columns' % orm(model), model)
166
+ end
167
+ module_function :extract_columns
168
+
169
+ def extract_ar_columns model
170
+ unless model.table_exists?
171
+ puts "WARN: %s table does not exists!" % model.table_name
172
+ return [[], :id]
173
+ end
174
+ pkey = nil
175
+ columns = model.columns.inject([]) do |f,c|
176
+ name, type = c.name.to_sym, c.type.to_s.downcase.to_sym
177
+ type = COLUMNS__DEFAULT_TYPE unless COLUMNS__HANDLED_TYPES.include?(type)
178
+ (c.primary && pkey = name) ? f : f << [name, type]
179
+ end
180
+ [columns.freeze, pkey]
181
+ end
182
+ module_function :extract_ar_columns
183
+
184
+ def extract_dm_columns model
185
+ pkey = nil
186
+ # TODO: find a way to handle Enum columns
187
+ columns = model.properties.inject([]) do |f,c|
188
+ name, type = c.name.to_sym, c.class.name.to_s.split('::').last.downcase.to_sym
189
+ type = COLUMNS__DEFAULT_TYPE unless [:serial].concat(COLUMNS__HANDLED_TYPES).include?(type)
190
+ (type == :serial && pkey = name) ? f : f << [name, type]
191
+ end
192
+ [columns.freeze, pkey]
193
+ end
194
+ module_function :extract_dm_columns
195
+
196
+ def extract_sq_columns model
197
+ pkey = nil
198
+ columns = model.db_schema.inject([]) do |map,(n,s)|
199
+ if s[:primary_key]
200
+ pkey = n
201
+ map
202
+ else
203
+ type = s[:db_type].to_s.split(/\W/).first.to_s.downcase.to_sym
204
+ unless COLUMNS__HANDLED_TYPES.include?(type)
205
+ type = s[:type].to_s.downcase.to_sym
206
+ unless COLUMNS__HANDLED_TYPES.include?(type)
207
+ type = COLUMNS__DEFAULT_TYPE
208
+ end
209
+ end
210
+ map << [n, type]
211
+ end
212
+ end
213
+ [columns.freeze, pkey]
214
+ end
215
+ module_function :extract_sq_columns
216
+
217
+ def quote_column model, column
218
+ send('quote_%s_column' % orm(model), model, column)
219
+ end
220
+ module_function :quote_column
221
+
222
+ def quote_ar_column model, column
223
+ model.connection.quote_column_name(column)
224
+ end
225
+ module_function :quote_ar_column
226
+
227
+ def quote_dm_column model, column
228
+ property = model.properties.find {|p| p.name == column}
229
+ model.repository(model.repository_name).adapter.property_to_column_name(property, false)
230
+ end
231
+ module_function :quote_dm_column
232
+
233
+ def quote_sq_column model, column
234
+ model.db.quote_identifier(column)
235
+ end
236
+ module_function :quote_sq_column
237
+
238
+ def ar? model
239
+ [:connection, :columns, :reflect_on_all_associations].all? do |m|
240
+ model.respond_to?(m)
241
+ end
242
+ end
243
+ module_function :ar?
244
+
245
+ def dm? model
246
+ [:repository, :properties, :relationships].all? do |m|
247
+ model.respond_to?(m)
248
+ end
249
+ end
250
+ module_function :dm?
251
+
252
+ def sq? model
253
+ [:db_schema, :columns, :dataset, :associations].all? do |m|
254
+ model.respond_to?(m)
255
+ end
256
+ end
257
+ module_function :sq?
258
+
259
+ def orm model
260
+ [:ar, :dm, :sq].find {|o| send('%s?' % o, model)}
261
+ end
262
+ alias is_orm? orm
263
+ module_function :orm
264
+ module_function :is_orm?
265
+
266
+ def number_with_delimiter n
267
+ n.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, '\1,')
268
+ end
269
+ module_function :number_with_delimiter
270
+
271
+ def normalize_html_attrs attrs
272
+ (attrs||{}).inject({}) {|h,(k,v)| h.merge k.to_s.downcase => v}
273
+ end
274
+ module_function :normalize_html_attrs
275
+
276
+ def dom_id_generator *args
277
+ (args + [args.__id__]).flatten.join('__').gsub(/\W+/, '')
278
+ end
279
+ module_function :dom_id_generator
280
+
281
+ def extract_constant smth
282
+ return Object.const_get(smth) if smth.is_a?(Symbol)
283
+ return smth.sub('::', '').split('::').inject(Object) {|o,c| o.const_get(c)} if smth.is_a?(String)
284
+ smth
285
+ end
286
+ module_function :extract_constant
287
+
288
+ end
data/rear.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # encoding: UTF-8
2
+
3
+ version = '0.2.0'
4
+ Gem::Specification.new do |s|
5
+
6
+ s.name = 'rear'
7
+ s.version = version
8
+ s.authors = ['Silviu Rusu']
9
+ s.email = ['slivuz@gmail.com']
10
+ s.homepage = 'https://github.com/espresso/rear'
11
+ s.summary = 'rear-%s' % version
12
+ s.description = 'ORM-agnostic CRUDifier for Espresso Framework'
13
+
14
+ s.required_ruby_version = '>= 1.9.2'
15
+
16
+ s.add_dependency 'e', '>= 0.4.8'
17
+ s.add_dependency 'el', '>= 0.4.8'
18
+ s.add_dependency 'slim', '>= 2.0'
19
+
20
+ s.add_development_dependency 'bundler'
21
+
22
+ s.require_paths = ['lib']
23
+ s.files = Dir['**/{*,.[a-z]*}'].reject {|e| e =~ /\.(gem|lock)\Z/}
24
+ s.executables = ['rear']
25
+
26
+ s.licenses = ['MIT']
27
+ end
data/test/helpers.rb ADDED
@@ -0,0 +1,33 @@
1
+ module EASpecHelpers
2
+
3
+ def new_book data = {}
4
+ data = {name: random_string, about: random_string}.merge(data)
5
+ item = Book.create(data)
6
+ item_id = item[:id].to_i
7
+ check(item_id) > 0
8
+ [item, item_id]
9
+ end
10
+
11
+ def count_books conditions = nil
12
+ args = conditions ? [{conditions: conditions}] : []
13
+ RearORM.new(Book).count *args
14
+ end
15
+
16
+ def extract_elements selector = nil
17
+ selector ||= last_request.env['PATH_INFO'] =~ /\/+edit\/+\d+/ ?
18
+ '.editor-column_value' : '.pane-column_value'
19
+ doc = Nokogiri::HTML(last_response.body)
20
+ columns = doc.css(selector)
21
+ columns
22
+ end
23
+
24
+ def readonly_error? last_response
25
+ is(last_response).client_error?
26
+ does(last_response.body) =~ /readonly/i
27
+ end
28
+
29
+ def random_string
30
+ ('A'..'Z').to_a.sample(5).join + [rand.to_s, rand.to_s].sample
31
+ end
32
+
33
+ end
data/test/models/ar.rb ADDED
@@ -0,0 +1,52 @@
1
+ ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
2
+
3
+ class Author < ActiveRecord::Base
4
+ end
5
+
6
+ class Genre < ActiveRecord::Base
7
+ end
8
+
9
+ class Book < ActiveRecord::Base
10
+ belongs_to :author
11
+ has_and_belongs_to_many :genres
12
+ end
13
+
14
+ class State < ActiveRecord::Base
15
+ has_many :cities, foreign_key: :state_code, primary_key: :code
16
+ end
17
+
18
+ class City < ActiveRecord::Base
19
+ belongs_to :state, foreign_key: :state_code, primary_key: :code
20
+ end
21
+
22
+ ActiveRecord::Migration.create_table :books_genres do |t|
23
+ t.integer :book_id
24
+ t.integer :genre_id
25
+ end
26
+
27
+ ActiveRecord::Migration.create_table :genres do |t|
28
+ t.string :name
29
+ end
30
+
31
+ ActiveRecord::Migration.create_table :authors do |t|
32
+ t.string :name
33
+ end
34
+
35
+ ActiveRecord::Migration.create_table :books do |t|
36
+ t.integer :author_id
37
+ t.string :name
38
+ t.text :about
39
+ t.string :cover
40
+ t.string :colors
41
+ t.date :created_at
42
+ end
43
+
44
+ ActiveRecord::Migration.create_table :states do |t|
45
+ t.string :name
46
+ t.string :code
47
+ end
48
+
49
+ ActiveRecord::Migration.create_table :cities do |t|
50
+ t.string :name
51
+ t.string :state_code
52
+ end
data/test/models/dm.rb ADDED
@@ -0,0 +1,53 @@
1
+ DataMapper.setup :default, 'sqlite::memory:'
2
+
3
+ class Author
4
+ include DataMapper::Resource
5
+
6
+ property :id, Serial
7
+ property :name, String
8
+ end
9
+
10
+ class Genre
11
+ include DataMapper::Resource
12
+
13
+ property :id, Serial
14
+ property :name, String
15
+
16
+ has n, :books, through: Resource
17
+ end
18
+
19
+ class Book
20
+ include DataMapper::Resource
21
+
22
+ property :id, Serial
23
+ property :name, String
24
+ property :about, Text
25
+ property :cover, String
26
+ property :colors, String
27
+ property :created_at, Date
28
+
29
+ belongs_to :author, required: false
30
+ has n, :genres, through: Resource
31
+ end
32
+
33
+ class State
34
+ include DataMapper::Resource
35
+
36
+ property :id, Serial
37
+ property :name, String, index: true
38
+ property :code, String, unique: true, length: 2
39
+
40
+ has n, :cities, child_key: :state_code, parent_key: :code
41
+ end
42
+
43
+ class City
44
+ include DataMapper::Resource
45
+
46
+ property :id, Serial
47
+ property :name, String, index: true
48
+
49
+ belongs_to :state, child_key: :state_code, parent_key: :code, required: false
50
+ end
51
+
52
+ DataMapper.finalize
53
+ DataMapper.auto_migrate!