marty 0.5.15 → 0.5.16

Sign up to get free protection for your applications and to get access to all the features.
Files changed (212) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +27 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +23 -0
  5. data/Gemfile +23 -0
  6. data/INDEPENDENCE_ISSUES.md +23 -0
  7. data/app/assets/images/marty/.gitkeep +0 -0
  8. data/app/components/marty/report_form.rb +9 -4
  9. data/gemini_deprecations.md +6 -0
  10. data/lib/marty/data_change.rb +99 -0
  11. data/lib/marty/data_conversion.rb +11 -3
  12. data/lib/marty/data_exporter.rb +9 -0
  13. data/lib/marty/version.rb +1 -1
  14. data/marty.gemspec +35 -0
  15. data/script/rails +8 -0
  16. data/spec/controllers/application_controller_spec.rb +52 -0
  17. data/spec/controllers/job_controller_spec.rb +226 -0
  18. data/spec/controllers/rpc_controller_spec.rb +379 -0
  19. data/spec/controllers/rpc_import_spec.rb +45 -0
  20. data/spec/dummy/README.rdoc +261 -0
  21. data/spec/dummy/Rakefile +7 -0
  22. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  23. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  24. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  25. data/spec/dummy/app/controllers/components_controller.rb +7 -0
  26. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  27. data/spec/dummy/app/mailers/.gitkeep +0 -0
  28. data/spec/dummy/app/models/.gitkeep +0 -0
  29. data/spec/dummy/app/models/gemini/amortization_type.rb +5 -0
  30. data/spec/dummy/app/models/gemini/bud_category.rb +7 -0
  31. data/spec/dummy/app/models/gemini/entity.rb +2 -0
  32. data/spec/dummy/app/models/gemini/extras/data_import.rb +5 -0
  33. data/spec/dummy/app/models/gemini/extras/settlement_import.rb +28 -0
  34. data/spec/dummy/app/models/gemini/fannie_bup.rb +29 -0
  35. data/spec/dummy/app/models/gemini/grouping.rb +8 -0
  36. data/spec/dummy/app/models/gemini/grouping_head_version.rb +14 -0
  37. data/spec/dummy/app/models/gemini/head.rb +7 -0
  38. data/spec/dummy/app/models/gemini/head_version.rb +14 -0
  39. data/spec/dummy/app/models/gemini/helper.rb +44 -0
  40. data/spec/dummy/app/models/gemini/loan_program.rb +11 -0
  41. data/spec/dummy/app/models/gemini/mortgage_type.rb +5 -0
  42. data/spec/dummy/app/models/gemini/simple.rb +6 -0
  43. data/spec/dummy/app/models/gemini/streamline_type.rb +16 -0
  44. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  45. data/spec/dummy/config.ru +4 -0
  46. data/spec/dummy/config/application.rb +82 -0
  47. data/spec/dummy/config/boot.rb +10 -0
  48. data/spec/dummy/config/database.yml.example +10 -0
  49. data/spec/dummy/config/environment.rb +5 -0
  50. data/spec/dummy/config/environments/development.rb +35 -0
  51. data/spec/dummy/config/environments/production.rb +69 -0
  52. data/spec/dummy/config/environments/test.rb +39 -0
  53. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  54. data/spec/dummy/config/initializers/delayed_job.rb +5 -0
  55. data/spec/dummy/config/initializers/inflections.rb +15 -0
  56. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  57. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  58. data/spec/dummy/config/initializers/session_store.rb +8 -0
  59. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  60. data/spec/dummy/config/locales/en.yml +5 -0
  61. data/spec/dummy/config/routes.rb +12 -0
  62. data/spec/dummy/db/migrate/20140801000000_create_groupings.rb +11 -0
  63. data/spec/dummy/db/migrate/20150406171536_create_categories.rb +27 -0
  64. data/spec/dummy/db/migrate/20150408200916_create_loan_programs.rb +26 -0
  65. data/spec/dummy/db/migrate/20150408201429_create_types.rb +21 -0
  66. data/spec/dummy/db/migrate/20150420000001_create_heads.rb +14 -0
  67. data/spec/dummy/db/migrate/20150420000002_create_head_versions.rb +15 -0
  68. data/spec/dummy/db/migrate/20150420000003_create_grouping_head_versions.rb +12 -0
  69. data/spec/dummy/db/migrate/20151023000001_create_simple.rb +12 -0
  70. data/spec/dummy/db/seeds.rb +8 -0
  71. data/spec/dummy/delorean/blame_report.dl +171 -0
  72. data/spec/dummy/delorean/data_report.dl +105 -0
  73. data/spec/dummy/delorean/fields.dl +52 -0
  74. data/spec/dummy/delorean/styles.dl +134 -0
  75. data/spec/dummy/lib/assets/.gitkeep +0 -0
  76. data/spec/dummy/lib/class_list.rb +3 -0
  77. data/spec/dummy/log/.gitkeep +0 -0
  78. data/spec/dummy/public/404.html +26 -0
  79. data/spec/dummy/public/422.html +26 -0
  80. data/spec/dummy/public/500.html +25 -0
  81. data/spec/dummy/public/favicon.ico +0 -0
  82. data/spec/dummy/public/icons/READ.txt +3 -0
  83. data/spec/dummy/public/icons/application_cascade.png +0 -0
  84. data/spec/dummy/public/icons/application_delete.png +0 -0
  85. data/spec/dummy/public/icons/application_put.png +0 -0
  86. data/spec/dummy/public/icons/application_view_detail.png +0 -0
  87. data/spec/dummy/public/icons/arrow_in.png +0 -0
  88. data/spec/dummy/public/icons/arrow_refresh.png +0 -0
  89. data/spec/dummy/public/icons/database_save.png +0 -0
  90. data/spec/dummy/public/icons/door_in.png +0 -0
  91. data/spec/dummy/public/icons/door_out.png +0 -0
  92. data/spec/dummy/public/icons/group.png +0 -0
  93. data/spec/dummy/public/icons/page_lightning.png +0 -0
  94. data/spec/dummy/public/icons/printer.png +0 -0
  95. data/spec/dummy/public/icons/report_disk.png +0 -0
  96. data/spec/dummy/public/icons/report_go.png +0 -0
  97. data/spec/dummy/public/icons/report_magnify.png +0 -0
  98. data/spec/dummy/public/icons/script.png +0 -0
  99. data/spec/dummy/public/icons/script_add.png +0 -0
  100. data/spec/dummy/public/icons/script_go.png +0 -0
  101. data/spec/dummy/public/icons/script_key.png +0 -0
  102. data/spec/dummy/public/icons/table_go.png +0 -0
  103. data/spec/dummy/public/icons/time.png +0 -0
  104. data/spec/dummy/public/icons/time_add.png +0 -0
  105. data/spec/dummy/public/icons/time_go.png +0 -0
  106. data/spec/dummy/public/icons/timeline_marker.png +0 -0
  107. data/spec/dummy/public/icons/user_add.png +0 -0
  108. data/spec/dummy/public/icons/user_delete.png +0 -0
  109. data/spec/dummy/public/icons/user_edit.png +0 -0
  110. data/spec/dummy/public/icons/wrench.png +0 -0
  111. data/spec/dummy/script/delayed_job +6 -0
  112. data/spec/dummy/script/rails +6 -0
  113. data/spec/features/javascripts/job_dashboard_live_search.js.coffee +8 -0
  114. data/spec/features/javascripts/login.js.coffee +8 -0
  115. data/spec/features/jobs_dashboard_netzke_spec.rb +24 -0
  116. data/spec/features/jobs_dashboard_spec.rb +49 -0
  117. data/spec/fixtures/scripts/load_tests/script1.dl +2 -0
  118. data/spec/fixtures/scripts/load_tests/script2.dl +2 -0
  119. data/spec/job_helper.rb +102 -0
  120. data/spec/lib/data_exporter_spec.rb +71 -0
  121. data/spec/lib/data_importer_spec.rb +461 -0
  122. data/spec/lib/xl_spec.rb +198 -0
  123. data/spec/lib/xl_styles_spec.rb +115 -0
  124. data/spec/models/api_auth_spec.rb +187 -0
  125. data/spec/models/posting_spec.rb +107 -0
  126. data/spec/models/promise_spec.rb +65 -0
  127. data/spec/models/script_spec.rb +187 -0
  128. data/spec/models/user_spec.rb +68 -0
  129. data/spec/requests/routes_spec.rb +12 -0
  130. data/spec/spec_helper.rb +61 -0
  131. data/spec/support/clean_db_helpers.rb +18 -0
  132. data/spec/support/delayed_job_helpers.rb +12 -0
  133. data/spec/support/user_helpers.rb +12 -0
  134. metadata +139 -89
  135. data/app/components/marty/auth_app.rb~ +0 -51
  136. data/app/components/marty/auth_app/javascripts/auth_app.js~ +0 -91
  137. data/app/components/marty/cm_form_panel.rb~ +0 -5
  138. data/app/components/marty/cm_grid_panel.rb~ +0 -35
  139. data/app/components/marty/data_import_view.rb~ +0 -142
  140. data/app/components/marty/extras/layout.rb~ +0 -46
  141. data/app/components/marty/live_search_grid_panel.rb~ +0 -49
  142. data/app/components/marty/main_auth_app.rb~ +0 -238
  143. data/app/components/marty/mcfly_grid_panel.rb~ +0 -80
  144. data/app/components/marty/new_posting_form.rb~ +0 -46
  145. data/app/components/marty/new_posting_window.rb~ +0 -21
  146. data/app/components/marty/pivot_grid.rb +0 -52
  147. data/app/components/marty/pivot_grid/endpoints.rb +0 -45
  148. data/app/components/marty/pivot_grid/javascripts/extensions.js +0 -150
  149. data/app/components/marty/pivot_grid/javascripts/pivot_grid.js +0 -86
  150. data/app/components/marty/pivot_grid/services.rb +0 -44
  151. data/app/components/marty/posting_grid.rb~ +0 -140
  152. data/app/components/marty/promise_view.rb~ +0 -157
  153. data/app/components/marty/promise_view/stylesheets/promise_view.css~ +0 -15
  154. data/app/components/marty/report_form.rb~ +0 -217
  155. data/app/components/marty/report_select.rb~ +0 -133
  156. data/app/components/marty/reporting.rb~ +0 -39
  157. data/app/components/marty/script_detail.rb~ +0 -430
  158. data/app/components/marty/script_form.rb~ +0 -233
  159. data/app/components/marty/script_form/javascripts/Ext.ux.form.field.CodeMirror.js~ +0 -909
  160. data/app/components/marty/script_grid.rb~ +0 -99
  161. data/app/components/marty/script_tester.rb~ +0 -213
  162. data/app/components/marty/scripting.rb~ +0 -124
  163. data/app/components/marty/select_report.rb~ +0 -143
  164. data/app/components/marty/simple_app.rb~ +0 -101
  165. data/app/components/marty/tag_grid.rb~ +0 -89
  166. data/app/components/marty/tree_panel.rb~ +0 -256
  167. data/app/components/marty/tree_panel/javascripts/tree_panel.js~ +0 -317
  168. data/app/components/marty/user_pivot.rb +0 -128
  169. data/app/components/marty/user_view.rb~ +0 -188
  170. data/app/controllers/marty/application_controller.rb~ +0 -133
  171. data/app/controllers/marty/components_controller.rb~ +0 -37
  172. data/app/controllers/marty/job_controller.rb~ +0 -28
  173. data/app/controllers/marty/rpc_controller.rb~ +0 -61
  174. data/app/helpers/marty/script_set.rb~ +0 -59
  175. data/app/models/marty/api_auth.rb~ +0 -48
  176. data/app/models/marty/data_change.rb~ +0 -141
  177. data/app/models/marty/enum.rb~ +0 -16
  178. data/app/models/marty/import_type.rb~ +0 -48
  179. data/app/models/marty/poop.rb~ +0 -169
  180. data/app/models/marty/posting.rb~ +0 -86
  181. data/app/models/marty/posting_type.rb~ +0 -21
  182. data/app/models/marty/promise.rb~ +0 -196
  183. data/app/models/marty/role.rb~ +0 -10
  184. data/app/models/marty/script.rb~ +0 -62
  185. data/app/models/marty/tag.rb~ +0 -91
  186. data/app/models/marty/user.rb~ +0 -148
  187. data/app/models/marty/user_role.rb~ +0 -13
  188. data/app/views/layouts/marty/application.html.erb~ +0 -11
  189. data/config/routes.rb~ +0 -10
  190. data/db/migrate/019_create_marty_postings.rb~ +0 -19
  191. data/db/migrate/095_create_marty_tags.rb~ +0 -19
  192. data/lib/marty.rb~ +0 -13
  193. data/lib/marty/content_handler.rb~ +0 -93
  194. data/lib/marty/data_exporter.rb~ +0 -137
  195. data/lib/marty/data_importer.rb~ +0 -114
  196. data/lib/marty/data_row_processor.rb~ +0 -206
  197. data/lib/marty/drop_folder_hook.rb~ +0 -17
  198. data/lib/marty/folder_hook.rb~ +0 -9
  199. data/lib/marty/lazy_column_loader.rb~ +0 -47
  200. data/lib/marty/mcfly_query.rb~ +0 -188
  201. data/lib/marty/migrations.rb~ +0 -65
  202. data/lib/marty/monkey.rb~ +0 -160
  203. data/lib/marty/permissions.rb~ +0 -69
  204. data/lib/marty/promise.rb~ +0 -41
  205. data/lib/marty/promise_job.rb~ +0 -121
  206. data/lib/marty/promise_proxy.rb~ +0 -69
  207. data/lib/marty/util.rb~ +0 -80
  208. data/lib/marty/version.rb~ +0 -3
  209. data/lib/marty/xl.rb~ +0 -526
  210. data/lib/pyxll/README.txt~ +0 -16
  211. data/lib/pyxll/gemini.py~ +0 -110
  212. data/lib/pyxll/pyxll.cfg~ +0 -12
