marty 8.5.0 → 9.3.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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintignore +1 -0
  3. data/.eslintrc.js +26 -0
  4. data/.gitignore +3 -0
  5. data/.gitlab-ci.yml +7 -0
  6. data/.prettierignore +14 -0
  7. data/.rubocop_todo.yml +1 -1
  8. data/Dockerfile.dummy +3 -0
  9. data/Makefile +1 -0
  10. data/app/assets/javascripts/marty/cable.js +7 -3
  11. data/app/assets/javascripts/marty/extjs/extensions/datetime_field/component.js +401 -0
  12. data/app/assets/javascripts/marty/extjs/extensions/datetime_field/field.js +140 -0
  13. data/app/assets/javascripts/marty/extjs/extensions/marty.js +845 -781
  14. data/app/assets/stylesheets/marty/codemirror/codemirror.css +215 -77
  15. data/app/assets/stylesheets/marty/codemirror/delorean.css +2 -2
  16. data/app/assets/stylesheets/marty/dark_mode.css +13 -3
  17. data/app/components/marty/auth_app/client/auth_app.js +107 -102
  18. data/app/components/marty/base_rule_view/client/base_rule_view.js +10 -8
  19. data/app/components/marty/data_grid_view/client/data_grid_edit.js +534 -519
  20. data/app/components/marty/form/client/form.js +3 -3
  21. data/app/components/marty/grid/client/grid.js +110 -87
  22. data/app/components/marty/import_view/client/import_view.js +18 -18
  23. data/app/components/marty/live_search_grid_panel/client/live_search_grid_panel.js +14 -13
  24. data/app/components/marty/main_auth_app/client/main_auth_app.js +42 -42
  25. data/app/components/marty/mcfly_grid_panel/client/mcfly_grid_panel.js +27 -18
  26. data/app/components/marty/new_posting_form/client/new_posting_form.js +3 -3
  27. data/app/components/marty/panel/client/panel.js +3 -3
  28. data/app/components/marty/posting_grid/client/posting_grid.js +24 -18
  29. data/app/components/marty/promise_view/client/promise_view.css +12 -12
  30. data/app/components/marty/promise_view/client/promise_view.js +46 -38
  31. data/app/components/marty/report_form/client/report_form.js +30 -28
  32. data/app/components/marty/report_select/client/report_select.js +28 -23
  33. data/app/components/marty/reporting/client/reporting.js +3 -3
  34. data/app/components/marty/script_form/client/script_form.js +29 -23
  35. data/app/components/marty/script_tester/client/script_tester.js +4 -5
  36. data/app/components/marty/scripting/client/scripting.js +40 -36
  37. data/app/components/marty/simple_app/client/simple_app.js +33 -24
  38. data/app/components/marty/simple_app/client/statusbar_ext.js +1 -1
  39. data/app/controllers/marty/rpc_controller.rb +3 -0
  40. data/app/models/marty/promise.rb +10 -2
  41. data/app/services/marty/data_grid/constraint.rb +2 -1
  42. data/app/services/marty/jobs/schedule.rb +2 -2
  43. data/app/services/marty/promises/delorean/create.rb +9 -2
  44. data/app/services/marty/promises/ruby/create.rb +7 -2
  45. data/config/initializers/delayed_job_config.rb +1 -0
  46. data/delorean/blame_report.dl +50 -58
  47. data/delorean/enum_report.dl +2 -3
  48. data/delorean/{marty_fields.dl → fields.dl} +16 -0
  49. data/delorean/styles.dl +216 -0
  50. data/delorean/table_report.dl +4 -4
  51. data/lib/marty/monkey.rb +17 -0
  52. data/lib/marty/promise_job.rb +9 -0
  53. data/lib/marty/promise_ruby_job.rb +8 -0
  54. data/lib/marty/version.rb +1 -1
  55. data/make-lint.mk +19 -0
  56. data/package.json +16 -0
  57. data/prettier.config.js +6 -0
  58. data/spec/controllers/diagnostic/controller_spec.rb +0 -1
  59. data/spec/controllers/rpc_controller_spec.rb +21 -7
  60. data/spec/dummy/delorean/data_report.dl +4 -4
  61. data/spec/dummy/delorean/fields.dl +1 -0
  62. data/spec/features/data_grid_spec.rb +37 -1
  63. data/spec/job_helper.rb +6 -0
  64. data/spec/lib/data_blame_spec.rb +4 -4
  65. data/spec/lib/data_importer_spec.rb +6 -4
  66. data/spec/models/promise_spec.rb +31 -0
  67. data/spec/spec_helper.rb +8 -0
  68. data/spec/support/download_helper.rb +53 -49
  69. data/spec/support/json_helper.rb +11 -0
  70. data/spec/support/shared_connection_db_helpers.rb +1 -0
  71. data/spec/support/suite.rb +20 -14
  72. data/yarn.lock +967 -0
  73. metadata +16 -4
  74. data/spec/dummy/delorean/marty_fields.dl +0 -1
