engine2 1.0.0

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