marty 4.0.0.rc2 → 5.1.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 (72) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +7 -0
  3. data/.rubocop.yml +1 -0
  4. data/.rubocop_todo.yml +3 -16
  5. data/.ssh-docker/.keep +0 -0
  6. data/Dockerfile.dummy +3 -0
  7. data/Gemfile +19 -15
  8. data/app/components/marty/base_rule_view.rb +104 -10
  9. data/app/components/marty/base_rule_view/client/base_rule_view.js +24 -0
  10. data/app/components/marty/data_grid_user_view.rb +39 -0
  11. data/app/components/marty/data_grid_view.rb +68 -18
  12. data/app/components/marty/extras/layout.rb +1 -1
  13. data/app/components/marty/grid.rb +1 -1
  14. data/app/components/marty/grid/client/grid.js +29 -13
  15. data/app/components/marty/import_view.rb +3 -3
  16. data/app/components/marty/main_auth_app.rb +11 -1
  17. data/app/components/marty/report_form.rb +6 -6
  18. data/app/components/marty/script_form.rb +5 -5
  19. data/app/components/marty/script_tester.rb +2 -2
  20. data/app/components/marty/user_view.rb +3 -9
  21. data/app/models/marty/base_rule.rb +92 -32
  22. data/app/models/marty/data_grid.rb +92 -22
  23. data/app/models/marty/event.rb +2 -2
  24. data/app/models/marty/promise.rb +4 -4
  25. data/app/models/marty/role_type.rb +14 -1
  26. data/app/services/marty/data_grid_view/save_grid.rb +2 -2
  27. data/app/services/marty/promises/delorean/create.rb +2 -2
  28. data/app/services/marty/promises/ruby/create.rb +2 -2
  29. data/config/locales/en.yml +11 -2
  30. data/db/migrate/108_add_data_grid_perms.rb +16 -0
  31. data/db/migrate/508_add_not_to_data_grids_tables.rb +18 -0
  32. data/db/migrate/509_update_dg_plpgsql_v1_fns.rb +13 -0
  33. data/db/sql/query_grid_dir_v1.sql +16 -2
  34. data/docker-compose.dummy.yml +1 -0
  35. data/lib/marty/content_handler.rb +2 -2
  36. data/lib/marty/data_change.rb +1 -1
  37. data/lib/marty/data_conversion.rb +3 -4
  38. data/lib/marty/data_importer.rb +4 -4
  39. data/lib/marty/mcfly_model.rb +7 -10
  40. data/lib/marty/migrations.rb +1 -1
  41. data/lib/marty/monkey.rb +2 -2
  42. data/lib/marty/promise_job.rb +5 -5
  43. data/lib/marty/promise_proxy.rb +2 -2
  44. data/lib/marty/promise_ruby_job.rb +4 -4
  45. data/lib/marty/version.rb +1 -1
  46. data/make-app.mk +1 -1
  47. data/marty.gemspec +13 -18
  48. data/other/marty/diagnostic/aws/ec2_instance.rb +17 -2
  49. data/other/marty/diagnostic/aws/error.rb +8 -0
  50. data/other/marty/diagnostic/database.rb +2 -2
  51. data/other/marty/diagnostic/delayed_job_version.rb +0 -1
  52. data/spec/dummy/app/components/gemini/my_rule_view.rb +32 -6
  53. data/spec/dummy/app/models/gemini/fannie_bup.rb +13 -20
  54. data/spec/dummy/app/models/gemini/my_rule.rb +4 -0
  55. data/spec/dummy/app/models/gemini/xyz_rule.rb +3 -1
  56. data/spec/dummy/config/application.rb +1 -0
  57. data/spec/dummy/config/initializers/secret_token.rb +1 -1
  58. data/spec/dummy/db/migrate/20190702115241_add_simple_guards_options_to_rules.rb +37 -0
  59. data/spec/features/data_grid_spec.rb +109 -47
  60. data/spec/features/reporting_spec.rb +4 -4
  61. data/spec/features/rule_spec.rb +62 -31
  62. data/spec/features/scripting_spec.rb +3 -3
  63. data/spec/features/user_view_spec.rb +17 -8
  64. data/spec/fixtures/csv/rule/MyRule.csv +4 -1
  65. data/spec/lib/data_importer_spec.rb +8 -8
  66. data/spec/lib/mcfly_model_spec.rb +6 -6
  67. data/spec/models/data_grid_spec.rb +139 -7
  68. data/spec/models/rule_spec.rb +116 -9
  69. data/spec/spec_helper.rb +2 -2
  70. data/spec/support/netzke.rb +4 -3
  71. metadata +55 -54
  72. data/Gemfile.lock +0 -289