@@ -1,4 +1,4 @@
1
- import MartyFields
1
+ import Fields
2
2
  import Styles
3
3
 
4
4
  EnumValuesReport:
@@ -7,5 +7,4 @@ EnumValuesReport:
7
7
  raw = Marty::Enums::Report.call()
8
8
  result = Marty::Helper.to_csv(raw)
9
9
  form = []
10
- format = "csv"
11
-
10
+ format = "csv"
@@ -60,3 +60,19 @@ TextField:
60
60
  xtype = ":textfield"
61
61
  field_label = "Text"
62
62
  name = "text_field"
63
+
64
+ ######################################################################
65
+
66
+ DateField:
67
+ field_label = "Date"
68
+ xtype = ":datefield"
69
+ name = "date"
70
+ value = nil
71
+ format = 'Y-m-d'
72
+
73
+ DatetimeField:
74
+ field_label = "Datetime"
75
+ xtype = ":datetimefield"
76
+ name = "datetime"
77
+ value = nil
78
+ format = 'Y-m-d H:i:s'
@@ -0,0 +1,216 @@
1
+ Style:
2
+ digit_0 = {"format_code" : "0"}
3
+ digit_1 = {"format_code" : "0.0"}
4
+ digit_2 = {"format_code" : "0.00"}
5
+ digit_3 = {"format_code" : "0.000"}
6
+ digit_4 = {"format_code" : "0.0000"}
7
+ digit_5 = {"format_code" : "0.00000"}
8
+ digit_6 = {"format_code" : "0.000000"}
9
+ date = {"format_code" : "mm/dd/yy"}
10
+ timestamp = {"format_code" : "m/d/yy h:mm AM/PM"}
11
+ datetime = {"format_code" : "yyyy-mm-dd h:mm:ss"}
12
+ date_w_time = {"format_code" : "mm/dd/yy h:mm:ss"}
13
+ dollars = {"format_code" : "$#,###"}
14
+ dollars_cent = {"format_code" : "$#,###.##"}
15
+ text = {"format_code" : "@"}
16
+
17
+ bg_gray = {"bg_color": "C5D9F1"}
18
+ bg_lightgreen = {"bg_color": "99CC00"}
19
+ bg_tan = {"bg_color": "FFCC66"}
20
+ bg_redish = {"bg_color": "EF5959"}
21
+ bg_lightgray = {"bg_color": "F1F5F5"}
22
+
23
+ quoted = {"quote_prefix": true}
24
+
25
+ gray_digit_3 = bg_gray + digit_3
26
+
27
+ bg_penny = {
28
+ "bg_color": "0D056F",
29
+ "fg_color": "FFFFFF",
30
+ }
31
+
32
+ bg_highlight = {
33
+ "bg_color": "FFFF00",
34
+ "fg_color": "0D056F",
35
+ }
36
+
37
+ bold = {"b": true}
38
+
39
+ penny_bold = bold + bg_penny
40
+
41
+ align_center = {
42
+ "alignment": {
43
+ "horizontal": ":center",
44
+ }
45
+ }
46
+
47
+ align_left = {
48
+ "alignment": {
49
+ "horizontal": ":left",
50
+ }
51
+ }
52
+
53
+ align_right = {
54
+ "alignment": {
55
+ "horizontal": ":right",
56
+ }
57
+ }
58
+
59
+ calibri = {"font_name": "Calibri"}
60
+ sz_14 = {"sz": 14}
61
+ sz_12 = {"sz": 12}
62
+ sz_10 = {"sz": 10}
63
+
64
+ h_hdr = bold + bg_highlight + align_center + calibri + sz_14
65
+ m_hdr = penny_bold + align_center + calibri + sz_14
66
+ s_hdr = penny_bold + align_left + calibri + sz_12
67
+ s_hdr_dt = s_hdr + datetime
68
+ s_hdr_date = s_hdr + date
69
+ s_hdr_dwt = s_hdr + date_w_time
70
+ g_bold = bg_gray + bold
71
+
72
+ border_thin = {
73
+ "style" : ":thin",
74
+ "color" : "000000",
75
+ }
76
+
77
+ ######################################################################
78
+
79
+ m_hdr_style0 = {"style": m_hdr}
80
+ m_hdr_style1 = {"style": [m_hdr]}
81
+
82
+ s_hdr_style2 = {"style": [s_hdr]*2}
83
+ s_hdr_style3 = {"style": [s_hdr]*3}
84
+
85
+ HeatMapFmt:
86
+ color_scale = [
87
+ {"type":":min", "val":0, "color":"FF63BE7B"},
88
+ {"type":":percent", "val":"50", "color":"FFFFEB84"},
89
+ {"type":":max", "val":0, "color":"FFF8696B"}
90
+ ]
91
+
92
+ cfmt = {
93
+ "type": ":colorScale",
94
+ "priority": 1,
95
+ "color_scale": color_scale,
96
+ }
97
+
98
+ ThresholdFmt:
99
+ winner =? 0
100
+ thresh_cell =? '$B$1'
101
+
102
+ cfmt = {
103
+ "type": ":expression",
104
+ "priority": 1,
105
+ "dxfId" : {
106
+ "fg_color": "FFF8696B",
107
+ "type": ":dxf",
108
+ "b": true,
109
+ },
110
+ "formula": 'ABS(INDIRECT("RC", FALSE))>'+thresh_cell,
111
+ }
112
+
113
+ cfmtnot0 = {
114
+ "type" : ":cellIs",
115
+ "operator" : ":notEqual",
116
+ "formula" : "0",
117
+ "dxfId" : {
118
+ "fg_color": "FFF8696B",
119
+ "type": ":dxf",
120
+ "b": true,
121
+ },
122
+ "priority" : 1,
123
+ }
124
+
125
+ cfmtequal = {
126
+ "type" : ":cellIs",
127
+ "operator" : ":equal",
128
+ "formula" : winner.to_s,
129
+ "dxfId" :
130
+ {
131
+ "fg_color": "FFF8696B",
132
+ "type": ":dxf",
133
+ "b": true,
134
+ },
135
+ "priority" : 1,
136
+ }
137
+
138
+ cfmtticks = {
139
+ "type": ":expression",
140
+ "priority": 1,
141
+ "dxfId" : {
142
+ "fg_color": "FFF8696B",
143
+ "type": ":dxf",
144
+ "b": true,
145
+ },
146
+ "formula": 'ABS(LEFT(INDIRECT("RC", FALSE),FIND("-",INDIRECT("RC", FALSE), 2)-1))+MID(SUBSTITUTE(INDIRECT("RC", FALSE),"+",""),FIND("-",INDIRECT("RC", FALSE), 2)+1,9)/32+(RIGHT(INDIRECT("RC", FALSE))="+")/64>'+thresh_cell,
147
+ }
148
+
149
+ Row:
150
+ blank_cells =? 0
151
+ values =? []
152
+ styles =? []
153
+
154
+ blank_styles = [{}]*blank_cells
155
+ blank = ["row", [nil]*blank_cells,
156
+ {"style": blank_styles}]
157
+
158
+ result = ["row", values + blank[1],
159
+ {"style": styles + blank_styles}]
160
+
161
+ Fill:
162
+ rows =?
163
+ fill =? nil
164
+ length =?
165
+
166
+ filled_rows = [
167
+ [r[0],
168
+ (if r[1].length >= length
169
+ then r[1]
170
+ else r[1] + [fill]*(length - r[1].length)
171
+ ),
172
+ {'style': (r[2]['style'] || []) + [{}]*(length - r[1].length)}]
173
+ for r in rows]
174
+
175
+ Merge:
176
+ init =? 0
177
+ rows_a =?
178
+ rows_b =?
179
+ blank_length =?
180
+
181
+ add_options =? {}
182
+
183
+ ra_len = rows_a.length
184
+ rb_len = rows_b.length
185
+
186
+ len = if ra_len >= rb_len then ra_len else rb_len
187
+
188
+ result = [
189
+ ["row",
190
+ ((rows_a[i] || Row(blank_cells=blank_length).blank)[1] +
191
+ (rows_b[i] || Row.blank)[1]),
192
+ {"style": (if rows_a[i][2]
193
+ then rows_a[i][2]['style']
194
+ else Row(blank_cells=blank_length).blank_styles) +
195
+ (if rows_b[i][2]
196
+ then rows_b[i][2]['style']
197
+ else [])
198
+ } + (add_options[i] || {})
199
+ ] for i in Marty::Helper.range_step(init, len-1, 1)]
200
+
201
+ Alternate:
202
+ bg_color_a =? 'F3F3F3'
203
+ bg_color_b =? 'FFFFFF'
204
+
205
+ color = {
206
+ 0: {'bg_color': bg_color_a},
207
+ 1: {'bg_color': bg_color_b},
208
+ }
209
+
210
+ rows =?
211
+
212
+ result = [
213
+ ['row',
214
+ rows[i][1],
215
+ {'style': [(color[i%2] + s) for s in rows[i][2]['style']]}
216
+ ] for i in Marty::Helper.range_step(0, rows.length - 1, 1)]
@@ -1,7 +1,7 @@
1
- import MartyFields
1
+ import Fields
2
2
  import Styles
