engine2 1.0.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 (145) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +2 -0
  3. data/Rakefile +138 -0
  4. data/conf/message.yaml +93 -0
  5. data/conf/message_pl.yaml +93 -0
  6. data/engine2.gemspec +34 -0
  7. data/lib/engine2.rb +34 -0
  8. data/lib/engine2/action.rb +217 -0
  9. data/lib/engine2/core.rb +572 -0
  10. data/lib/engine2/handler.rb +134 -0
  11. data/lib/engine2/meta.rb +969 -0
  12. data/lib/engine2/meta/decode_meta.rb +110 -0
  13. data/lib/engine2/meta/delete_meta.rb +73 -0
  14. data/lib/engine2/meta/form_meta.rb +144 -0
  15. data/lib/engine2/meta/infra_meta.rb +292 -0
  16. data/lib/engine2/meta/link_meta.rb +133 -0
  17. data/lib/engine2/meta/list_meta.rb +284 -0
  18. data/lib/engine2/meta/save_meta.rb +63 -0
  19. data/lib/engine2/meta/view_meta.rb +22 -0
  20. data/lib/engine2/model.rb +390 -0
  21. data/lib/engine2/models/Files.rb +38 -0
  22. data/lib/engine2/models/UserInfo.rb +24 -0
  23. data/lib/engine2/post_bootstrap.rb +83 -0
  24. data/lib/engine2/pre_bootstrap.rb +27 -0
  25. data/lib/engine2/scheme.rb +202 -0
  26. data/lib/engine2/templates.rb +229 -0
  27. data/lib/engine2/type_info.rb +342 -0
  28. data/lib/engine2/version.rb +9 -0
  29. data/public/assets/javascripts.js +13 -0
  30. data/public/assets/styles.css +4 -0
  31. data/public/css/angular-motion.css +1022 -0
  32. data/public/css/angular-ui-tree.min.css +1 -0
  33. data/public/css/app.css +196 -0
  34. data/public/css/bootstrap-additions.css +1560 -0
  35. data/public/css/bootstrap.min.css +11 -0
  36. data/public/css/font-awesome.min.css +4 -0
  37. data/public/favicon.ico +0 -0
  38. data/public/fonts/FontAwesome.otf +0 -0
  39. data/public/fonts/fontawesome-webfont.eot +0 -0
  40. data/public/fonts/fontawesome-webfont.svg +655 -0
  41. data/public/fonts/fontawesome-webfont.ttf +0 -0
  42. data/public/fonts/fontawesome-webfont.woff +0 -0
  43. data/public/fonts/fontawesome-webfont.woff2 +0 -0
  44. data/public/fonts/glyphicons-halflings-regular.eot +0 -0
  45. data/public/fonts/glyphicons-halflings-regular.svg +288 -0
  46. data/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  47. data/public/fonts/glyphicons-halflings-regular.woff +0 -0
  48. data/public/fonts/glyphicons-halflings-regular.woff2 +0 -0
  49. data/public/images/file.png +0 -0
  50. data/public/images/folder-closed.png +0 -0
  51. data/public/images/folder.png +0 -0
  52. data/public/images/node-closed-2.png +0 -0
  53. data/public/images/node-closed-light.png +0 -0
  54. data/public/images/node-closed.png +0 -0
  55. data/public/images/node-opened-2.png +0 -0
  56. data/public/images/node-opened-light.png +0 -0
  57. data/public/images/node-opened.png +0 -0
  58. data/public/img/ajax-loader-dark.gif +0 -0
  59. data/public/img/ajax-loader-light.gif +0 -0
  60. data/public/img/ajax-loader.gif +0 -0
  61. data/public/js/angular-animate.js +4115 -0
  62. data/public/js/angular-cookies.js +322 -0
  63. data/public/js/angular-local-storage.js +455 -0
  64. data/public/js/angular-route.js +1022 -0
  65. data/public/js/angular-sanitize.js +717 -0
  66. data/public/js/angular-strap.js +4339 -0
  67. data/public/js/angular-strap.tpl.js +43 -0
  68. data/public/js/angular-ui-tree.js +1569 -0
  69. data/public/js/angular.js +30714 -0
  70. data/public/js/i18n/angular-locale_pl.js +115 -0
  71. data/public/js/lodash.custom.min.js +97 -0
  72. data/public/js/ng-file-upload-shim.min.js +2 -0
  73. data/public/js/ng-file-upload.min.js +3 -0
  74. data/views/app.coffee +3 -0
  75. data/views/engine2.coffee +557 -0
  76. data/views/engine2actions.coffee +849 -0
  77. data/views/engine2templates.coffee +0 -0
  78. data/views/fields/blob.slim +22 -0
  79. data/views/fields/bs_select.slim +10 -0
  80. data/views/fields/bsselect_picker.slim +18 -0
  81. data/views/fields/bsselect_picker_opt.slim +22 -0
  82. data/views/fields/checkbox.slim +11 -0
  83. data/views/fields/checkbox_buttons.slim +6 -0
  84. data/views/fields/checkbox_buttons_opt.slim +8 -0
  85. data/views/fields/currency.slim +10 -0
  86. data/views/fields/date.slim +21 -0
  87. data/views/fields/date_range.slim +44 -0
  88. data/views/fields/date_time.slim +42 -0
  89. data/views/fields/datetime.slim +42 -0
  90. data/views/fields/decimal.slim +11 -0
  91. data/views/fields/decimal_date.slim +22 -0
  92. data/views/fields/decimal_time.slim +26 -0
  93. data/views/fields/email.slim +13 -0
  94. data/views/fields/file_store.slim +61 -0
  95. data/views/fields/input_text.slim +14 -0
  96. data/views/fields/integer.slim +11 -0
  97. data/views/fields/list_bsselect.slim +18 -0
  98. data/views/fields/list_bsselect_opt.slim +21 -0
  99. data/views/fields/list_buttons.slim +3 -0
  100. data/views/fields/list_buttons_opt.slim +5 -0
  101. data/views/fields/list_select.slim +11 -0
  102. data/views/fields/list_select_opt.slim +15 -0
  103. data/views/fields/password.slim +14 -0
  104. data/views/fields/radio_checkbox.slim +10 -0
  105. data/views/fields/scaffold.slim +2 -0
  106. data/views/fields/scaffold_picker.slim +20 -0
  107. data/views/fields/select_picker.slim +12 -0
  108. data/views/fields/select_picker_opt.slim +16 -0
  109. data/views/fields/text_area.slim +10 -0
  110. data/views/fields/time.slim +22 -0
  111. data/views/fields/typeahead_picker.slim +25 -0
  112. data/views/index.slim +44 -0
  113. data/views/infra/index.slim +5 -0
  114. data/views/infra/inspect.slim +81 -0
  115. data/views/modals/close_m.slim +15 -0
  116. data/views/modals/confirm_m.slim +19 -0
  117. data/views/modals/empty_m.slim +12 -0
  118. data/views/modals/menu_m.slim +13 -0
  119. data/views/modals/yes_no_m.slim +19 -0
  120. data/views/panels/menu_m.slim +9 -0
  121. data/views/scaffold/confirm.slim +3 -0
  122. data/views/scaffold/fields.slim +10 -0
  123. data/views/scaffold/form.slim +11 -0
  124. data/views/scaffold/list.slim +42 -0
  125. data/views/scaffold/message.slim +3 -0
  126. data/views/scaffold/search.slim +20 -0
  127. data/views/scaffold/view.slim +18 -0
  128. data/views/search_fields/bsmselect_picker.slim +25 -0
  129. data/views/search_fields/bsselect_picker.slim +24 -0
  130. data/views/search_fields/checkbox.slim +11 -0
  131. data/views/search_fields/checkbox2.slim +14 -0
  132. data/views/search_fields/checkbox_buttons.slim +10 -0
  133. data/views/search_fields/date_range.slim +46 -0
  134. data/views/search_fields/decimal_date_range.slim +47 -0
  135. data/views/search_fields/input_text.slim +18 -0
  136. data/views/search_fields/integer.slim +18 -0
  137. data/views/search_fields/integer_range.slim +27 -0
  138. data/views/search_fields/list_bsmselect.slim +24 -0
  139. data/views/search_fields/list_bsselect.slim +22 -0
  140. data/views/search_fields/list_buttons.slim +8 -0
  141. data/views/search_fields/list_select.slim +17 -0
  142. data/views/search_fields/scaffold_picker.slim +19 -0
  143. data/views/search_fields/select_picker.slim +17 -0
  144. data/views/search_fields/typeahead_picker.slim +25 -0
  145. metadata +327 -0