@@ -115,7 +115,7 @@ module Layout
115
115
  end
116
116
 
117
117
  def get_sorter(col)
118
- lambda { |rel, dir| rel.order("#{col}::text #{dir.to_s}") }
118
+ lambda { |rel, dir| rel.order(Arel.sql("#{col}::text #{dir}")) }
119
119
  end
120
120
 
121
121
  ######################################################################
@@ -52,7 +52,7 @@ class Marty::Grid < ::Netzke::Grid::Base
52
52
 
53
53
  def get_json_sorter(json_col, field)
54
54
  lambda do |r, dir|
55
- r.order("#{json_col} ->> '#{field}' " + dir.to_s)
55
+ r.order(Arel.sql("#{json_col} ->> '#{field}' " + dir.to_s))
56
56
  end
57
57
  end
58
58
 
@@ -1,13 +1,13 @@
1
1
  {
2
- getComponent: function (name) {
2
+ getComponent: function(name) {
3
3
  return Ext.getCmp(name);
4
4
  },
5
5
 
6
- findComponent: function (name) {
6
+ findComponent: function(name) {
7
7
  return Ext.ComponentQuery.query(`[name=${name}]`)[0];
8
8
  },
9
9
 
10
- setDisableComponentActions: function (prefix, flag) {
10
+ setDisableComponentActions: function(prefix, flag) {
11
11
  for (var key in this.actions) {
12
12
  if (key.substring(0, prefix.length) == prefix) {
13
13
  this.actions[key].setDisabled(flag);
@@ -15,7 +15,7 @@
15
15
  }
16
16
  },
17
17
 
18
- initComponent: function () {
18
+ initComponent: function() {
19
19
  this.dockedItems = this.dockedItems || [];
20
20
  if (this.paging == 'pagination') {
21
21
  this.dockedItems.push({
@@ -57,7 +57,7 @@
57
57
 
58
58
  var children = me.serverConfig.child_components || [];
59
59
  me.onSelectionChange(
60
- function (m) {
60
+ function(m) {
61
61
  var has_sel = m.hasSelection();
62
62
 
63
63
  var rid = null;
@@ -92,7 +92,7 @@
92
92
  var store = me.getStore();
93
93
  var linked = me.serverConfig.linked_components || [];
94
94
  for (var event of ['update', 'netzkerefresh']) {
95
- store.on(event, function () {
95
+ store.on(event, function() {
96
96
  for (var link of linked) {
97
97
  var comp = me.findComponent(link);
98
98
  if (comp && comp.reload) {
@@ -103,19 +103,30 @@
103
103
  }
104
104
  },
105
105
 
106
- onSelectionChange: function (f) {
106
+ onSelectionChange: function(f) {
107
107
  var me = this;
108
108
  me.getSelectionModel().on('selectionchange', f);
109
109
  },
110
110
 
111
- doViewInForm: function (record) {
111
+ // override netzkeReloadStore to allow option passthrough
112
+ // reference: http://api.netzke.org/client/files/doc_client_netzke-basepack_javascripts_grid_event_handlers.js.html
113
+ netzkeReloadStore: function(opts = {}) {
114
+ var store = this.getStore();
115
+
116
+ // HACK to work around buffered store's buggy reload()
117
+ if (!store.lastRequestStart) {
118
+ store.load(opts);
119
+ } else store.reload(opts);
120
+ },
121
+
122
+ doViewInForm: function(record) {
112
123
  this.netzkeLoadComponent("view_window", {
113
124
  serverConfig: {
114
125
  record_id: record.id
115
126
  },
116
- callback: function (w) {
127
+ callback: function(w) {
117
128
  w.show();
118
- w.on('close', function () {
129
+ w.on('close', function() {
119
130
  if (w.closeRes === "ok") {
120
131
  this.netzkeReloadStore();
121
132
  }
@@ -124,11 +135,16 @@
124
135
  });
125
136
  },
126
137
 
127
- reload: function (opts = {}) {
138
+ // always reset store to first page on reload
139
+ // to avoid load bug when moving from a higher page count
140
+ // to a grid with a lower page count
141
+ reload: function(opts = {
142
+ start: 0
143
+ }) {
128
144
  this.netzkeReloadStore(opts);
129
145
  },
130
146
 
131
- reloadAll: function () {
147
+ reloadAll: function() {
132
148
  var me = this;
133
149
  var children = me.serverConfig.child_components || [];
134
150
  this.store.reload();
@@ -140,7 +156,7 @@
140
156
  }
141
157
  },
142
158
 
143
- clearFilters: function () {
159
+ clearFilters: function() {
144
160
  this.filters.clearFilters();
145
161
  },
146
162
  }
@@ -84,10 +84,10 @@ class Marty::ImportView < Marty::Form
84
84
  result << messages if messages
85
85
 
86
86
  client.set_result result.join('<br/>')
87
- rescue Marty::DataImporter::Error => exc
87
+ rescue Marty::DataImporter::Error => e
88
88
  result = [
89
- "Import failed on line(s): #{exc.lines.join(', ')}",
90
- "Error: #{exc.to_s}",
89
+ "Import failed on line(s): #{e.lines.join(', ')}",
90
+ "Error: #{e.to_s}",
91
91
  ]
92
92
 
93
93
  client.set_result '<font color="red">' + result.join('<br/>') + '</font>'
@@ -4,6 +4,7 @@ require 'marty/api_log_view'
4
4
  require 'marty/config_view'
5
5
  require 'marty/data_grid_view'
6
6
  require 'marty/schedule_jobs_dashboard'
7
+ require 'marty/data_grid_user_view'
7
8
  require 'marty/event_view'
8
9
  require 'marty/import_type_view'
9
10
  require 'marty/new_posting_window'
@@ -96,6 +97,7 @@ class Marty::MainAuthApp < Marty::AuthApp
96
97
  icon_cls: 'fa fa-window-restore glyph',
97
98
  menu: [
98
99
  :data_grid_view,
100
+ :data_grid_user_view,
99
101
  :reporting,
100
102
  :scripting,
101
103
  :promise_view,
@@ -237,7 +239,14 @@ class Marty::MainAuthApp < Marty::AuthApp
237
239
  end
238
240
 
239
241
  action :data_grid_view do |a|
240
- a.text = I18n.t('data_grid_view', default: 'Data Grids')
242
+ a.text = I18n.t('data_grid_view')
243
+ a.handler = :netzke_load_component_by_action
244
+ a.icon_cls = 'fa fa-table glyph'
245
+ a.disabled = !self.class.has_any_perm?
246
+ end
247
+
248
+ action :data_grid_user_view do |a|
249
+ a.text = I18n.t('data_grid_user_view')
241
250
  a.handler = :netzke_load_component_by_action
242
251
  a.icon_cls = 'fa fa-table glyph'
243
252
  a.disabled = !self.class.has_any_perm?
@@ -388,6 +397,7 @@ class Marty::MainAuthApp < Marty::AuthApp
388
397
  component :config_view
389
398
 
390
399
  component :data_grid_view
400
+ component :data_grid_user_view
391
401
 
392
402
  component :event_view
393
403
 
@@ -61,10 +61,10 @@ class Marty::ReportForm < Marty::Form
61
61
 
62
62
  begin
63
63
  engine.evaluate(node, 'result', d_params)
64
- rescue StandardError => exc
65
- Marty::Util.logger.error "run_eval failed: #{exc.backtrace}"
64
+ rescue StandardError => e
65
+ Marty::Util.logger.error "run_eval failed: #{e.backtrace}"
66
66
 
67
- res = Delorean::Engine.grok_runtime_exception(exc)
67
+ res = Delorean::Engine.grok_runtime_exception(e)
68
68
  res['backtrace'] =
69
69
  res['backtrace'].map { |m, line, fn| "#{m}:#{line} #{fn}" }.join('\n')
70
70
  res
@@ -156,15 +156,15 @@ class Marty::ReportForm < Marty::Form
156
156
  raise 'bad form items' unless items.is_a?(Array)
157
157
  raise 'bad format' unless
158
158
  Marty::ContentHandler::GEN_FORMATS.member?(format)
159
- rescue StandardError => exc
159
+ rescue StandardError => e
160
160
  c.title = 'ERROR'
161
161
  c.items =
162
162
  [
163
163
  {
164
- field_label: 'Exception',
164
+ field_label: 'exception',
165
165
  xtype: :displayfield,
166
166
  name: 'displayfield1',
167
- value: "<span style=\"color:red;\">#{exc}</span>"
167
+ value: "<span style=\"color:red;\">#{e}</span>"
168
168
  },
169
169
  ]
170
170
  return
@@ -83,10 +83,10 @@ class Marty::ScriptForm < Marty::Form
83
83
  begin
84
84
  dev = Marty::Tag.find_by_name('DEV')
85
85
  Marty::ScriptSet.new(dev).parse_check(script.name, data['body'])
86
- rescue Delorean::ParseError => exc
87
- client.netzke_notify exc.message
86
+ rescue Delorean::ParseError => e
87
+ client.netzke_notify e.message
88
88
  client.netzke_apply_form_errors({})
89
- client.set_line_error(exc.line)
89
+ client.set_line_error(e.line)
90
90
  return
91
91
  end
92
92
 
@@ -120,8 +120,8 @@ class Marty::ScriptForm < Marty::Form
120
120
  'PrettyScript',
121
121
  rep_params)
122
122
  client.get_report(path)
123
- rescue StandardError => exc
124
- return client.netzke_notify "ERROR: #{exc}"
123
+ rescue StandardError => e
124
+ return client.netzke_notify "ERROR: #{e}"
125
125
  end
126
126
  end
127
127
 
@@ -75,8 +75,8 @@ class Marty::ScriptTester < Marty::Form
75
75
  client.set_result result.join('<br/>')
76
76
  rescue SystemStackError
77
77
  return client.netzke_notify 'System Stack Error'
78
- rescue StandardError => exc
79
- res = Delorean::Engine.grok_runtime_exception(exc)
78
+ rescue StandardError => e
79
+ res = Delorean::Engine.grok_runtime_exception(e)
80
80
 
81
81
  result = ["Error: #{res['error']}", 'Backtrace:'] +
82
82
  res['backtrace'].map { |m, line, fn| "#{m}:#{line} #{fn}" }
@@ -33,9 +33,7 @@ module Marty; class UserView < Marty::Grid
33
33
  def self.set_roles(roles, user)
34
34
  roles = [] unless roles.present?
35
35
 
36
- roles = Marty::RoleType.get_all.select do |role|
37
- roles.include?(I18n.t("roles.#{role}", default: role))
38
- end
36
+ roles = ::Marty::RoleType.from_nice_names(roles)
39
37
 
40
38
  roles_in_user = user.user_roles.map(&:role)
41
39
  roles_to_delete = roles_in_user - roles
@@ -152,14 +150,10 @@ module Marty; class UserView < Marty::Grid
152
150
  c.type = :string
153
151
 
154
152
  c.getter = lambda do |r|
155
- r.user_roles.map do |ur|
156
- I18n.t("roles.#{ur.role}", default: ur.role)
157
- end
153
+ Marty::RoleType.to_nice_names(r.user_roles.map(&:role))
158
154
  end
159
155
 
160
- store = ::Marty::RoleType.get_all.sort.map do |role|
161
- I18n.t("roles.#{role}", default: role)
162
- end
156
+ store = ::Marty::RoleType.to_nice_names(::Marty::RoleType::VALUES.sort)
163
157
 
164
158
  c.editor_config = {
165
159
  multi_select: true,
@@ -53,13 +53,16 @@ class Marty::BaseRule < Marty::Base
53
53
 
54
54
  def validate
55
55
  self.class.guard_info.each { |name, h| check(name, h) }
56
+
56
57
  grids.each do |vn, gn|
57
58
  return errors[:grids] << "- Bad grid name '#{gn}' for '#{vn}'" unless
58
59
  gn.blank? || Marty::DataGrid.lookup('infinity', gn)
59
60
  end
61
+
60
62
  cg_err = computed_guards.delete('~~ERROR~~')
61
63
  errors[:computed] <<
62
64
  "- Error in rule '#{name}' field 'computed_guards': #{cg_err.capitalize}" if cg_err
65
+
63
66
  res_err = results.delete('~~ERROR~~')
64
67
  errors[:computed] <<
65
68
  "- Error in rule '#{name}' field 'results': #{res_err.capitalize}" if res_err
@@ -67,12 +70,14 @@ class Marty::BaseRule < Marty::Base
67
70
 
68
71
  validates_presence_of :name
69
72
  validate :validate
73
+ validate :validate_simple_guard_options
70
74
 
71
75
  before_validation(on: [:create, :update]) do
72
- self.simple_guards ||= {}
73
- self.computed_guards ||= {}
74
- self.grids ||= {}
75
- self.results ||= {}
76
+ self.simple_guards ||= {}
77
+ self.simple_guards_options ||= {}
78
+ self.computed_guards ||= {}
79
+ self.grids ||= {}
80
+ self.results ||= {}
76
81
  end
77
82
 
78
83
  before_create do
@@ -83,38 +88,93 @@ class Marty::BaseRule < Marty::Base
83
88
  end
84
89
  end
85
90
 
86
- def self.get_subq(field, subfield, multi, type, vraw)
87
- arrow = multi || ![:range, :string, :date, :datetime].include?(type) ?
88
- '->' : '->>'
89
- op = multi || type == :range ? '@>' : '='
90
- value0 = [:string, :date, :datetime].include?(type) ?
91
- ActiveRecord::Base.connection.quote(vraw) :
92
- type == :range ? vraw.to_f :
93
- "'#{vraw}'::jsonb"
94
- value = multi ? %Q('["%s"]') % value0[1..-2] : value0
95
- fieldcast = type == :range ? '::numrange' : ''
96
- "(#{field}#{arrow}'#{subfield}')#{fieldcast} #{op} #{value}"
91
+ private
92
+
93
+ def validate_simple_guard_options
94
+ simple_guards_options.each do |guard_name, value|
95
+ field_path = "'simple_guard_options' -> '#{guard_name}' -> 'not'"
96
+
97
+ guard_info = self.class.guard_info[guard_name.to_s]
98
+
99
+ if guard_info.blank?
100
+ errors[:simple_guards_options] <<
101
+ "- Error in rule '#{name}' #{field_path}."\
102
+ "Guard '#{guard_name}' doesn't exist."
103
+
104
+ next
105
+ end
106
+
107
+ not_is_allowed = guard_info.fetch(:allow_not, true)
108
+
109
+ not_field = value['not'] || value[:not]
110
+ next if not_field.nil?
111
+
112
+ if not_field.is_a?(TrueClass)
113
+ next if not_is_allowed
114
+
115
+ errors[:simple_guards_options] <<
116
+ "- Error in rule '#{name}' #{field_path}. True value is not allowed"
117
+ next
118
+ end
119
+
120
+ next if not_field.is_a?(FalseClass)
121
+
122
+ errors[:simple_guards_options] <<
123
+ "- Error in rule '#{name}' #{field_path} field must be a boolean"
124
+ end
97
125
  end
98
126
 
99
- def self.get_matches_(_pt, attrs, params)
100
- q = select('DISTINCT ON (name) *').where(attrs)
127
+ class << self
128
+ def get_subq(field, subfield, multi, type, vraw)
129
+ arrow = multi || ![:range, :string, :date, :datetime].include?(type) ?
130
+ '->' : '->>'
131
+ op = multi || type == :range ? '@>' : '='
132
+ value0 = [:string, :date, :datetime].include?(type) ?
133
+ ActiveRecord::Base.connection.quote(vraw) :
134
+ type == :range ? vraw.to_f :
135
+ "'#{vraw}'::jsonb"
136
+ value = multi ? %Q('["%s"]') % value0[1..-2] : value0
137
+ fieldcast = type == :range ? '::numrange' : ''
138
+ "(#{field}#{arrow}'#{subfield}')#{fieldcast} #{op} #{value}"
139
+ end
140
+
141
+ def get_matches_(_pt, attrs, params)
142
+ q = select('DISTINCT ON (name) *').where(attrs)
101
143
 
102
- params.each do |k, vraw|
103
144
  h = guard_info
104
- use_k = (h[k] && k) ||
105
- (h[k + '_array'] && k + '_array') ||
106
- (h[k + '_range'] && k + '_range')
107
- next unless use_k
108
-
109
- multi, type = h[use_k].values_at(:multi, :type)
110
- filts = [vraw].flatten.map do |v|
111
- qstr = get_subq('simple_guards', use_k, multi, type, v)
112
- end.join(' OR ')
113
- isn = "simple_guards->'#{use_k}' IS NULL OR"
114
-
115
- q = q.where("(#{isn} #{filts})")
145
+
146
+ params.each do |k, vraw|
147
+ use_k = (h[k] && k) ||
148
+ (h[k + '_array'] && k + '_array') ||
149
+ (h[k + '_range'] && k + '_range')
150
+
151
+ next unless use_k
152
+
153
+ param_guard_info = h[use_k]
154
+
155
+ multi = param_guard_info[:multi]
156
+ type = param_guard_info[:type]
157
+ with_not = param_guard_info.fetch(:allow_not, true)
158
+
159
+ filts = [vraw].flatten.map do |v|
160
+ qstr = get_subq('simple_guards', use_k, multi, type, v)
161
+ end.join(' OR ')
162
+
163
+ condition = if with_not
164
+ "CASE
165
+ WHEN (simple_guards_options->'#{use_k}'->>'not')::boolean
166
+ THEN simple_guards->'#{use_k}' IS NULL OR NOT #{filts}
167
+ ELSE simple_guards->'#{use_k}' IS NULL OR #{filts}
168
+ END
169
+ "
170
+ else
171
+ "simple_guards->'#{use_k}' IS NULL OR #{filts}"
172
+ end
173
+
174
+ q = q.where(condition)
175
+ end
176
+ # print q.to_sql
177
+ q.order(:name)
116
178
  end
117
- # print q.to_sql
118
- q.order(:name)
119
179
  end
120
180
  end
@@ -8,9 +8,11 @@ class Marty::DataGrid < Marty::Base
8
8
  'integer' => Marty::GridIndexInteger,
9
9
  'string' => Marty::GridIndexString,
10
10
  'boolean' => Marty::GridIndexBoolean,
11
- }
11
+ }.freeze
12
12
 
13
- ARRSEP = '|'
13
+ ARRSEP = '|'.freeze
14
+ NOT_STRING_START = 'NOT ('.freeze
15
+ NOT_STRING_END = ')'.freeze
14
16
 
15
17
  class DataGridValidator < ActiveModel::Validator
16
18
  def validate(dg)
@@ -253,8 +255,7 @@ class Marty::DataGrid < Marty::Base
253
255
 
254
256
  # private method used to cache lookup_grid_distinct_entry_h result
255
257
  delorean_fn :lookup_grid_h_priv,
256
- to_hash: false, private: true, cache: true, sig: 4 do |pt, dgh, h, distinct|
257
-
258
+ private: true, cache: true, sig: 4 do |pt, dgh, h, distinct|
258
259
  lookup_grid_distinct_entry_h(
259
260
  pt, h, dgh, nil, true, false, distinct)['result']
260
261
  end
@@ -271,9 +272,9 @@ class Marty::DataGrid < Marty::Base
271
272
  end
272
273
 
273
274
  def self.lookup_grid_distinct_entry_h(
274
- pt, h, dgh, visited = nil, follow = true,
275
- return_grid_data = false, distinct = true
276
- )
275
+ pt, h, dgh, visited = nil, follow = true,
276
+ return_grid_data = false, distinct = true
277
+ )
277
278
 
278
279
  # Perform grid lookup, if result is another data_grid, and follow is true,
279
280
  # then perform lookup on the resulting grid. Allows grids to be nested
@@ -327,9 +328,10 @@ class Marty::DataGrid < Marty::Base
327
328
  # should unify this with Marty::DataConversion.convert
328
329
 
329
330
  type = inf['type']
331
+ nots = inf.fetch('nots', [])
330
332
  klass = type.constantize unless INDEX_MAP[type]
331
333
 
332
- inf['keys'].map do |v|
334
+ keys = inf['keys'].map do |v|
333
335
  case type
334
336
  when 'numrange', 'int4range'
335
337
  Marty::Util.pg_range_to_human(v)
@@ -351,6 +353,12 @@ class Marty::DataGrid < Marty::Base
351
353
  v.join(ARRSEP) if v
352
354
  end
353
355
  end
356
+
357
+ keys.each_with_index.map do |v, index|
358
+ next v unless nots[index]
359
+
360
+ add_not(v)
361
+ end
354
362
  end
355
363
 
356
364
  # FIXME: this is only here to appease Netzke add_in_form
@@ -360,13 +368,19 @@ class Marty::DataGrid < Marty::Base
360
368
  def export_array
361
369
  # add data type metadata row if not default
362
370
  lenstr = 'lenient' if lenient
363
- typestr = data_type unless [nil, DEFAULT_DATA_TYPE].member?(data_type) &&
364
- !constraint.present?
365
371
 
366
- len_dt = [lenstr, typestr].compact.join(' ')
372
+ typestr = data_type unless [nil, DEFAULT_DATA_TYPE].member?(data_type)
373
+ len_type = [lenstr, typestr].compact.join(' ')
367
374
 
368
- meta_rows = len_dt.present? || constraint.present? ?
369
- [[len_dt, constraint]] : []
375
+ meta_rows = if (lenient || typestr) && constraint
376
+ [[len_type, constraint]]
377
+ elsif lenient || typestr
378
+ [[len_type]]
379
+ elsif constraint
380
+ [['', constraint]]
381
+ else
382
+ []
383
+ end
370
384
 
371
385
  meta_rows += metadata.map do |inf|
372
386
  [inf['attr'], inf['type'], inf['dir'], inf['rs_keep'] || '']
@@ -409,6 +423,8 @@ class Marty::DataGrid < Marty::Base
409
423
  def self.parse_fvalue(pt, v, type, klass)
410
424
  return unless v
411
425
 
426
+ v = remove_not(v)
427
+
412
428
  case type
413
429
  when 'numrange', 'int4range'
414
430
  Marty::Util.human_to_pg_range(v)
@@ -461,11 +477,20 @@ class Marty::DataGrid < Marty::Base
461
477
 
462
478
  def self.parse_keys(pt, keys, type)
463
479
  klass = maybe_get_klass(type)
480
+
464
481
  keys.map do |v|
465
482
  parse_fvalue(pt, v, type, klass)
466
483
  end
467
484
  end
468
485
 
486
+ def self.parse_nots(_pt, keys)
487
+ keys.map do |v|
488
+ next false unless v
489
+
490
+ v.starts_with?(NOT_STRING_START) && v.ends_with?(NOT_STRING_END)
491
+ end
492
+ end
493
+
469
494
  # parse grid external representation into metadata/data
470
495
  def self.parse(pt, grid_text, options)
471
496
  options[:headers] ||= false
@@ -506,11 +531,15 @@ class Marty::DataGrid < Marty::Base
506
531
  raise "unknown metadata type #{type}" unless
507
532
  Marty::DataGrid.type_to_index(type)
508
533
 
534
+ keys = key && parse_keys(pt, [key], type)
535
+ nots = key && parse_nots(pt, [key])
536
+
509
537
  res = {
510
538
  'attr' => attr,
511
539
  'type' => type,
512
540
  'dir' => dir,
513
- 'keys' => key && parse_keys(pt, [key], type),
541
+ 'keys' => keys,
542
+ 'nots' => nots,
514
543
  }
515
544
  res['rs_keep'] = rs_keep if rs_keep
516
545
  res
@@ -530,6 +559,7 @@ class Marty::DataGrid < Marty::Base
530
559
  row[0, v_infos.count].any?
531
560
 
532
561
  inf['keys'] = parse_keys(pt, row[v_infos.count, row.count], inf['type'])
562
+ inf['nots'] = parse_nots(pt, row[v_infos.count, row.count])
533
563
  end
534
564
 
535
565
  raise 'horiz. info keys length mismatch!' unless
@@ -542,6 +572,7 @@ class Marty::DataGrid < Marty::Base
542
572
 
543
573
  v_infos.each_with_index do |inf, i|
544
574
  inf['keys'] = parse_keys(pt, v_key_cols[i], inf['type'])
575
+ inf['nots'] = parse_nots(pt, v_key_cols[i])
545
576
  end
546
577
 
547
578
  raise 'vert. info keys length mismatch!' unless
@@ -553,18 +584,23 @@ class Marty::DataGrid < Marty::Base
553
584
 
554
585
  # based on data type, decide to check using convert or instance
555
586
  # lookup. FIXME: DRY.
587
+
556
588
  if String === c_data_type
557
589
  tsym = c_data_type.to_sym
558
590
 
559
591
  data = data_rows.map do |r|
560
592
  r[v_infos.count, r.count].map do |v|
561
- Marty::DataConversion.convert(v, tsym) if v
593
+ next v unless v
594
+
595
+ Marty::DataConversion.convert(v, tsym)
562
596
  end
563
597
  end
564
598
  else
565
599
  data = data_rows.map do |r|
566
600
  r[v_infos.count, r.count].map do |v|
567
- next v if !v || Marty::DataGrid.
601
+ next v unless v
602
+
603
+ next v if Marty::DataGrid.
568
604
  find_class_instance(pt, c_data_type, v)
569
605
 
570
606
  raise "can't find key '#{v}' for class #{data_type}"
@@ -572,12 +608,24 @@ class Marty::DataGrid < Marty::Base
572
608
  end
573
609
  end
574
610
 
575
- [metadata, data, data_type, lenient, constraint]
611
+ {
612
+ metadata: metadata,
613
+ data: data,
614
+ data_type: data_type,
615
+ lenient: lenient,
616
+ constraint: constraint,
617
+ }
576
618
  end
577
619
 
578
620
  def self.create_from_import(name, import_text, created_dt = nil)
579
- metadata, data, data_type, lenient, constraint = parse(created_dt,
580
- import_text, {})
621
+ parsed_result = parse(created_dt, import_text, {})
622
+
623
+ metadata = parsed_result[:metadata]
624
+ data = parsed_result[:data]
625
+ data_type = parsed_result[:data_type]
626
+ lenient = parsed_result[:lenient]
627
+ constraint = parsed_result[:constraint]
628
+
581
629
  dg = new
582
630
  dg.name = name
583
631
  dg.data = data
@@ -591,8 +639,13 @@ class Marty::DataGrid < Marty::Base
591
639
  end
592
640
 
593
641
  def update_from_import(name, import_text, created_dt = nil)
594
- new_metadata, data, data_type, lenient, constraint =
595
- self.class.parse(created_dt, import_text, {})
642
+ parsed_result = self.class.parse(created_dt, import_text, {})
643
+
644
+ new_metadata = parsed_result[:metadata]
645
+ data = parsed_result[:data]
646
+ data_type = parsed_result[:data_type]
647
+ lenient = parsed_result[:lenient]
648
+ constraint = parsed_result[:constraint]
596
649
 
597
650
  self.name = name
598
651
  self.data = data
@@ -609,7 +662,10 @@ class Marty::DataGrid < Marty::Base
609
662
  def build_index
610
663
  # create indices for the metadata
611
664
  metadata.each do |inf|
612
- attr, type, keys = inf['attr'], inf['type'], inf['keys']
665
+ attr = inf['attr']
666
+ type = inf['type']
667
+ keys = inf['keys']
668
+ nots = inf.fetch('nots', [])
613
669
 
614
670
  # find index class
615
671
  idx_class = Marty::DataGrid.type_to_index(type)
@@ -621,6 +677,7 @@ class Marty::DataGrid < Marty::Base
621
677
  gi.created_dt = created_dt
622
678
  gi.data_grid_id = group_id
623
679
  gi.index = index
680
+ gi.not = nots[index] || false
624
681
  gi.save!
625
682
  end
626
683
  end
@@ -796,4 +853,17 @@ class Marty::DataGrid < Marty::Base
796
853
  # we convert to the opposite (what to prune)
797
854
  [opposite_sign(opstr.to_sym), ident]
798
855
  end
856
+
857
+ def self.remove_not(string)
858
+ return string unless string.starts_with?(NOT_STRING_START)
859
+ return string unless string.ends_with?(NOT_STRING_END)
860
+
861
+ remove_from_left = NOT_STRING_START.size
862
+ remove_from_right = NOT_STRING_END.size
863
+ string.slice(remove_from_left...-remove_from_right)
864
+ end
865
+
866
+ def self.add_not(string)
867
+ "#{NOT_STRING_START}#{string}#{NOT_STRING_END}"
868
+ end
799
869
  end