3
3
 
4
- ReadableField: MartyFields::CheckboxField
4
+ ReadableField: Fields::CheckboxField
5
5
  name = "readable"
6
6
  field_label = "Disable Encoding"
7
7
 
@@ -26,8 +26,8 @@ TableReport:
26
26
  row_count = result_raw.length
27
27
  result = Marty::Helper.to_csv(result_raw, {'readable' : readable})
28
28
  form = [
29
- MartyFields::PostingField,
30
- MartyFields::ClassField,
29
+ Fields::PostingField,
30
+ Fields::ClassField,
31
31
  ReadableField,
32
32
  ]
33
33
  format = "csv"
@@ -376,6 +376,23 @@ module Netzke
376
376
  end
377
377
  end
378
378
 
379
+ module Netzke
380
+ module Core
381
+ class ClientClassConfig
382
+ # FIXME: move to Netzke
383
+ # This fix removes ; in the end of JS code that is required by JS linters
384
+ # And adds new line before closing bracket, so that if there is a comment
385
+ # in the end of file, it won't break the code.
386
+ def override_from_file(path)
387
+ str = File.read(path)
388
+ str.chomp!("\n").chomp!(';')
389
+ %{#{class_name}.override(#{str}
390
+ );}
391
+ end
392
+ end
393
+ end
394
+ end
395
+
379
396
  require 'delayed_cron_job'