@@ -0,0 +1,134 @@
1
+ # coding: utf-8
2
+
3
+ module Engine2
4
+ class Handler < Sinatra::Base
5
+ reset!
6
+ API ||= "/api"
7
+
8
+ def halt_json code, cause, message
9
+ halt code, {'Content-Type' => 'application/json'}, {message: message, cause: cause}.to_json
10
+ end
11
+
12
+ def halt_forbidden cause = '', message = LOCS[:access_forbidden]
13
+ halt_json 403, cause, message
14
+ end
15
+
16
+ def halt_unauthorized cause = '', message = LOCS[:access_unauthorized]
17
+ halt_json 401, cause, message
18
+ end
19
+
20
+ def halt_not_found cause = '', message = LOCS[:access_not_found]
21
+ halt_json 404, cause, message
22
+ end
23
+
24
+ def halt_method_not_allowed cause = '', message = LOCS[:access_method_not_allowed]
25
+ halt_json 405, cause, message
26
+ end
27
+
28
+ def halt_server_error cause, message
29
+ halt_json 500, cause, message
30
+ end
31
+
32
+ def permit access
33
+ halt_forbidden 'Permission denied' unless access
34
+ end
35
+
36
+ def initial?
37
+ params[:initial]
38
+ end
39
+
40
+ def logged_in?
41
+ !user.nil?
42
+ end
43
+
44
+ def user
45
+ session[:user]
46
+ end
47
+
48
+ def no_cache
49
+ # agent = request.user_agent
50
+ # if agent && (agent["MSIE"] || agent["Trident"])
51
+ # headers["Pragma"] = "no-cache"
52
+ # headers["Expires"] = "0"
53
+ # headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
54
+ # end
55
+ end
56
+
57
+ def post_to_json
58
+ JSON.parse(request.body.read, symbolize_names: true) # rescue halt_server_error
59
+ end
60
+
61
+ def param_to_json name
62
+ permit param = params[name]
63
+ JSON.parse(param, symbolize_names: true) # rescue halt_server_error
64
+ end
65
+
66
+ def serve_api_resource verb, path
67
+ path = path.split('/') # -1 ?
68
+ is_meta = path.pop if path.last == 'meta'
69
+ action = ROOT
70
+ path.each do |pat|
71
+ action = action[pat.to_sym]
72
+ halt_not_found unless action
73
+ halt_unauthorized unless action.check_access!(self)
74
+ end
75
+
76
+ meta = action.*
77
+ response = if is_meta
78
+ params[:access] ? action.access_info(self) : {meta: meta.get, actions: action.actions_info(self)}
79
+ else
80
+ if meta.http_method == verb && meta.invokable
81
+ begin
82
+ meta.invoke!(self)
83
+ rescue => error
84
+ attachment nil, nil
85
+ # content_type :json
86
+ serve_api_error(error)
87
+ end
88
+ else
89
+ halt_method_not_allowed
90
+ end
91
+ end
92
+
93
+ if response.is_a?(Hash)
94
+ content_type :json
95
+ response.to_json
96
+ else
97
+ response
98
+ end
99
+ end
100
+
101
+ [:get, :post, :delete].each do |verb|
102
+ send(verb, "#{API}/*"){|path| serve_api_resource(verb, path)}
103
+ end
104
+
105
+ def serve_api_error error
106
+ halt_server_error Rack::Utils.escape_html(error.inspect) + "<hr>" + error.backtrace.take(30).map{|b| Rack::Utils.escape_html(b)}.join("<br>"), LOCS[:error]
107
+ end
108
+
109
+ get "/js/*.js" do |c|
110
+ coffee c.to_sym
111
+ end
112
+
113
+ get '/*' do |name|
114
+ headers 'Cache-Control' => 'no-cache, no-store, must-revalidate', 'Pragma' => 'no-cache', 'Expires' => '0'
115
+ if name.empty?
116
+ load 'engine2.rb' if settings.environment == :development
117
+ name = 'index'
118
+ end
119
+ slim name.to_sym
120
+ end
121
+
122
+ set :slim, pretty: true, sort_attrs: false
123
+ set :views, ["views", "#{PATH}/views"]
124
+ set :public_folder, "#{PATH}/public"
125
+ set :sessions, expire_after: 3600 # , :httponly => true, :secure => production?
126
+
127
+ helpers do
128
+ def find_template(views, name, engine, &block)
129
+ views.each{|v| super(v, name, engine, &block)}
130
+ end
131
+ end
132
+
133
+ end
134
+ end
@@ -0,0 +1,969 @@
1
+ # coding: utf-8
2
+ module Engine2
3
+ class Meta
4
+ attr_reader :action, :assets, :invokable, :static
5
+
6
+ class << self
7
+ def meta_type mt = nil
8
+ mt ? @meta_type = mt : @meta_type
9
+ end
10
+
11
+ def http_method hm = nil
12
+ hm ? @http_method = hm : @http_method
13
+ end
14
+
15
+ def inherited cls
16
+ cls.http_method http_method
17
+ end
18
+ end
19
+
20
+ http_method :get
21
+
22
+ def initialize action, assets, static = self
23
+ @meta = {}
24
+ @action = action
25
+ @assets = assets
26
+ @static = static
27
+ end
28
+
29
+ # def self.method_added name
30
+ # puts "ADDED #{name}"
31
+ # end
32
+
33
+ def http_method
34
+ @http_method # || (raise E2Error.new("No http method for meta #{self.class}"))
35
+ end
36
+
37
+ def meta_type
38
+ @meta_type || (raise E2Error.new("No meta_type for meta #{self.class}"))
39
+ end
40
+
41
+ def check_static_meta
42
+ raise E2Error.new("Static meta required") if dynamic?
43
+ end
44
+
45
+ def invoke! handler
46
+ if rmp = @request_meta_proc
47
+ meta = self.class.new(action, assets, self)
48
+ meta.instance_exec(handler, *meta.request_meta_proc_params(handler), &rmp)
49
+ meta.post_process
50
+
51
+ {response: meta.invoke(handler), meta: meta.get}
52
+ else
53
+ response = invoke(handler)
54
+ if response.is_a?(Hash)
55
+ {response: response}
56
+ else
57
+ response
58
+ end
59
+ end
60
+ end
61
+
62
+ def response args
63
+ (@meta[:response] ||= {}).merge!(args)
64
+ end
65
+
66
+ def get
67
+ @meta
68
+ end
69
+
70
+ def dynamic?
71
+ self != @static
72
+ end
73
+
74
+ # def [] *keys
75
+ # @meta.path(*keys)
76
+ # end
77
+
78
+ # def []= *keys, value
79
+ # @meta.path!(*keys, value)
80
+ # end
81
+
82
+ def lookup *keys
83
+ if dynamic? # we are the request meta
84
+ value = @meta.path(*keys)
85
+ value.nil? ? @static.get.path(*keys) : value
86
+ # value || @static.value.path(keys)
87
+ else
88
+ @meta.path(*keys)
89
+ end
90
+ end
91
+
92
+ def merge *keys
93
+ if keys.length == 1
94
+ key = keys.first
95
+ dynamic? ? @static.get[key].merge(@meta[key] || {}) : @meta[key]
96
+ else
97
+ dynamic? ? @static.get.path(*keys).merge(@meta.path(*keys)) : @meta.path(*keys)
98
+ end
99
+ end
100
+
101
+ def freeze_meta
102
+ hash = @meta
103
+ hash.freeze
104
+ # hash.each_pair{|k, v| freeze(v) if v.is_a? Hash}
105
+ freeze
106
+ end
107
+
108
+ def request_meta_proc_params handler
109
+ []
110
+ end
111
+
112
+ def request &blk
113
+ raise E2Error.new("No block given for request meta") unless blk
114
+ raise E2Error.new("Request meta already supplied") if @request_meta_proc
115
+ raise E2Error.new("No request block in request meta allowed") if dynamic?
116
+ @request_meta_proc = blk
117
+ nil
118
+ end
119
+
120
+ def pre_run
121
+ @meta_type = self.class.meta_type
122
+ @http_method = self.class.http_method
123
+ end
124
+
125
+ def action_defined
126
+ end
127
+
128
+ def post_run
129
+ @invokable = respond_to?(:invoke)
130
+ post_process
131
+ end
132
+
133
+ def post_process
134
+ end
135
+
136
+ def split_keys id
137
+ Sequel::split_keys(id)
138
+ end
139
+ end
140
+
141
+ class DummyMeta < Meta
142
+ meta_type :dummy
143
+
144
+ # def invoke handler
145
+ # {}
146
+ # end
147
+ end
148
+
149
+ module MetaAPISupport
150
+ def info
151
+ @meta[:info] ||= {}
152
+ end
153
+
154
+ def config
155
+ @meta[:config] ||= {}
156
+ end
157
+
158
+ def info! *fields, options
159
+ raise E2Error.new("No fields given to info") if fields.empty?
160
+ fields.each do |field|
161
+ if options
162
+ (info[field] ||= {}).merge! options # rmerge ?
163
+ else
164
+ info[field] = false
165
+ end
166
+ end
167
+ end
168
+
169
+ def decorate list
170
+ list.each do |f|
171
+ m = (info[f] ||= {})
172
+ m[:loc] ||= LOCS[f]
173
+ end
174
+ end
175
+
176
+ def render field, options
177
+ info! field, render: options
178
+ end
179
+
180
+ def hide_fields *flds
181
+ info! *flds, hidden: true
182
+ end
183
+
184
+ def show_fields *flds
185
+ info! *flds, hidden: false
186
+ end
187
+
188
+ def field_filter *flds, filter
189
+ info! *flds, filter: filter
190
+ end
191
+ end
192
+
193
+ module MetaMenuSupport
194
+ def menu menu_name, &blk
195
+ @menus ||= {}
196
+ @menus[menu_name] ||= ActionMenuBuilder.new(:root)
197
+ @menus[menu_name].instance_eval(&blk) if blk
198
+ @menus[menu_name]
199
+ end
200
+
201
+ def post_process
202
+ super
203
+ if @menus && !@menus.empty?
204
+ @meta[:menus] = {}
205
+ @menus.each_pair do |name, menu|
206
+ @meta[:menus][name] = {entries: menu.to_a, properties: menu.properties}
207
+ end
208
+ end
209
+ end
210
+ end
211
+
212
+ module MetaModelSupport
213
+ def pre_run
214
+ if !(mdl = @assets[:model])
215
+ act = action
216
+ begin
217
+ act = act.parent
218
+ raise E2Error.new("Model not found in tree for action: #{action.name}") unless act
219
+ mdl = act.*.assets[:model]
220
+ end until mdl
221
+
222
+ if asc = @assets[:assoc]
223
+ @assets[:model] = Object.const_get(asc[:class_name])
224
+ # raise E2Error.new("Association '#{asc}' for model '#{asc[:class_name]}' not found") unless @assets[:model]
225
+ else
226
+ @assets[:model] = mdl
227
+ asc = act.*.assets[:assoc]
228
+ @assets[:assoc] = asc if asc
229
+ end
230
+ end
231
+
232
+ # @meta[:model!] = assets[:model]
233
+ # @meta[:assoc!] = assets[:assoc] ? assets[:assoc][:name] : nil
234
+ # @meta[:meta_class!] = self.class
235
+ super
236
+ end
237
+
238
+ def hide_pk
239
+ hide_fields *assets[:model].primary_keys
240
+ end
241
+
242
+ def show_pk
243
+ show_fields *assets[:model].primary_keys
244
+ end
245
+
246
+ def get_type_info name
247
+ model = assets[:model]
248
+ info = model.type_info[name]
249
+ unless info
250
+ if name =~ /^(\w+)__(\w+?)$/ # (?:___\w+)?
251
+ assoc = model.many_to_one_associations[$1.to_sym] || model.one_to_one_associations[$1.to_sym]
252
+ raise E2Error.new("Association #{$1} not found for model #{model}") unless assoc
253
+ m = Object.const_get(assoc[:class_name])
254
+ info = m.type_info.fetch($2.to_sym)
255
+ else
256
+ raise E2Error.new("Type info not found for '#{name}' in model '#{model}'")
257
+ end
258
+ end
259
+ info
260
+ end
261
+
262
+ # def parent_model_name
263
+ # model = @assets[:model]
264
+ # prnt = action.parent
265
+
266
+ # while prnt && prnt.*.assets[:model] == model
267
+ # prnt = prnt.parent
268
+ # end
269
+ # m = prnt.*.assets[:model]
270
+ # m ? m.name : nil
271
+ # end
272
+
273
+ def action_defined
274
+ super
275
+ # p_model_name = parent_model_name
276
+ model = @assets[:model]
277
+
278
+ mt = meta_type
279
+ case mt
280
+ when :list, :star_to_many_list, :star_to_many_link_list, :star_to_many_field, :star_to_many_field_link_list # :many_to_one_list
281
+ model.many_to_one_associations.each do |assoc_name, assoc|
282
+ unless assoc[:propagate] == false # || p_model_name == assoc[:class_name]
283
+ dc = model.type_info[assoc[:keys].first][:decode]
284
+ action.run_scheme :decode, model, assoc_name, dc[:search]
285
+ end
286
+ end
287
+ end
288
+
289
+ case mt
290
+ when :modify, :create
291
+ model.many_to_one_associations.each do |assoc_name, assoc|
292
+ unless assoc[:propagate] == false # || p_model_name == assoc[:class_name]
293
+ dc = model.type_info[assoc[:keys].first][:decode]
294
+ action.run_scheme :decode, model, assoc_name, dc[:form]
295
+ end
296
+ end
297
+ end
298
+
299
+ case mt
300
+ when :list #, :star_to_many_list, :many_to_one_list # list dropdowns
301
+ divider = false
302
+ model.one_to_many_associations.merge(model.many_to_many_associations).each do |assoc_name, assoc|
303
+ unless assoc[:propagate] == false
304
+ menu(:item_menu).divider unless divider
305
+ divider ||= true
306
+ menu(:item_menu).option :"#{assoc_name}!", icon: "list" # , click: "action.show_assoc($index, \"#{assoc_name}!\")"
307
+ action.run_scheme :star_to_many, :"#{assoc_name}!", assoc
308
+ end
309
+ end
310
+ end
311
+
312
+ case mt
313
+ when :modify, :create
314
+ model.type_info.each do |field, info|
315
+ case info[:type]
316
+ when :blob_store
317
+ action.run_scheme :blob_store, model, field
318
+ when :foreign_blob_store
319
+ action.run_scheme :foreign_blob_store, model, field
320
+ when :file_store
321
+ action.run_scheme :file_store, model, field
322
+ when :star_to_many_field
323
+ assoc = model.association_reflections[info[:assoc_name]] # info[:name] ?
324
+ raise E2Error.new("Association '#{info[:assoc_name]}' not found for model '#{model}'") unless assoc
325
+ action.run_scheme :star_to_many_field, assoc, field
326
+ end
327
+ end
328
+ end
329
+ end
330
+
331
+ def unsupported_association assoc
332
+ raise E2Error.new("Unsupported association: #{assoc}")
333
+ end
334
+ end
335
+
336
+ module MetaQuerySupport
337
+ def query q, &blk
338
+ @query = q.naked
339
+ @query.row_proc = blk if blk
340
+ end
341
+
342
+ def post_run
343
+ query select(*assets[:model].columns) unless @query
344
+ super
345
+ end
346
+
347
+ def get_query # move to query ?
348
+ if dynamic?
349
+ @query || @static.get_query
350
+ else
351
+ @query
352
+ end
353
+ end
354
+
355
+ def select *args, &blk
356
+ assets[:model].select(*args, &blk).ensure_primary_key.setup! (@meta[:fields] = [])
357
+ end
358
+ end
359
+
360
+ module MetaTabSupport
361
+ def select_tabs tabs, *args, &blk
362
+ field_tabs tabs
363
+ select *tabs.map{|name, fields|fields}.flatten, *args, &blk
364
+ end
365
+
366
+ def field_tabs hash
367
+ @meta[:tabs] = hash.map{|k, v| {name: k, loc: LOCS[k], fields: v} }
368
+ end
369
+
370
+ def lazy_tab tab_name
371
+ tabs = @meta[:tabs]
372
+ raise E2Error.new("No tabs defined") unless tabs
373
+ tab = tabs.find{|t| t[:name] == tab_name}
374
+ raise E2Error.new("No tab #{tab_name} defined") unless tab
375
+ tab[:lazy] = true
376
+ end
377
+ end
378
+
379
+ module MetaAngularSupport
380
+ def ng_execute expr
381
+ (@meta[:execute] ||= "") << expr + ";"
382
+ end
383
+
384
+ def ng_record! name, value
385
+ value = case value
386
+ when String
387
+ "'#{value}'"
388
+ when nil
389
+ 'null'
390
+ else
391
+ value
392
+ end
393
+
394
+ "action.record['#{name}'] = #{value}"
395
+ end
396
+
397
+ def ng_record name
398
+ "action.record['#{name}']"
399
+ end
400
+
401
+ def ng_info! name, *selector, expression
402
+ # expression = "'#{expression}'" if expression.is_a? String
403
+ "action.meta.info['#{name}'].#{selector.join('.')} = #{expression}"
404
+ end
405
+
406
+ def ng_call name, *args
407
+ # TODO
408
+ end
409
+ end
410
+
411
+ module MetaPanelSupport
412
+ def pre_run
413
+ modal_action true
414
+ super
415
+ end
416
+
417
+ def post_run
418
+ super
419
+ if @meta[:panel]
420
+ panel_panel_template 'menu_m' unless panel[:panel_template] == false
421
+ # modal_action false if panel[:panel_template] == false
422
+ panel_class '' unless panel[:class]
423
+ end
424
+ end
425
+
426
+ def glyphicon name
427
+ "<span class='glyphicon glyphicon-#{name}'></span>"
428
+ end
429
+
430
+ def panel
431
+ @meta[:panel] ||= {}
432
+ end
433
+
434
+ def modal_action modal = true
435
+ panel[:modal_action] = modal
436
+ end
437
+
438
+ def panel_template tmpl
439
+ panel[:template] = tmpl
440
+ end
441
+
442
+ def panel_panel_template tmpl
443
+ panel[:panel_template] = tmpl
444
+ end
445
+
446
+ def panel_class cls
447
+ panel[:class] = cls
448
+ end
449
+
450
+ def panel_title tle
451
+ panel[:title] = tle
452
+ end
453
+ end
454
+
455
+ class MenuMeta < Meta
456
+ include MetaMenuSupport
457
+ meta_type :menu
458
+
459
+ def invoke handler
460
+ {}
461
+ end
462
+ end
463
+
464
+ class ConfirmMeta < Meta
465
+ include MetaPanelSupport, MetaMenuSupport
466
+ meta_type :confirm
467
+
468
+ def pre_run
469
+ super
470
+ panel_template 'scaffold/message'
471
+ panel_title LOCS[:confirmation]
472
+ panel_class 'modal-default'
473
+
474
+ menu :panel_menu do
475
+ option :approve, icon: "ok", loc: LOCS[:ok], disabled: 'action.action_pending'
476
+ option :cancel, icon: "remove"
477
+ end
478
+ end
479
+
480
+ def invoke handler
481
+ params = handler.request.params
482
+ # params.merge({arguments: params.keys})
483
+ end
484
+ end
485
+
486
+ module MetaOnChangeSupport
487
+ def on_change field, &blk
488
+ action_name = :"#{field}_on_change"
489
+ act = action.define_action action_name, (blk.arity > 2 ? OnChangeGetMeta : OnChangePostMeta)
490
+ act.*{request &blk}
491
+
492
+ info! field, remote_onchange: action_name
493
+ info! field, remote_onchange_record: :true if blk.arity > 2
494
+ end
495
+
496
+ class OnChangeMeta < Meta
497
+ include MetaAPISupport, MetaAngularSupport
498
+
499
+ def request_meta_proc_params handler
500
+ if handler.request.post?
501
+ json = handler.post_to_json
502
+ [json[:value], json[:record]]
503
+ else
504
+ params = handler.request.params
505
+ [params["value"], params["record"]]
506
+ end
507
+ end
508
+
509
+ def invoke handler
510
+ {}
511
+ end
512
+ end
513
+
514
+ class OnChangeGetMeta < OnChangeMeta
515
+ meta_type :on_change
516
+
517
+ def request_meta_proc_params handler
518
+ params = handler.request.params
519
+ [params["value"], params["record"]]
520
+ end
521
+ end
522
+
523
+ class OnChangePostMeta < OnChangeMeta
524
+ http_method :post
525
+ meta_type :on_change
526
+
527
+ def request_meta_proc_params handler
528
+ json = handler.post_to_json
529
+ [json[:value], json[:record]]
530
+ end
531
+ end
532
+ end
533
+
534
+ module MetaListSupport
535
+ include MetaModelSupport, MetaAPISupport, MetaTabSupport, MetaPanelSupport, MetaMenuSupport, MetaOnChangeSupport
536
+ attr_reader :filters, :orders
537
+
538
+ def pre_run
539
+ super
540
+ config.merge!(per_page: 10, use_count: false, show_item_menu: true, selectable: true) # search_active: false,
541
+
542
+ # modal_action self.class != ListMeta
543
+ panel_template 'scaffold/list'
544
+ panel_panel_template 'panels/menu_m' unless action.parent.*.assets[:model]
545
+ search_template 'scaffold/search'
546
+ panel_title "#{glyphicon('list')} #{LOCS[assets[:model].name.to_sym]}"
547
+ menu(:panel_menu).option :cancel, icon: "remove"
548
+ menu :menu do
549
+ properties break: 2, group_class: "btn-group-xs"
550
+ option :search_toggle, icon: "search", show: "action.meta.search_fields", class: "action.ui_state.search_active && 'active'", button_loc: false
551
+
552
+ # divider
553
+ option :refresh, icon: "refresh", button_loc: false
554
+ option :default_order, icon: "signal", button_loc: false
555
+ option :select_toggle, icon: "check", enabled: "action.meta.config.selectable", button_loc: false
556
+ divider
557
+ option :debug_info, icon: "list-alt" do
558
+ option :show_meta, icon: "eye-open"
559
+ end
560
+ end
561
+
562
+ menu :item_menu do
563
+ properties break: 1, group_class: "btn-group-xs"
564
+ end
565
+
566
+ @meta[:state] = [:query, :ui_state]
567
+ end
568
+
569
+ def post_run
570
+ unless panel[:class]
571
+ panel_class case @meta[:fields].size
572
+ when 1..3; ''
573
+ when 4..6; 'modal-large'
574
+ else; 'modal-huge'
575
+ end
576
+ end
577
+
578
+ super
579
+ @meta[:primary_fields] = assets[:model].primary_keys
580
+ end
581
+
582
+ # def find_renderer type_info
583
+ # renderer = DefaultSearchRenderers[type_info[:type]] || DefaultSearchRenderers[type_info[:otype]]
584
+ # raise E2Error.new("No search renderer found for field '#{type_info[:name]}'") unless renderer
585
+ # renderer.(self, type_info)
586
+ # end
587
+
588
+ def post_process
589
+ if fields = @meta[:search_fields]
590
+ fields = fields - static.get[:search_fields] if dynamic?
591
+
592
+ decorate(fields)
593
+ fields.each do |name|
594
+ type_info = get_type_info(name)
595
+
596
+ # render = info[name][:render]
597
+ # if not render
598
+ # info[name][:render] = find_renderer(type_info)
599
+ # else
600
+ # info[name][:render].merge!(find_renderer(type_info)){|key, v1, v2|v1}
601
+ # end
602
+
603
+ info[name][:render] ||= begin # set before :fields
604
+ renderer = DefaultSearchRenderers[type_info[:type]] || DefaultSearchRenderers[type_info[:otype]]
605
+ raise E2Error.new("No search renderer found for field '#{type_info[:name]}'") unless renderer
606
+ renderer.(self, type_info)
607
+ end
608
+
609
+ proc = SearchRendererPostProcessors[type_info[:type]] || ListRendererPostProcessors[type_info[:type]] # ?
610
+ proc.(self, name, type_info) if proc
611
+ end
612
+ end
613
+
614
+ if fields = @meta[:fields]
615
+ fields = fields - static.get[:fields] if dynamic?
616
+
617
+ decorate(fields)
618
+ fields.each do |name|
619
+ type_info = get_type_info(name)
620
+ proc = ListRendererPostProcessors[type_info[:type]]
621
+ proc.(self, name, type_info) if proc
622
+ end
623
+ end
624
+
625
+ super
626
+ end
627
+
628
+ def search_template template
629
+ panel[:search_template] = template
630
+ end
631
+
632
+ def sortable *flds
633
+ flds = @meta[:fields] if flds.empty?
634
+ info! *flds, sort: true
635
+ end
636
+
637
+ def search_live *flds
638
+ flds = @meta[:search_fields] if flds.empty?
639
+ info! *flds, search_live: true
640
+ end
641
+
642
+ def searchable *flds
643
+ @meta.delete(:tabs)
644
+ @meta[:search_fields] = *flds
645
+ end
646
+
647
+ def searchable_tabs tabs
648
+ searchable *tabs.map{|name, fields|fields}.flatten
649
+ field_tabs tabs
650
+ end
651
+
652
+ def template
653
+ SearchTemplates
654
+ end
655
+
656
+ def filter name, &blk
657
+ (@filters ||= {})[name] = blk
658
+ end
659
+
660
+ def order name, &blk
661
+ (@orders ||= {})[name] = blk
662
+ end
663
+ end
664
+
665
+ module MetaApproveSupport
666
+ include MetaModelSupport
667
+ attr_reader :validations
668
+
669
+ def validate_fields *fields
670
+ if fields.empty?
671
+ @validate_fields
672
+ else
673
+ @validate_fields = assets[:model].type_info.keys & (fields + assets[:model].primary_keys).uniq
674
+ end
675
+ end
676
+
677
+ def before_approve handler, record
678
+ end
679
+
680
+ def after_approve handler, record
681
+ end
682
+
683
+ def validate_and_approve handler, record, json
684
+ static.before_approve(handler, record)
685
+ record.valid?
686
+ validate_record(handler, record)
687
+ if record.errors.empty?
688
+ static.after_approve(handler, record)
689
+ true
690
+ else
691
+ false
692
+ end
693
+ end
694
+
695
+ def allocate_record handler, json
696
+ model = assets[:model]
697
+ json_rec = json[:record]
698
+ handler.permit json_rec.is_a?(Hash)
699
+ val_fields = (dynamic? ? static.validate_fields : @validate_fields) || model.type_info.keys
700
+ handler.permit (json_rec.keys - val_fields).empty?
701
+
702
+ record = model.call(json_rec)
703
+ record.validate_fields = val_fields
704
+ record
705
+ end
706
+
707
+ def record handler, record
708
+ {errors: nil}
709
+ end
710
+
711
+ def invoke handler
712
+ json = handler.post_to_json
713
+ record = allocate_record(handler, json)
714
+ validate_and_approve(handler, record, json) ? static.record(handler, record) : {record: record.to_hash, errors: record.errors}
715
+ end
716
+
717
+ def validate name, &blk
718
+ (@validations ||= {})[name] = blk
719
+ end
720
+
721
+ def validate_record handler, record
722
+ @validations.each do |name, val|
723
+ unless record.errors[name]
724
+ result = val.(record, handler)
725
+ record.errors.add(name, result) if result
726
+ end
727
+ end if @validations
728
+ end
729
+
730
+ def post_run
731
+ super
732
+ validate_fields *action.parent.*.get[:fields] unless validate_fields
733
+ end
734
+ end
735
+
736
+ module MetaViewSupport
737
+ include MetaModelSupport, MetaAPISupport, MetaTabSupport, MetaPanelSupport, MetaMenuSupport
738
+
739
+ def pre_run
740
+ super
741
+ panel_template 'scaffold/view'
742
+ panel_title LOCS[:view_title]
743
+
744
+ menu(:panel_menu).option :cancel, icon: "remove"
745
+ action.parent.*.menu(:item_menu).option action.name, icon: "file", button_loc: false
746
+ end
747
+
748
+ def post_process
749
+ if fields = @meta[:fields]
750
+ fields = fields - static.get[:fields] if dynamic?
751
+
752
+ decorate(fields)
753
+ fields.each do |name|
754
+ type_info = get_type_info(name)
755
+ proc = ListRendererPostProcessors[type_info[:type]]
756
+ proc.(self, name, type_info) if proc
757
+ end
758
+ end
759
+
760
+ super
761
+ end
762
+ end
763
+
764
+ (FormRendererPostProcessors ||= {}).merge!(
765
+ boolean: lambda{|meta, field, info|
766
+ meta.info[field][:render].merge! true_value: info[:true_value], false_value: info[:false_value]
767
+ meta.info[field][:dont_strip] = info[:dont_strip] if info[:dont_strip]
768
+ },
769
+ date: lambda{|meta, field, info|
770
+ meta.info[field][:render].merge! format: info[:format], model_format: info[:model_format]
771
+ if date_to = info[:other_date]
772
+ meta.info[field][:render].merge! other_date: date_to #, format: info[:format], model_format: info[:model_format]
773
+ meta.hide_fields date_to
774
+ elsif time = info[:other_time]
775
+ meta.info[field][:render].merge! other_time: time
776
+ meta.hide_fields time
777
+ end
778
+ },
779
+ time: lambda{|meta, field, info|
780
+ meta.info[field][:render].merge! format: info[:format], model_format: info[:model_format]
781
+ },
782
+ decimal_date: lambda{|meta, field, info|
783
+ FormRendererPostProcessors[:date].(meta, field, info)
784
+ meta.info! field, type: :decimal_date
785
+ },
786
+ decimal_time: lambda{|meta, field, info|
787
+ FormRendererPostProcessors[:time].(meta, field, info)
788
+ meta.info! field, type: :decimal_time
789
+ },
790
+ datetime: lambda{|meta, field, info|
791
+ meta.info[field][:render].merge! date_format: info[:date_format], time_format: info[:time_format], date_model_format: info[:date_model_format], time_model_format: info[:time_model_format]
792
+ },
793
+ # date_range: lambda{|meta, field, info|
794
+ # meta.info[field][:render].merge! other_date: info[:other_date], format: info[:format], model_format: info[:model_format]
795
+ # meta.hide_fields info[:other_date]
796
+ # meta.info[field][:decimal_date] = true if info[:validations][:decimal_date]
797
+ # },
798
+ list_select: lambda{|meta, field, info|
799
+ meta.info[field][:render].merge! list: info[:list]
800
+ },
801
+ many_to_one: lambda{|meta, field, info|
802
+ field_info = meta.info[field]
803
+ field_info[:assoc] = :"#{info[:assoc_name]}!"
804
+ field_info[:fields] = info[:keys]
805
+ field_info[:type] = info[:otype]
806
+ # field_info[:table_loc] = LOCS[info[:assoc_name]]
807
+
808
+ (info[:keys] - [field]).each do |of|
809
+ f_info = meta.info.fetch(of)
810
+ f_info[:hidden] = true
811
+ f_info[:type] = meta.assets[:model].type_info[of].fetch(:otype)
812
+ end
813
+ },
814
+ file_store: lambda{|meta, field, info|
815
+ meta.info[field][:render].merge! multiple: info[:multiple]
816
+ # meta[:model] = meta.action.model.table_name
817
+ },
818
+ star_to_many_field: lambda{|meta, field, info|
819
+ field_info = meta.info[field]
820
+ field_info[:assoc] = :"#{info[:assoc_name]}!"
821
+ # meta.info[field][:render].merge! multiple: info[:multiple]
822
+ # field_info = meta.info[field]
823
+ # field_info[:resource] ||= "#{Handler::API}#{meta.model.namespace}/#{info[:assoc_name]}"
824
+ }
825
+ )
826
+
827
+ (ListRendererPostProcessors ||= {}).merge!(
828
+ boolean: lambda{|meta, field, info|
829
+ meta.info! field, type: :boolean # move to meta ?
830
+ meta.info[field][:render] ||= {}
831
+ meta.info[field][:render].merge! true_value: info[:true_value], false_value: info[:false_value]
832
+ },
833
+ list_select: lambda{|meta, field, info|
834
+ meta.info! field, type: :list_select
835
+ meta.info[field][:render] ||= {}
836
+ meta.info[field][:render].merge! list: info[:list]
837
+ },
838
+ datetime: lambda{|meta, field, info|
839
+ meta.info! field, type: :datetime
840
+ },
841
+ decimal_date: lambda{|meta, field, info|
842
+ meta.info! field, type: :decimal_date
843
+ },
844
+ decimal_time: lambda{|meta, field, info|
845
+ meta.info! field, type: :decimal_time
846
+ },
847
+ # date_range: lambda{|meta, field, info|
848
+ # meta.info[field][:type] = :decimal_date if info[:validations][:decimal_date] # ? :decimal_date : :date
849
+ # }
850
+ )
851
+
852
+ (SearchRendererPostProcessors ||= {}).merge!(
853
+ many_to_one: lambda{|meta, field, info|
854
+ model = meta.assets[:model]
855
+ if model.type_info[field]
856
+ keys = info[:keys]
857
+ else
858
+ meta.check_static_meta
859
+ model = Object.const_get(model.many_to_one_associations[field[/^\w+?(?=__)/].to_sym][:class_name])
860
+ # meta.action.define_action :"#{info[:assoc_name]}!" do # assoc_#{aname}
861
+ # define_action :decode, DecodeEntryMeta, assoc: model.association_reflections[info[:assoc_name]] do
862
+ # run_scheme :default_many_to_one
863
+ # end
864
+ # end
865
+
866
+ # verify associations ?
867
+ # model = Model.models.fetch(field[/^\w+?(?=__)/].to_sym)
868
+ keys = info[:keys].map{|k| :"#{model.table_name}__#{k}"}
869
+ end
870
+
871
+ field_info = meta.info[field]
872
+ field_info[:assoc] = :"#{info[:assoc_name]}!"
873
+ field_info[:fields] = keys
874
+ field_info[:type] = info[:otype]
875
+ # field_info[:table_loc] = LOCS[info[:assoc_name]]
876
+
877
+ (keys - [field]).each do |of|
878
+ f_info = meta.info[of]
879
+ raise E2Error.new("Missing searchable field: '#{of}' in model '#{meta.assets[:model]}'") unless f_info
880
+ f_info[:hidden_search] = true
881
+ f_info[:type] = model.type_info[of].fetch(:otype)
882
+ end
883
+ },
884
+ date: lambda{|meta, field, info|
885
+ meta.info[field][:render] ||= {}
886
+ meta.info[field][:render].merge! format: info[:format], model_format: info[:model_format] # Model::DEFAULT_DATE_FORMAT
887
+ },
888
+ decimal_date: lambda{|meta, field, info|
889
+ SearchRendererPostProcessors[:date].(meta, field, info)
890
+ }
891
+ )
892
+
893
+ (DefaultFormRenderers ||= {}).merge!(
894
+ date: lambda{|meta, info|
895
+ info[:other_date] ? Templates.date_range : (info[:other_time] ? Templates.date_time : Templates.date_picker)
896
+
897
+ },
898
+ time: lambda{|meta, info| Templates.time_picker},
899
+ datetime: lambda{|meta, info| Templates.datetime_picker},
900
+ file_store: lambda{|meta, info| Templates.file_store},
901
+ blob: lambda{|meta, info| Templates.blob}, # !!!
902
+ blob_store: lambda{|meta, info| Templates.blob},
903
+ foreign_blob_store: lambda{|meta, info| Templates.blob},
904
+ string: lambda{|meta, info| Templates.input_text(info[:length])},
905
+ text: lambda{|meta, info| Templates.text},
906
+ integer: lambda{|meta, info| Templates.integer},
907
+ decimal: lambda{|meta, info| Templates.decimal},
908
+ decimal_date: lambda{|meta, info| DefaultFormRenderers[:date].(meta, info)},
909
+ decimal_time: lambda{|meta, info| Templates.time_picker},
910
+ email: lambda{|meta, info| Templates.email(info[:length])},
911
+ password: lambda{|meta, info| Templates.password(info[:length])},
912
+ # date_range: lambda{|meta, info| Templates.date_range},
913
+ boolean: lambda{|meta, info| Templates.checkbox_buttons(optional: !info[:required])},
914
+ currency: lambda{|meta, info| Templates.currency},
915
+ list_select: lambda{|meta, info|
916
+ length = info[:list].length
917
+ if length <= 3
918
+ Templates.list_buttons(optional: !info[:required])
919
+ elsif length <= 15
920
+ max_length = info[:list].max_by{|a|a.last.length}.last.length
921
+ Templates.list_bsselect(max_length, optional: !info[:required])
922
+ else
923
+ max_length = info[:list].max_by{|a|a.last.length}.last.length
924
+ Templates.list_select(max_length, optional: !info[:required])
925
+ end
926
+ },
927
+ star_to_many_field: lambda{|meta, info| Templates.scaffold},
928
+ many_to_one: lambda{|meta, info| # Templates.scaffold_picker
929
+ tmpl_type = info[:decode][:form]
930
+ case
931
+ when tmpl_type[:scaffold]; Templates.scaffold_picker
932
+ when tmpl_type[:list]; Templates.bsselect_picker
933
+ when tmpl_type[:typeahead];Templates.typeahead_picker
934
+ else
935
+ raise E2Error.new("Unknown decode type #{tmpl_type}")
936
+ end
937
+ }, # required/opt
938
+ )
939
+
940
+ (DefaultSearchRenderers ||= {}).merge!(
941
+ date: lambda{|meta, info| SearchTemplates.date_range},
942
+ decimal_date: lambda{|meta, info| SearchTemplates.date_range},
943
+ integer: lambda{|meta, info| SearchTemplates.integer_range},
944
+ string: lambda{|meta, info| SearchTemplates.input_text},
945
+ boolean: lambda{|meta, info| SearchTemplates.checkbox_buttons},
946
+ list_select: lambda{|meta, info|
947
+ length = info[:list].length
948
+ if length <= 3
949
+ SearchTemplates.list_buttons
950
+ elsif length <= 15
951
+ # max_length = info[:list].max_by{|a|a.last.length}.last.length
952
+ SearchTemplates.list_bsselect(multiple: info[:multiple])
953
+ else
954
+ # max_length = info[:list].max_by{|a|a.last.length}.last.length
955
+ SearchTemplates.list_select
956
+ end
957
+ },
958
+ many_to_one: lambda{|meta, info|
959
+ tmpl_type = info[:decode][:search]
960
+ case
961
+ when tmpl_type[:scaffold]; SearchTemplates.scaffold_picker(multiple: tmpl_type[:multiple])
962
+ when tmpl_type[:list]; SearchTemplates.bsselect_picker(multiple: tmpl_type[:multiple])
963
+ when tmpl_type[:typeahead];SearchTemplates.typeahead_picker
964
+ else
965
+ raise E2Error.new("Unknown decode type #{tmpl_type}")
966
+ end
967
+ }
968
+ )
969
+ end