@@ -1,137 +0,0 @@
1
- require 'base64'
2
- require 'zlib'
3
-
4
- class Marty::DataExporter
5
- # given an array of hashes, return set of all keys
6
- def self.hash_array_keys(hl)
7
- hl.each_with_object(Set.new) { |h, keys| keys.merge(h.keys) }
8
- end
9
-
10
- def self.hash_array_merge(hl, transpose)
11
- # given a list of hashes hl, generates a merged hash. The
12
- # resulting hash contains a superset of all the hash keys. The
13
- # values are corresponding values from each hash in hl.
14
- keys = hash_array_keys(hl)
15
-
16
- if transpose
17
- keys.each_with_object({}) { |k, rh|
18
- rh[k] = hl.map { |h| h[k] }
19
- }
20
- else
21
- [keys.to_a] + hl.map {|h| keys.map {|k| h[k]}}
22
- end
23
- end
24
-
25
- def self.encode_json(s)
26
- Base64.strict_encode64 Zlib.deflate(s)
27
- end
28
-
29
- def self.decode_json(s)
30
- Zlib.inflate Base64.strict_decode64(s)
31
- end
32
-
33
- def self.to_csv(obj, config=nil)
34
- obj = [obj] unless obj.respond_to? :map
35
-
36
- config ||= {}
37
-
38
- # if all array items are hashes, we merge them
39
- obj = hash_array_merge(obj, config["transpose"]) if
40
- obj.is_a?(Array) && obj.map {|x| x.is_a? Hash}.all?
41
-
42
- # symbolize config keys as expected by CSV.generate
43
- conf = config.each_with_object({}) { |(k,v), h|
44
- h[k.to_sym] = v unless k.to_s == "transpose"
45
- }
46
-
47
- # FIXME: very hacky to default row_sep to CRLF
48
- conf[:row_sep] ||= "\r\n"
49
-
50
- # FIXME: the following is ridiculously complex. We have different
51
- # data paths for hashes and arrays. Also, arrays can turn into
52
- # hashes is all their items are hashes! We map to complex objects
53
- # to JSON when inside hashes, but not arrays. Really need to
54
- # rethink this. Probably should have separate functions for
55
- # to_csv for hash and arrays.
56
-
57
- if obj.is_a? Hash
58
- csv_string = CSV.generate(conf) do |csv|
59
- obj.each do |x|
60
- csv << x.flatten(1).map(&:to_s)
61
- end
62
- end
63
- else
64
- csv_string = CSV.generate(conf) do |csv|
65
- obj.each do |x|
66
- x = [x] unless x.respond_to? :map
67
- csv << x.map { |v|
68
- v.is_a?(Array) || v.is_a?(Hash) ? encode_json(v.to_json) : v.to_s
69
- }
70
- end
71
- end
72
- end
73
- end
74
-
75
- def self.class_info(klass)
76
- @class_info ||= {}
77
-
78
- return @class_info[klass] if @class_info[klass]
79
-
80
- associations = klass.reflect_on_all_associations.map(&:name)
81
-
82
- @class_info[klass] = {
83
- cols:
84
- klass.columns.map(&:name) - Marty::DataRowProcessor::MCFLY_COLUMNS.to_a,
85
-
86
- assoc:
87
- associations.each_with_object({}) { |a, h|
88
- h["#{a}_id"] = Marty::DataRowProcessor.assoc_info(klass, a)
89
- },
90
- }
91
- end
92
-
93
- def self.export_attr(obj, c, info)
94
- v = obj.send(c.to_sym)
95
- assoc_info = info[:assoc][c] unless v.nil?
96
- return [v] unless assoc_info
97
-
98
- assoc_obj = assoc_info[:assoc_class].find(v)
99
-
100
- assoc_info[:assoc_keys].map {|k| assoc_obj.send(k.to_sym)}
101
- end
102
-
103
- def self.export_header_attr(c, info)
104
- assoc_info = info[:assoc][c]
105
- return c unless assoc_info
106
-
107
- # remove _id
108
- c = c[0..-4]
109
-
110
- assoc_keys = assoc_info[:assoc_keys]
111
-
112
- # FIXME: this doesn't work if k is also an association. Needs to
113
- # be recursive.
114
- assoc_keys.length > 1 ? assoc_keys.map {|k| "#{c}__#{k}"} : c
115
- end
116
-
117
- # Given a Mcfly klass, generate an export array. Can potentially
118
- # use up a lot of memory if the result set is large.
119
- def self.do_export(ts, klass, sort_field=nil)
120
- info = class_info(klass)
121
-
122
- # strip _id from assoc fields
123
- header = [ info[:cols].map {|c| export_header_attr(c, info)}.flatten(1) ]
124
-
125
- query = klass
126
-
127
- # is it Mcfly?
128
- if (klass.const_get(:MCFLY_UNIQUENESS) rescue nil)
129
- ts = Mcfly.normalize_infinity(ts)
130
- query = query.where("obsoleted_dt >= ? AND created_dt < ?", ts, ts)
131
- end
132
-
133
- header + query.
134
- order(sort_field || :id).
135
- map {|obj| info[:cols].map {|c| export_attr(obj, c, info)}.flatten(1)}
136
- end
137
- end
@@ -1,114 +0,0 @@
1
- module Marty
2
- class DataImporterError < StandardError
3
- attr_reader :lines
4
-
5
- def initialize(message, lines)
6
- super(message)
7
- @lines = lines
8
- end
9
- end
10
-
11
- class DataImporter
12
- # perform cleaning and do_import and summarize its results
13
- def self.do_import_summary(klass,
14
- data,
15
- dt='infinity',
16
- cleaner_function=nil,
17
- validation_function=nil,
18
- col_sep="\t",
19
- allow_dups=false
20
- )
21
-
22
- recs = self.do_import(klass,
23
- data,
24
- dt,
25
- cleaner_function,
26
- validation_function,
27
- col_sep,
28
- allow_dups,
29
- )
30
-
31
- recs.each_with_object(Hash.new(0)) {|(op, id), h|
32
- h[op] += 1
33
- }
34
- end
35
-
36
- # Given a Mcfly klass and CSV data, import data into the database
37
- # and report on affected rows. Result is an array of tuples.
38
- # Each tuple is associated with one data row and looks like [tag,
39
- # id]. Tag is one of :same, :update, :create and "id" is the id
40
- # of the affected row.
41
- def self.do_import(klass,
42
- data,
43
- dt='infinity',
44
- cleaner_function=nil,
45
- validation_function=nil,
46
- col_sep="\t",
47
- allow_dups=false
48
- )
49
-
50
- parsed = data.is_a?(Array) ? data :
51
- CSV.new(data, headers: true, col_sep: col_sep)
52
-
53
- klass.transaction do
54
- cleaner_ids = cleaner_function ? klass.send(cleaner_function.to_sym) :
55
- []
56
-
57
- raise "bad cleaner function result" unless
58
- cleaner_ids.all? {|id| id.is_a?(Fixnum) }
59
-
60
- row_proc = nil
61
- eline = 0
62
-
63
- begin
64
- res = parsed.each_with_index.map { |row, line|
65
- eline = line
66
-
67
- row_proc ||= Marty::DataRowProcessor.
68
- new(klass,
69
- row.respond_to?(:headers) ? row.headers : row.keys,
70
- dt,
71
- )
72
- # skip lines which are all nil
73
- next :blank if row.to_hash.values.none?
74
-
75
- row_proc.create_or_update(row)
76
- }
77
- rescue => exc
78
- # to find problems with the importer, comment out the rescue block
79
- raise Marty::DataImporterError.new(exc.to_s, [eline])
80
- end
81
-
82
- ids = {}
83
-
84
- # raise an error if record referenced more than once.
85
- res.each_with_index do
86
- |(op, id), line|
87
- raise Marty::DataImporterError.
88
- new("record referenced more than once", [ids[id], line]) if
89
- op != :blank && ids.member?(id) && !allow_dups
90
-
91
- ids[id] = line
92
- end
93
-
94
- begin
95
- # Validate affected rows if necessary
96
- klass.send(validation_function.to_sym, ids.keys) if
97
- validation_function
98
- rescue => exc
99
- raise Marty::DataImporterError.new(exc.to_s, [])
100
- end
101
-
102
- remainder_ids = cleaner_ids - ids.keys
103
-
104
- raise Marty::DataImporterError.
105
- new("Missing import data. " +
106
- "Please provide header line and at least one data line.", [1]) if
107
- ids.keys.compact.count == 0
108
-
109
- klass.delete(remainder_ids)
110
- res + remainder_ids.map {|id| [:clean, id]}
111
- end
112
- end
113
- end
114
- end
@@ -1,206 +0,0 @@
1
- class Marty::DataRowProcessor
2
- attr_accessor :klass, :headers, :dt, :key_attrs, :hmap
3
-
4
- EXCEL_START_DATE = Date.parse('1/1/1900')-2
5
-
6
- MCFLY_COLUMNS = Set[
7
- "id",
8
- "group_id",
9
- "user_id",
10
- "created_dt",
11
- "obsoleted_dt",
12
- "o_user_id",
13
- ]
14
-
15
- # Given a Mcfly class, return the set of attributes (excluding id)
16
- # used to uniquely identify an instance.
17
- def self.get_keys(klass)
18
- raise "bad class arg #{klass}" unless
19
- klass.is_a?(Class) && klass < ActiveRecord::Base
20
-
21
- attrs = klass.const_get(:MCFLY_UNIQUENESS)
22
-
23
- raise "class has no :MCFLY_UNIQUENESS" unless attrs
24
-
25
- attrs = attrs[0..-2] + attrs.last.fetch(:scope, []) if
26
- attrs.last.is_a?(Hash)
27
- attrs -= [:obsoleted_dt]
28
-
29
- raise "key list for #{klass} is empty" if attrs.empty?
30
- attrs
31
- end
32
-
33
- def self.assoc_info(klass, a)
34
- assoc_class = klass.reflect_on_association(a.to_sym).klass
35
- keys = self.get_keys(assoc_class) rescue nil
36
-
37
- assoc_keys = keys || [assoc_class.attribute_names.reject{|x| x=="id"}.first]
38
-
39
- {assoc_keys: assoc_keys, assoc_class: assoc_class, mcfly: keys}
40
- end
41
-
42
- FLOAT_PAT = /^-?\d+(\.\d+)?$/
43
-
44
- PATS = {
45
- integer: /^-?\d+(\.0+)?$/,
46
- float: FLOAT_PAT,
47
- decimal: FLOAT_PAT,
48
- }
49
-
50
- def convert(v, type)
51
- pat = PATS[type]
52
-
53
- raise "bad #{type} #{v.inspect}" if
54
- v.is_a?(String) && pat && !(v =~ pat)
55
-
56
- case type
57
- when :boolean
58
- case v.downcase
59
- when "true" then true
60
- when "false" then false
61
- else raise "unknown boolean #{v}"
62
- end
63
- when :string, :text
64
- v
65
- when :integer
66
- v.to_i
67
- when :float
68
- v.to_f
69
- when :decimal
70
- v.to_d
71
- when :date
72
- # Dates are kept as float in Google spreadsheets. Need to
73
- # convert them to dates. FIXME: 'infinity' as a date in
74
- # Rails 3.2 appears to be broken. Setting a date field to
75
- # 'infinity' sets it to nil.
76
- v =~ FLOAT_PAT ? EXCEL_START_DATE + v.to_f :
77
- Mcfly.is_infinity(v) ? 'infinity' : v.to_date
78
- when :datetime
79
- Mcfly.is_infinity(v) ? 'infinity' : v.to_datetime
80
- when :numrange, :int4range, :int8range
81
- v.to_s
82
- when :float_array, :json
83
- JSON.parse Marty::DataExporter.decode_json(v)
84
- else
85
- raise "unknown type #{type} for #{v}"
86
- end
87
- end
88
-
89
- def initialize(klass, headers, dt)
90
- @klass = klass
91
- @headers = headers
92
- @dt = dt
93
- @key_attrs = self.class.get_keys(klass)
94
-
95
- # # HACK: not sure why there's a nil at the end of headers sometimes
96
- # headers.pop if headers[-1].nil?
97
-
98
- raise "row headers have nil! #{headers.inspect}" unless headers.all?
99
-
100
- associations = klass.reflect_on_all_associations.map(&:name)
101
-
102
- cols = klass.columns.each_with_object({}) { |c, h|
103
- h[c.name] = c
104
- }
105
-
106
- @hmap = headers.each_with_object({}) do
107
- |a, h|
108
- # handle klass__attr type headers generated by Netzke. Just
109
- # keeps klass since we should be able to find the key attr.
110
- aclass = a.split('__').first
111
-
112
- if associations.member?(aclass.to_sym)
113
- h[a] = self.class.assoc_info(klass, aclass)
114
- next
115
- end
116
-
117
- raise "unknown column #{a}" unless cols[a]
118
-
119
- # for JSON fields in Rails 3.x type is nil, so use sql_type
120
- type = cols[a].type || cols[a].sql_type
121
- type = "#{type}_array" if cols[a].array
122
- h[a] = type.to_sym
123
- end
124
- end
125
-
126
- def create_or_update(row)
127
- assoc_options = {}
128
-
129
- options = row.each_with_object({}) do |(a, v), h|
130
- # ignore Mcfly columns
131
- next if MCFLY_COLUMNS.member? a
132
-
133
- if hmap[a].is_a?(Hash)
134
- assoc_options[a] = v
135
- else
136
- raise "bad col #{a} value #{v}, row: #{row}" unless hmap[a]
137
-
138
- # if it's not a hash (association) then its a type symbol
139
- h[a] = v && convert(v, hmap[a])
140
- end
141
- end
142
-
143
- assoc_groups = assoc_options.keys.group_by {|x| x.split('__').first}
144
-
145
- assoc_groups.each do |aclass, attrs|
146
- # if group has only one attr and the attr is nil or AR obj, then
147
- # we don't need to search.
148
- if attrs.length == 1
149
- a = attrs.first
150
- v = assoc_options[a]
151
-
152
- if v.nil? || v.is_a?(ActiveRecord::Base)
153
- options["#{aclass}_id"] = v && v.id
154
- next
155
- end
156
- end
157
-
158
- srch = attrs.each_with_object({}) do |a, h|
159
- v = assoc_options[a]
160
- ac, aa = a.split('__')
161
-
162
- aa ||= hmap[a][:assoc_keys].first
163
-
164
- h[aa] = v
165
- h[:obsoleted_dt] = 'infinity' if hmap[a][:mcfly]
166
- end
167
-
168
- srch_class = hmap[attrs.first][:assoc_class]
169
-
170
- av = srch_class.where(srch).first
171
-
172
- raise "#{aclass} not found #{srch}" unless av
173
-
174
- options[ "#{aclass}_id" ] = av.id
175
- end
176
-
177
- find_options = options.select { |k,v| key_attrs.member? k.to_sym }
178
-
179
- raise "invalid entry" if find_options.empty?
180
-
181
- find_options['obsoleted_dt'] = 'infinity'
182
-
183
- obj = klass.where(find_options).first || klass.new
184
-
185
- options.each do
186
- |k, v|
187
- # For each attr, check to see if it's begin changed before
188
- # setting it. The AR obj.changed? doesn't work properly
189
- # with array, JSON or lazy attrs.
190
- obj.send("#{k}=", v) if obj.send(k) != v
191
- end
192
-
193
- # FIXME: obj.changed? doesn't work properly for timestamp
194
- # fields in Rails 3.2. It evaluates to true even when datetime
195
- # is not changed. Caused by lack of awareness of timezones.
196
- tag = obj.new_record? ? :create : (obj.changed? ? :update : :same)
197
-
198
- raise "old created_dt >= current #{obj} #{obj.created_dt} #{dt}" if
199
- (tag == :update) && !Mcfly.is_infinity(dt) && (obj.created_dt > dt)
200
-
201
- obj.created_dt = dt unless tag == :same || Mcfly.is_infinity(dt)
202
- obj.save!
203
-
204
- [tag, obj.id]
205
- end
206
- end