380
397
  require_relative './delayed_job/scheduled_job_plugin.rb'
381
398
 
@@ -8,6 +8,7 @@ class Marty::PromiseJob < Struct.new(:promise,
8
8
  :params,
9
9
  :attrs,
10
10
  :hook,
11
+ :max_run_time
11
12
  )
12
13
  # def log(msg)
13
14
  # open('/tmp/dj.out', 'a') { |f| f.puts msg }
@@ -41,10 +42,18 @@ class Marty::PromiseJob < Struct.new(:promise,
41
42
  end
42
43
 
43
44
  # log "DONE #{Process.pid} #{promise.id} #{Time.now.to_f} #{res}"
45
+ rescue ::Delayed::WorkerTimeout => e
46
+ timeout_error = StandardError.new(
47
+ ::Marty::Promise.timeout_message(promise)
48
+ )
49
+ timeout_error.set_backtrace(e.backtrace)
50
+
51
+ res = Delorean::Engine.grok_runtime_exception(timeout_error)
44
52
  rescue StandardError => e
45
53
  res = Delorean::Engine.grok_runtime_exception(e)
46
54
  # log "ERR- #{Process.pid} #{promise.id} #{Time.now.to_f} #{e}"
47
55
  end
56
+
48
57
  promise.set_result(res)
49
58
  process_hook(res)
50
59
  end
@@ -4,6 +4,7 @@ class Marty::PromiseRubyJob < Struct.new(:promise,
4
4
  :method_name,
5
5
  :method_args,
6
6
  :hook,
7
+ :max_run_time
7
8
  )
8
9
 
9
10
  def enqueue(job)
@@ -26,6 +27,13 @@ class Marty::PromiseRubyJob < Struct.new(:promise,
26
27
  ENV['__promise_id'] = promise.id.to_s
27
28
  mod = module_name.constantize
28
29
  res = { 'result' => mod.send(method_name, *method_args) }
30
+ rescue ::Delayed::WorkerTimeout => e
31
+ timeout_error = StandardError.new(
32
+ ::Marty::Promise.timeout_message(promise)
33
+ )
34
+ timeout_error.set_backtrace(e.backtrace)
35
+
36
+ res = Delorean::Engine.grok_runtime_exception(timeout_error)
29
37
  rescue StandardError => e
30
38
  res = ::Marty::Promise.exception_to_result(promise: promise, exception: e)
31
39
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Marty
4
- VERSION = '8.5.0'
4
+ VERSION = '9.3.0'
5
5
  end
@@ -0,0 +1,19 @@
1
+ lint:
2
+ make lint-ruby
3
+ make lint-js
4
+
5
+ lint-fix:
6
+ make lint-ruby-fix
7
+ make lint-js-fix
8
+
9
+ lint-ruby:
10
+ bundle exec rubocop
11
+
12
+ lint-ruby-fix:
13
+ bundle exec rubocop -a
14
+
15
+ lint-js:
16
+ yarn lint
17
+
18
+ lint-js-fix:
19
+ yarn lintfix
@@ -0,0 +1,16 @@
1
+ {
2
+ "private": true,
3
+ "scripts": {
4
+ "lint": "eslint 'app/**/*.{js,jsx}' && prettier --check \"app/**/*.{js,jsx,css,scss}\"",
5
+ "lintfix": "eslint --fix 'app/**/*.{js,jsx}' && prettier --write \"app/**/*.{js,jsx,css,scss}\""
6
+ },
7
+ "dependencies": {
8
+ },
9
+ "devDependencies": {
10
+ "babel-eslint": "^10.0.1",
11
+ "eslint": "^6.0.0",
12
+ "eslint-config-prettier": "^6.0.0",
13
+ "eslint-plugin-prettier": "^3.1.0",
14
+ "prettier": "^1.17.1"
15
+ }
16
+ }
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ arrowParens: "always",
3
+ printWidth: 80,
4
+ quoteProps: "consistent",
5
+ semi: true
6
+ };
@@ -8,7 +8,6 @@ end
8
8
  module Marty::Diagnostic
9
9
  RSpec.describe Controller, type: :controller do
10
10
  before(:each) { @routes = Marty::Engine.routes }
11
- let(:json_response) { JSON.parse(response.body) }
12
11
 
13
12
  def my_ip
14
13
  Node.my_ip
@@ -488,6 +488,8 @@ describe Marty::RpcController do
488
488
  let(:p1) { @p1 }
489
489
  let(:p2) { @p2 }
490
490
 
491
+ let(:json_response) { JSON.parse(response.body) }
492
+
491
493
  it 'should be able to post' do
492
494
  post 'evaluate', params: {
493
495
  format: :json,
@@ -511,7 +513,7 @@ describe Marty::RpcController do
511
513
  params: { a: 333, d: 5 }.to_json,
512
514
  background: true,
513
515
  }
514
- res = ActiveSupport::JSON.decode response.body
516
+ res = json_response
515
517
  expect(res).to include('job_id')
516
518
  job_id = res['job_id']
517
519
 
@@ -787,10 +789,10 @@ describe Marty::RpcController do
787
789
  params: params
788
790
  }
789
791
  expect = 'Schema error for M1/A attrs=b: Schema not defined'
790
- res = JSON.parse(response.body)
791
- expect(res.keys.size).to eq(1)
792
- expect(res.keys[0]).to eq('error')
793
- expect(res.values[0]).to eq(expect)
792
+ expect(json_response['backtrace']).to be nil
793
+ expect(json_response.size).to eq(1)
794
+ expect(json_response.keys[0]).to eq('error')
795
+ expect(json_response.values[0]).to eq(expect)
794
796
  end
795
797
 
796
798
  it 'returns an error message on missing attributes in schema script' do
@@ -849,6 +851,7 @@ describe Marty::RpcController do
849
851
  }
850
852
  expect = '[""The property \'#/p\' of type string did not '\
851
853
  'match the following type: integer'
854
+
852
855
  expect(response.body).to include(expect)
853
856
  end
854
857
 
@@ -870,9 +873,10 @@ describe Marty::RpcController do
870
873
  attrs: attr,
871
874
  params: params
872
875
  }
873
- res = JSON.parse(response.body)
874
876
  errpart = 'of type string did not match the following type: integer'
875
- expect(res['error']).to include(errpart)
877
+ expect(json_response['error']).to include(errpart)
878
+ expect(json_response['backtrace']).to be nil
879
+
876
880
  logs = Marty::Log.all
877
881
  expect(logs.count).to eq(1)
878
882
  expect(logs[0].details['error'][0]).to include(errpart)
@@ -1386,41 +1390,51 @@ describe Marty::RpcController do
1386
1390
  it 'returns bad attrs if attr is not a string' do
1387
1391
  get :evaluate, params: { format: :json, attrs: 0 }
1388
1392
  expect(response.body).to match(/"error":"Malformed attrs"/)
1393
+ expect(json_response['backtrace']).to be nil
1389
1394
  end
1390
1395
 
1391
1396
  it 'returns malformed attrs for improperly formatted json' do
1392
1397
  get :evaluate, params: { format: :json, attrs: '{' }
1393
1398
  expect(response.body).to match(/"error":"Malformed attrs"/)
1399
+ expect(json_response['backtrace']).to be nil
1394
1400
  end
1395
1401
 
1396
1402
  it 'returns malformed attrs if attr is not an array of strings' do
1397
1403
  get :evaluate, params: { format: :json, attrs: '{}' }
1398
1404
  expect(response.body).to match(/"error":"Malformed attrs"/)
1405
+ expect(json_response['backtrace']).to be nil
1399
1406
 
1400
1407
  get :evaluate, params: { format: :json, attrs: '[0]' }
1401
1408
  expect(response.body).to match(/"error":"Malformed attrs"/)
1409
+ expect(json_response['backtrace']).to be nil
1402
1410
  end
1403
1411
 
1404
1412
  it 'returns malformed params for improperly formatted json' do
1405
1413
  get :evaluate, params: { format: :json, attrs: 'e', params: '{' }
1406
1414
  expect(response.body).to match(/"error":"Malformed params"/)
1415
+ expect(json_response['backtrace']).to be nil
1407
1416
  end
1408
1417
 
1409
1418
  it 'returns malformed params if params is not a hash' do
1410
1419
  get :evaluate, params: { format: :json, attrs: 'e', params: '[0]' }
1411
1420
  expect(response.body).to match(/"error":"Malformed params"/)
1421
+ expect(json_response['backtrace']).to be nil
1412
1422
  end
1413
1423
 
1414
1424
  it 'returns engine/tag lookup error if script not found' do
1415
1425
  get :evaluate, params: { format: :json, script: 'M1', attrs: 'e', tag: 'invalid' }
1416
1426
  expect(response.body).to match(/bad tag identifier.*invalid/)
1427
+ expect(json_response['backtrace']).to be nil
1428
+
1417
1429
  get :evaluate, params: { format: :json, script: 'Invalid', attrs: 'e', tag: t1.name }
1418
1430
  expect(response.body).to match(/"error":"Can't get engine:/)
1431
+ expect(json_response['backtrace']).to be nil
1419
1432
  end
1420
1433
 
1421
1434
  it 'returns the script runtime error (no node specified)' do
1422
1435
  get :evaluate, params: { format: :json, script: 'M1', attrs: 'e', tag: t1.name }
1423
1436
  expect(response.body).to match(/"error":"bad node/)
1437
+ expect(json_response['backtrace']).to be nil
1424
1438
  end
1425
1439
  end
1426
1440
  end