gooddata 0.6.0 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (208) hide show
  1. checksums.yaml +13 -5
  2. data/.rubocop.yml +23 -0
  3. data/.travis.yml +9 -4
  4. data/CLI.md +439 -0
  5. data/Gemfile +0 -1
  6. data/README.md +2 -2
  7. data/Rakefile +60 -8
  8. data/doc/templates/default/module/setup.rb +1 -1
  9. data/examples.rb +2 -0
  10. data/gooddata +2 -0
  11. data/gooddata.gemspec +12 -8
  12. data/lib/gooddata.rb +0 -2
  13. data/lib/gooddata/bricks/base_downloader.rb +52 -47
  14. data/lib/gooddata/bricks/brick.rb +20 -31
  15. data/lib/gooddata/bricks/bricks.rb +1 -1
  16. data/lib/gooddata/bricks/middleware/base_middleware.rb +9 -7
  17. data/lib/gooddata/bricks/middleware/bench_middleware.rb +12 -10
  18. data/lib/gooddata/bricks/middleware/bulk_salesforce_middleware.rb +28 -28
  19. data/lib/gooddata/bricks/middleware/fs_upload_middleware.rb +20 -16
  20. data/lib/gooddata/bricks/middleware/gooddata_middleware.rb +21 -19
  21. data/lib/gooddata/bricks/middleware/logger_middleware.rb +10 -8
  22. data/lib/gooddata/bricks/middleware/restforce_middleware.rb +36 -34
  23. data/lib/gooddata/bricks/middleware/stdout_middleware.rb +11 -9
  24. data/lib/gooddata/bricks/middleware/twitter_middleware.rb +14 -12
  25. data/lib/gooddata/bricks/pipeline.rb +28 -0
  26. data/lib/gooddata/bricks/utils.rb +10 -8
  27. data/lib/gooddata/cli/cli.rb +1 -6
  28. data/lib/gooddata/cli/commands/auth_cmd.rb +1 -1
  29. data/lib/gooddata/cli/commands/console_cmd.rb +7 -5
  30. data/lib/gooddata/cli/commands/domain_cmd.rb +45 -0
  31. data/lib/gooddata/cli/commands/process_cmd.rb +42 -5
  32. data/lib/gooddata/cli/commands/project_cmd.rb +96 -36
  33. data/lib/gooddata/cli/commands/projects_cmd.rb +21 -0
  34. data/lib/gooddata/cli/commands/role_cmd.rb +28 -0
  35. data/lib/gooddata/cli/commands/run_ruby_cmd.rb +5 -5
  36. data/lib/gooddata/cli/commands/scaffold_cmd.rb +1 -1
  37. data/lib/gooddata/cli/commands/{profile_cmd.rb → user_cmd.rb} +7 -9
  38. data/lib/gooddata/cli/shared.rb +3 -2
  39. data/lib/gooddata/client.rb +16 -304
  40. data/lib/gooddata/commands/api.rb +13 -5
  41. data/lib/gooddata/commands/auth.rb +47 -40
  42. data/lib/gooddata/commands/base.rb +4 -2
  43. data/lib/gooddata/commands/commands.rb +1 -1
  44. data/lib/gooddata/commands/datasets.rb +20 -7
  45. data/lib/gooddata/commands/domain.rb +23 -0
  46. data/lib/gooddata/commands/process.rb +23 -117
  47. data/lib/gooddata/commands/project.rb +147 -0
  48. data/lib/gooddata/commands/projects.rb +8 -102
  49. data/lib/gooddata/commands/role.rb +26 -0
  50. data/lib/gooddata/commands/runners.rb +41 -38
  51. data/lib/gooddata/commands/scaffold.rb +46 -43
  52. data/lib/gooddata/commands/user.rb +33 -0
  53. data/lib/gooddata/connection.rb +43 -353
  54. data/lib/gooddata/core/connection.rb +389 -0
  55. data/lib/gooddata/core/core.rb +5 -4
  56. data/lib/gooddata/core/logging.rb +48 -0
  57. data/lib/gooddata/core/nil_logger.rb +13 -0
  58. data/lib/gooddata/core/project.rb +70 -0
  59. data/lib/gooddata/core/rest.rb +120 -0
  60. data/lib/gooddata/core/threaded.rb +14 -0
  61. data/lib/gooddata/core/user.rb +19 -0
  62. data/lib/gooddata/data/data.rb +2 -1
  63. data/lib/gooddata/data/guesser.rb +16 -12
  64. data/lib/gooddata/exceptions/command_failed.rb +1 -1
  65. data/lib/gooddata/exceptions/exceptions.rb +2 -1
  66. data/lib/gooddata/exceptions/no_project_error.rb +11 -0
  67. data/lib/gooddata/exceptions/project_not_found.rb +1 -1
  68. data/lib/gooddata/extensions/big_decimal.rb +6 -2
  69. data/lib/gooddata/extract.rb +10 -8
  70. data/lib/gooddata/goodzilla/goodzilla.rb +61 -59
  71. data/lib/gooddata/helpers.rb +15 -9
  72. data/lib/gooddata/models/account_settings.rb +124 -0
  73. data/lib/gooddata/models/attributes/anchor.rb +37 -0
  74. data/lib/gooddata/models/attributes/attributes.rb +8 -0
  75. data/lib/gooddata/models/attributes/date_attribute.rb +25 -0
  76. data/lib/gooddata/models/attributes/time_attribute.rb +24 -0
  77. data/lib/gooddata/models/columns/attribute.rb +71 -0
  78. data/lib/gooddata/models/columns/columns.rb +8 -0
  79. data/lib/gooddata/models/columns/date_column.rb +63 -0
  80. data/lib/gooddata/models/columns/fact_model.rb +54 -0
  81. data/lib/gooddata/models/columns/label.rb +55 -0
  82. data/lib/gooddata/models/columns/reference.rb +57 -0
  83. data/lib/gooddata/models/dashboard_builder.rb +26 -0
  84. data/lib/gooddata/models/data_result.rb +10 -9
  85. data/lib/gooddata/models/domain.rb +131 -0
  86. data/lib/gooddata/models/empty_result.rb +5 -8
  87. data/lib/gooddata/models/facts/facts.rb +8 -0
  88. data/lib/gooddata/models/facts/time_fact.rb +20 -0
  89. data/lib/gooddata/models/folders/attribute_folder.rb +20 -0
  90. data/lib/gooddata/models/folders/fact_folder.rb +20 -0
  91. data/lib/gooddata/models/folders/folders.rb +8 -0
  92. data/lib/gooddata/models/invitation.rb +78 -0
  93. data/lib/gooddata/models/links.rb +6 -6
  94. data/lib/gooddata/models/md_object.rb +25 -0
  95. data/lib/gooddata/models/metadata.rb +160 -62
  96. data/lib/gooddata/models/metadata/attribute.rb +81 -0
  97. data/lib/gooddata/models/metadata/column.rb +61 -0
  98. data/lib/gooddata/models/{dashboard.rb → metadata/dashboard.rb} +12 -7
  99. data/lib/gooddata/models/{data_set.rb → metadata/data_set.rb} +5 -4
  100. data/lib/gooddata/models/metadata/date_dimension.rb +26 -0
  101. data/lib/gooddata/models/metadata/display_form.rb +61 -0
  102. data/lib/gooddata/models/metadata/fact.rb +36 -0
  103. data/lib/gooddata/models/metadata/folder.rb +24 -0
  104. data/lib/gooddata/models/metadata/metadata.rb +8 -0
  105. data/lib/gooddata/models/metadata/metric.rb +197 -0
  106. data/lib/gooddata/models/metadata/report.rb +115 -0
  107. data/lib/gooddata/models/{report_definition.rb → metadata/report_definition.rb} +16 -10
  108. data/lib/gooddata/models/metadata/schema.rb +227 -0
  109. data/lib/gooddata/models/model.rb +38 -1339
  110. data/lib/gooddata/models/models.rb +5 -2
  111. data/lib/gooddata/models/module_constants.rb +29 -0
  112. data/lib/gooddata/models/process.rb +142 -13
  113. data/lib/gooddata/models/profile.rb +4 -6
  114. data/lib/gooddata/models/project.rb +406 -136
  115. data/lib/gooddata/models/project_blueprint.rb +221 -0
  116. data/lib/gooddata/models/project_builder.rb +136 -0
  117. data/lib/gooddata/models/project_creator.rb +138 -0
  118. data/lib/gooddata/models/project_metadata.rb +11 -10
  119. data/lib/gooddata/models/project_role.rb +92 -0
  120. data/lib/gooddata/models/references/date_reference.rb +44 -0
  121. data/lib/gooddata/models/references/references.rb +8 -0
  122. data/lib/gooddata/models/references/time_reference.rb +13 -0
  123. data/lib/gooddata/models/report_data_result.rb +11 -11
  124. data/lib/gooddata/models/schedule.rb +284 -0
  125. data/lib/gooddata/models/schema_blueprint.rb +158 -0
  126. data/lib/gooddata/models/schema_builder.rb +81 -0
  127. data/lib/gooddata/models/tab_builder.rb +23 -0
  128. data/lib/gooddata/models/user.rb +165 -0
  129. data/lib/gooddata/version.rb +1 -1
  130. data/lib/templates/project/data/devs.csv +1 -1
  131. data/lib/templates/project/data/repos.csv +1 -1
  132. data/lib/templates/project/model/model.rb.erb +7 -11
  133. data/spec/bricks/bricks_spec.rb +2 -0
  134. data/spec/data/test-ci-data.csv +2 -0
  135. data/spec/data/test_project_model_spec.json +7 -27
  136. data/spec/helpers/blueprint_helper.rb +2 -0
  137. data/spec/helpers/cli_helper.rb +2 -0
  138. data/spec/helpers/connection_helper.rb +14 -1
  139. data/spec/helpers/project_helper.rb +16 -0
  140. data/spec/helpers/schema_helper.rb +16 -0
  141. data/spec/integration/command_projects_spec.rb +7 -7
  142. data/spec/integration/create_from_template_spec.rb +2 -2
  143. data/spec/integration/full_project_spec.rb +160 -7
  144. data/spec/integration/partial_md_export_import_spec.rb +3 -3
  145. data/spec/logging_in_logging_out_spec.rb +2 -1
  146. data/spec/spec_helper.rb +26 -4
  147. data/spec/unit/bricks/bricks_spec.rb +15 -7
  148. data/spec/unit/bricks/middleware/bench_middleware_spec.rb +2 -0
  149. data/spec/unit/bricks/middleware/bulk_salesforce_middleware_spec.rb +2 -0
  150. data/spec/unit/bricks/middleware/gooddata_middleware_spec.rb +2 -0
  151. data/spec/unit/bricks/middleware/logger_middleware_spec.rb +2 -0
  152. data/spec/unit/bricks/middleware/restforce_middleware_spec.rb +2 -0
  153. data/spec/unit/bricks/middleware/stdout_middleware_spec.rb +2 -0
  154. data/spec/unit/bricks/middleware/twitter_middleware_spec.rb +2 -0
  155. data/spec/unit/cli/cli_spec.rb +2 -0
  156. data/spec/unit/cli/commands/cmd_api_spec.rb +23 -15
  157. data/spec/unit/cli/commands/cmd_auth_spec.rb +8 -4
  158. data/spec/unit/cli/commands/cmd_domain_spec.rb +82 -0
  159. data/spec/unit/cli/commands/cmd_process_spec.rb +29 -13
  160. data/spec/unit/cli/commands/cmd_project_spec.rb +51 -30
  161. data/spec/unit/cli/commands/cmd_role_spec.rb +44 -0
  162. data/spec/unit/cli/commands/cmd_run_ruby_spec.rb +8 -4
  163. data/spec/unit/cli/commands/cmd_scaffold_spec.rb +48 -11
  164. data/spec/unit/cli/commands/cmd_user_spec.rb +29 -0
  165. data/spec/unit/commands/command_api_spec.rb +1 -1
  166. data/spec/unit/commands/command_auth_spec.rb +100 -18
  167. data/spec/unit/commands/command_dataset_spec.rb +4 -0
  168. data/spec/unit/commands/command_process_spec.rb +9 -4
  169. data/spec/unit/commands/command_projects_spec.rb +10 -6
  170. data/spec/unit/commands/command_scaffold_spec.rb +5 -1
  171. data/spec/unit/commands/command_user_spec.rb +22 -0
  172. data/spec/unit/core/connection_spec.rb +35 -6
  173. data/spec/unit/core/logging_spec.rb +65 -0
  174. data/spec/unit/core/nil_logger_spec.rb +9 -0
  175. data/spec/unit/core/project_spec.rb +51 -0
  176. data/spec/unit/core/rest_spec.rb +33 -0
  177. data/spec/unit/data/guesser_spec.rb +5 -0
  178. data/spec/unit/godzilla/goodzilla_spec.rb +2 -0
  179. data/spec/unit/models/account_settings_spec.rb +28 -0
  180. data/spec/unit/models/anchor_spec.rb +32 -0
  181. data/spec/unit/models/attribute_column_spec.rb +7 -0
  182. data/spec/unit/models/domain_spec.rb +45 -0
  183. data/spec/unit/models/invitation_spec.rb +13 -0
  184. data/spec/unit/models/md_object_spec.rb +47 -0
  185. data/spec/unit/models/metric.rb +92 -0
  186. data/spec/unit/{model → models}/model_spec.rb +9 -7
  187. data/spec/unit/models/project_blueprint_spec.rb +202 -0
  188. data/spec/unit/models/project_creator.rb +73 -0
  189. data/spec/unit/models/project_role_spec.rb +90 -0
  190. data/spec/unit/models/project_spec.rb +143 -0
  191. data/spec/unit/models/schedule_spec.rb +491 -0
  192. data/spec/unit/{model → models}/schema_builder_spec.rb +2 -0
  193. data/spec/unit/{model → models}/tools_spec.rb +13 -7
  194. data/spec/unit/models/user_spec.rb +16 -0
  195. data/test/test_upload.rb +2 -0
  196. metadata +189 -86
  197. data/lib/gooddata/commands/profile.rb +0 -11
  198. data/lib/gooddata/models/attribute.rb +0 -29
  199. data/lib/gooddata/models/display_form.rb +0 -9
  200. data/lib/gooddata/models/fact.rb +0 -19
  201. data/lib/gooddata/models/metric.rb +0 -99
  202. data/lib/gooddata/models/report.rb +0 -89
  203. data/spec/data/blueprint_valid.json +0 -37
  204. data/spec/unit/cli/commands/cmd_profile_spec.rb +0 -16
  205. data/spec/unit/commands/command_profile_spec.rb +0 -18
  206. data/spec/unit/core/core_spec.rb +0 -7
  207. data/spec/unit/model/blueprint_spec.rb +0 -132
  208. data/spec/unit/model/project_blueprint_spec.rb +0 -44
@@ -0,0 +1,115 @@
1
+ # encoding: UTF-8
2
+
3
+ require_relative '../metadata'
4
+ require_relative 'metadata'
5
+
6
+ module GoodData
7
+ class Report < GoodData::MdObject
8
+ root_key :report
9
+
10
+ class << self
11
+ def [](id, options = {})
12
+ if id == :all
13
+ fail 'You have to specify a project ID' if GoodData.project.nil?
14
+ uri = GoodData.project.md['query'] + '/reports/'
15
+ GoodData.get(uri)['query']['entries']
16
+ else
17
+ super
18
+ end
19
+ end
20
+
21
+ def create(options = {})
22
+ title = options[:title]
23
+ summary = options[:summary] || ''
24
+ rd = options[:rd] || ReportDefinition.create(:top => options[:top], :left => options[:left])
25
+ rd.save
26
+
27
+ report = {
28
+ 'report' => {
29
+ 'content' => {
30
+ 'domains' => [],
31
+ 'definitions' => [rd.uri]
32
+ },
33
+ 'meta' => {
34
+ 'tags' => '',
35
+ 'deprecated' => '0',
36
+ 'summary' => summary,
37
+ 'title' => title
38
+ }
39
+ }
40
+ }
41
+ # TODO: write test for report definitions with explicit identifiers
42
+ report['report']['meta']['identifier'] = options[:identifier] if options[:identifier]
43
+ Report.new report
44
+ end
45
+ end
46
+
47
+ def results
48
+ content['results']
49
+ end
50
+
51
+ def definitions
52
+ content['definitions']
53
+ end
54
+
55
+ def latest_report_definition_uri
56
+ definitions.last
57
+ end
58
+
59
+ def latest_report_definition
60
+ GoodData::MdObject[latest_report_definition_uri]
61
+ end
62
+
63
+ def remove_definition(definition)
64
+ def_uri = is_a?(GoodData::ReportDefinition) ? definition.uri : definition
65
+ content['definitions'] = definitions.reject { |x| x == def_uri }
66
+ self
67
+ end
68
+
69
+ # TODO: Cover with test. You would probably need something that will be able to create a report easily from a definition
70
+ def remove_definition_but_latest
71
+ to_remove = definitions - [latest_report_definition_uri]
72
+ to_remove.each do |uri|
73
+ remove_definition(uri)
74
+ end
75
+ self
76
+ end
77
+
78
+ def purge_report_of_unused_definitions!
79
+ full_list = definitions
80
+ remove_definition_but_latest
81
+ purged_list = definitions
82
+ to_remove = full_list - purged_list
83
+ save
84
+ to_remove.each { |uri| GoodData.delete(uri) }
85
+ self
86
+ end
87
+
88
+ def execute
89
+ fail 'You have to save the report before executing. If you do not want to do that please use GoodData::ReportDefinition' unless saved?
90
+ result = GoodData.post '/gdc/xtab2/executor3', 'report_req' => { 'report' => uri }
91
+ data_result_uri = result['execResult']['dataResult']
92
+ result = GoodData.get data_result_uri
93
+ while result['taskState'] && result['taskState']['status'] == 'WAIT'
94
+ sleep 10
95
+ result = GoodData.get data_result_uri
96
+ end
97
+ ReportDataResult.new(GoodData.get data_result_uri)
98
+ end
99
+
100
+ def exportable?
101
+ true
102
+ end
103
+
104
+ def export(format)
105
+ result = GoodData.post('/gdc/xtab2/executor3', 'report_req' => { 'report' => uri })
106
+ result1 = GoodData.post('/gdc/exporter/executor', :result_req => { :format => format, :result => result })
107
+ png = GoodData.get(result1['uri'], :process => false)
108
+ while png.code == 202
109
+ sleep(1)
110
+ png = GoodData.get(result1['uri'], :process => false)
111
+ end
112
+ png
113
+ end
114
+ end
115
+ end
@@ -1,5 +1,6 @@
1
1
  # encoding: UTF-8
2
2
 
3
+ require_relative '../metadata'
3
4
  require_relative 'metadata'
4
5
 
5
6
  # GoodData Module
@@ -10,7 +11,7 @@ module GoodData
10
11
  root_key :reportDefinition
11
12
 
12
13
  class << self
13
- def [](id)
14
+ def [](id, options = {})
14
15
  if id == :all
15
16
  uri = GoodData.project.md['query'] + '/reportdefinition/'
16
17
  result = GoodData.get(uri)
@@ -22,7 +23,7 @@ module GoodData
22
23
 
23
24
  def create_metrics_part(left, top)
24
25
  stuff = Array(left) + Array(top)
25
- stuff.select { |item| item.respond_to?(:is_metric?) }.map do |metric|
26
+ stuff.select { |item| item.respond_to?(:metric?) && item.metric? }.map do |metric|
26
27
  create_metric_part(metric)
27
28
  end
28
29
  end
@@ -47,14 +48,14 @@ module GoodData
47
48
  def create_part(stuff)
48
49
  stuff = Array(stuff)
49
50
  parts = stuff.reduce([]) do |memo, item|
50
- if item.respond_to?(:is_metric?)
51
+ if item.respond_to?(:metric?) && item.metric?
51
52
  memo
52
53
  else
53
54
  memo << create_attribute_part(item)
54
55
  end
55
56
  memo
56
57
  end
57
- if stuff.any? { |item| item.respond_to?(:is_metric?) }
58
+ if stuff.any? { |item| item.respond_to?(:metric?) && item.metric? }
58
59
  parts << 'metricGroup'
59
60
  end
60
61
  parts
@@ -62,7 +63,7 @@ module GoodData
62
63
 
63
64
  def find(stuff)
64
65
  stuff.map do |item|
65
- if item.respond_to?(:is_attribute?)
66
+ if item.respond_to?(:attribute?) && item.attribute?
66
67
  item.display_forms.first
67
68
  elsif item.is_a?(String)
68
69
  x = GoodData::MdObject.get_by_id(item)
@@ -112,7 +113,7 @@ module GoodData
112
113
  left = Array(options[:left])
113
114
  top = Array(options[:top])
114
115
 
115
- metrics = (left + top).select { |item| item.respond_to?(:is_metric?) }
116
+ metrics = (left + top).select { |item| item.respond_to?(:metric?) && item.metric? }
116
117
 
117
118
  unsaved_metrics = metrics.reject { |i| i.saved? }
118
119
  unsaved_metrics.each { |m| m.title = 'Untitled metric' unless m.title }
@@ -120,7 +121,7 @@ module GoodData
120
121
  begin
121
122
  unsaved_metrics.each { |m| m.save }
122
123
  rd = GoodData::ReportDefinition.create(options)
123
- get_data_result(execute_inline(rd))
124
+ data_result(execute_inline(rd))
124
125
  ensure
125
126
  unsaved_metrics.each { |m| m.delete if m && m.saved? }
126
127
  end
@@ -140,7 +141,11 @@ module GoodData
140
141
  GoodData.post(uri, data)
141
142
  end
142
143
 
143
- def get_data_result(result)
144
+ # TODO: refactor the method. It should be instance method
145
+ # Method used for getting a data_result from a wire representation of
146
+ # @param result [Hash, Object] Wire data from JSON
147
+ # @return [GoodData::ReportDataResult]
148
+ def data_result(result)
144
149
  data_result_uri = result['execResult']['dataResult']
145
150
  result = GoodData.get data_result_uri
146
151
 
@@ -186,6 +191,8 @@ module GoodData
186
191
  }
187
192
  }
188
193
  }
194
+ # TODO: write test for report definitions with explicit identifiers
195
+ pars['reportDefinition']['meta']['identifier'] = options[:identifier] if options[:identifier]
189
196
 
190
197
  ReportDefinition.new(pars)
191
198
  end
@@ -204,8 +211,7 @@ module GoodData
204
211
  else
205
212
  ReportDefinition.execute_inline(self)
206
213
  end
207
-
208
- get_data_result(result)
214
+ ReportDefinition.data_result(result)
209
215
  end
210
216
  end
211
217
  end
@@ -0,0 +1,227 @@
1
+ # encoding: UTF-8
2
+
3
+ require_relative '../../helpers'
4
+
5
+ require_relative '../attributes/anchor'
6
+ require_relative '../columns/columns'
7
+ require_relative '../md_object'
8
+
9
+ module GoodData
10
+ module Model
11
+ ##
12
+ # Server-side representation of a local data set; includes connection point,
13
+ # attributes and labels, facts, folders and corresponding pieces of physical
14
+ # model abstractions.
15
+ #
16
+ class Schema < MdObject
17
+ attr_reader :fields, :attributes, :facts, :folders, :references, :labels, :name, :title, :anchor
18
+
19
+ def self.load(file)
20
+ Schema.new JSON.load(open(file))
21
+ end
22
+
23
+ def initialize(a_config, a_name = 'Default Name', a_title = 'Default Title')
24
+ super()
25
+ @fields = []
26
+ @attributes = []
27
+ @facts = []
28
+ @folders = {
29
+ :facts => {},
30
+ :attributes => {}
31
+ }
32
+ @references = []
33
+ @labels = []
34
+
35
+ a_config[:name] = a_name unless a_config[:name]
36
+ a_config[:title] = a_config[:name] unless a_config[:title]
37
+ a_config[:title] = a_title unless a_config[:title]
38
+ a_config[:title] = a_config[:title].humanize
39
+
40
+ fail 'Schema name not specified' unless a_config[:name]
41
+ @name = a_config[:name]
42
+ @title = a_config[:title]
43
+ self.config = (a_config)
44
+ end
45
+
46
+ def config=(config)
47
+ config[:columns].each do |c|
48
+ case c[:type].to_s
49
+ when 'attribute'
50
+ add_attribute c
51
+ when 'fact'
52
+ add_fact c
53
+ when 'date'
54
+ add_date c
55
+ when 'anchor'
56
+ set_anchor c
57
+ when 'label'
58
+ add_label c
59
+ when 'reference'
60
+ add_reference c
61
+ else
62
+ fail "Unexpected type #{c[:type]} in #{c.inspect}"
63
+ end
64
+ end
65
+ @anchor = Anchor.new(nil, self) unless @anchor
66
+ end
67
+
68
+ def type_prefix
69
+ 'dataset'
70
+ end
71
+
72
+ ##
73
+ # Underlying fact table name
74
+ #
75
+ def table
76
+ @table ||= FACT_COLUMN_PREFIX + name
77
+ end
78
+
79
+ ##
80
+ # Generates MAQL DDL script to drop this data set and included pieces
81
+ #
82
+ def to_maql_drop
83
+ maql = ''
84
+ [attributes, facts].each do |obj|
85
+ maql += obj.to_maql_drop
86
+ end
87
+ maql += "DROP {#{identifier}};\n"
88
+ end
89
+
90
+ ##
91
+ # Generates MAQL DDL script to create this data set and included pieces
92
+ #
93
+ def to_maql_create
94
+ # TODO: Use template (.erb)
95
+ maql = "# Create the '#{title}' data set\n"
96
+ maql += "CREATE DATASET {#{identifier}} VISUAL (TITLE \"#{title}\");\n\n"
97
+ [attributes, facts, { 1 => @anchor }].each do |objects|
98
+ objects.values.each do |obj|
99
+ maql += "# Create '#{obj.title}' and add it to the '#{title}' data set.\n"
100
+ maql += obj.to_maql_create
101
+ maql += "ALTER DATASET {#{identifier}} ADD {#{obj.identifier}};\n\n"
102
+ end
103
+ end
104
+
105
+ labels.each do |label|
106
+ maql += "# Creating Labels\n"
107
+ maql += label.to_maql_create
108
+ end
109
+
110
+ references.values.each do |ref|
111
+ maql += "# Creating references\n"
112
+ maql += ref.to_maql_create
113
+ end
114
+
115
+ folders_maql = "# Create folders\n"
116
+ (folders[:attributes].values + folders[:facts].values).each { |folder| folders_maql += folder.to_maql_create }
117
+ folders_maql + "\n" + maql + "SYNCHRONIZE {#{identifier}};\n"
118
+ end
119
+
120
+ def upload(path, project = nil, mode = 'FULL')
121
+ if path =~ URI.regexp
122
+ Tempfile.open('remote_file') do |temp|
123
+ temp << open(path).read
124
+ temp.flush
125
+ upload_data(temp, mode)
126
+ end
127
+ else
128
+ upload_data(path, mode)
129
+ end
130
+ end
131
+
132
+ def upload_data(path, mode)
133
+ GoodData::Model.upload_data(path, to_manifest(mode))
134
+ end
135
+
136
+ # Generates the SLI manifest describing the data loading
137
+ #
138
+ def to_manifest(mode = 'FULL')
139
+ {
140
+ 'dataSetSLIManifest' => {
141
+ 'parts' => fields.reduce([]) do |memo, f|
142
+ val = f.to_manifest_part(mode)
143
+ memo << val unless val.nil?
144
+ memo
145
+ end,
146
+ 'dataSet' => identifier,
147
+ 'file' => 'data.csv', # should be configurable
148
+ 'csvParams' => {
149
+ 'quoteChar' => '"',
150
+ 'escapeChar' => '"',
151
+ 'separatorChar' => ',',
152
+ 'endOfLine' => "\n"
153
+ }
154
+ }
155
+ }
156
+ end
157
+
158
+ def to_wire_model
159
+ {
160
+ 'dataset' => {
161
+ 'identifier' => identifier,
162
+ 'title' => title,
163
+ 'anchor' => @anchor.to_wire_model,
164
+ 'facts' => facts.map { |f| f.to_wire_model },
165
+ 'attributes' => attributes.map { |a| a.to_wire_model },
166
+ 'references' => references.map { |r| r.is_a?(DateReference) ? r.schema_ref : type_prefix + '.' + r.schema_ref }
167
+ } }
168
+ end
169
+
170
+ private
171
+
172
+ def add_attribute(column)
173
+ attribute = Attribute.new column, self
174
+ fields << attribute
175
+ attributes << attribute
176
+ add_attribute_folder(attribute.folder)
177
+ # folders[AttributeFolder.new(attribute.folder)] = 1 if attribute.folder
178
+ end
179
+
180
+ def add_attribute_folder(name)
181
+ return if name.nil?
182
+ return if folders[:attributes].key?(name)
183
+ folders[:attributes][name] = AttributeFolder.new(name)
184
+ end
185
+
186
+ def add_fact(column)
187
+ fact = Fact.new column, self
188
+ fields << fact
189
+ facts << fact
190
+ add_fact_folder(fact.folder)
191
+ # folders[FactFolder.new(fact.folder)] = 1 if fact.folder
192
+ end
193
+
194
+ def add_fact_folder(name)
195
+ return if name.nil?
196
+ return if folders[:facts].key?(name)
197
+ folders[:facts][name] = FactFolder.new(name)
198
+ end
199
+
200
+ def add_label(column)
201
+ label = Label.new(column, nil, self)
202
+ labels << label
203
+ fields << label
204
+ end
205
+
206
+ def add_reference(column)
207
+ reference = Reference.new(column, self)
208
+ fields << reference
209
+ references << reference
210
+ end
211
+
212
+ def add_date(column)
213
+ date = DateColumn.new column, self
214
+ @fields << date
215
+ date.parts.values.each { |p| @fields << p }
216
+ date.facts.each { |f| facts << f }
217
+ date.attributes.each { |a| attributes << a }
218
+ date.references.each { |r| references << r }
219
+ end
220
+
221
+ def set_anchor(column) # rubocop:disable AccessorMethodName
222
+ @anchor = Anchor.new column, self
223
+ @fields << @anchor
224
+ end
225
+ end
226
+ end
227
+ end
@@ -1,9 +1,23 @@
1
1
  # encoding: UTF-8
2
2
 
3
- require_relative '../helpers'
4
-
3
+ require_relative '../core/connection'
4
+ require_relative '../core/rest'
5
+
6
+ require_relative 'attributes/attributes'
7
+ require_relative 'columns/columns'
8
+ require_relative 'facts/facts'
9
+ require_relative 'folders/folders'
10
+ require_relative 'metadata/metadata'
11
+ require_relative 'references/references'
12
+
13
+ require_relative 'links'
14
+ require_relative 'module_constants'
15
+ require_relative 'metadata/schema'
16
+
17
+ require 'fileutils'
18
+ require 'multi_json'
5
19
  require 'open-uri'
6
- require 'active_support/all'
20
+ require 'zip'
7
21
 
8
22
  ##
9
23
  # Module containing classes that counter-part GoodData server-side meta-data
@@ -11,49 +25,25 @@ require 'active_support/all'
11
25
  #
12
26
  module GoodData
13
27
  module Model
14
- # GoodData REST API categories
15
- LDM_CTG = 'ldm'
16
- LDM_MANAGE_CTG = 'ldm-manage'
17
-
18
- # Model naming conventions
19
- FIELD_PK = 'id'
20
- FK_SUFFIX = '_id'
21
- FACT_COLUMN_PREFIX = 'f_'
22
- DATE_COLUMN_PREFIX = 'dt_'
23
- TIME_COLUMN_PREFIX = 'tm_'
24
- LABEL_COLUMN_PREFIX = 'nm_'
25
- ATTRIBUTE_FOLDER_PREFIX = 'dim'
26
- ATTRIBUTE_PREFIX = 'attr'
27
- LABEL_PREFIX = 'label'
28
- FACT_PREFIX = 'fact'
29
- DATE_FACT_PREFIX = 'dt'
30
- DATE_ATTRIBUTE = 'date'
31
- DATE_ATTRIBUTE_DEFAULT_DISPLAY_FORM = 'mdyy'
32
- TIME_FACT_PREFIX = 'tm.dt'
33
- TIME_ATTRIBUTE_PREFIX = 'attr.time'
34
- FACT_FOLDER_PREFIX = 'ffld'
35
-
36
- SKIP_FIELD = false
37
-
38
28
  class << self
39
- def add_dataset(name, columns, project = nil)
40
- Schema.new('columns' => columns, 'name' => name)
41
- add_schema(schema, project)
42
- end
29
+ # def add_dataset(name, columns, project = nil)
30
+ # Schema.new('columns' => columns, 'name' => name)
31
+ # add_schema(schema, project)
32
+ # end
43
33
 
44
- def add_schema(schema, project = nil)
45
- unless schema.respond_to?(:to_maql_create) || schema.is_a?(String) then
46
- raise ArgumentError.new("Schema object or schema file path expected, got '#{schema}'")
47
- end
48
- schema = Schema.load(schema) unless schema.respond_to?(:to_maql_create)
49
- project = GoodData.project unless project
50
- ldm_links = GoodData.get project.md[LDM_CTG]
51
- ldm_uri = Links.new(ldm_links)[LDM_MANAGE_CTG]
52
- GoodData.post ldm_uri, {'manage' => {'maql' => schema.to_maql_create}}
53
- end
34
+ # def add_schema(schema, project = nil)
35
+ # unless schema.respond_to?(:to_maql_create) || schema.is_a?(String)
36
+ # fail(ArgumentError, "Schema object or schema file path expected, got '#{schema}'")
37
+ # end
38
+ # schema = Schema.load(schema) unless schema.respond_to?(:to_maql_create)
39
+ # project = GoodData.project unless project
40
+ # ldm_links = GoodData.get project.md[LDM_CTG]
41
+ # ldm_uri = Links.new(ldm_links)[LDM_MANAGE_CTG]
42
+ # GoodData.post ldm_uri, 'manage' => { 'maql' => schema.to_maql_create }
43
+ # end
54
44
 
55
45
  # Load given file into a data set described by the given schema
56
- def upload_data(path, manifest, options={})
46
+ def upload_data(path, manifest, options = {})
57
47
  project = options[:project] || GoodData.project
58
48
  # mode = options[:mode] || "FULL"
59
49
  path = path.path if path.respond_to? :path
@@ -63,7 +53,7 @@ module GoodData
63
53
  dir = Dir.mktmpdir
64
54
  begin
65
55
  Zip::File.open("#{dir}/upload.zip", Zip::File::CREATE) do |zip|
66
- # TODO make sure schema columns match CSV column names
56
+ # TODO: make sure schema columns match CSV column names
67
57
  zip.get_output_stream('upload_info.json') { |f| f.puts JSON.pretty_generate(manifest) }
68
58
  if inline_data
69
59
  zip.get_output_stream('data.csv') do |f|
@@ -83,10 +73,11 @@ module GoodData
83
73
  end
84
74
 
85
75
  # kick the load
86
- pull = {'pullIntegration' => File.basename(dir)}
76
+ pull = { 'pullIntegration' => File.basename(dir) }
87
77
  link = project.md.links('etl')['pull']
88
78
  task = GoodData.post link, pull
89
- while GoodData.get(task['pullTask']['uri'])['taskStatus'] === 'RUNNING' || GoodData.get(task['pullTask']['uri'])['taskStatus'] === 'PREPARED'
79
+ # TODO: Refactor the task status out
80
+ while GoodData.get(task['pullTask']['uri'])['taskStatus'] == 'RUNNING' || GoodData.get(task['pullTask']['uri'])['taskStatus'] == 'PREPARED'
90
81
  sleep 30
91
82
  end
92
83
  if GoodData.get(task['pullTask']['uri'])['taskStatus'] == 'ERROR'
@@ -103,1302 +94,10 @@ module GoodData
103
94
  d = Marshal.load(Marshal.dump(a_schema_blueprint))
104
95
  d[:columns] = d[:columns] + b_schema_blueprint[:columns]
105
96
  d[:columns].uniq!
106
- columns_that_failed_to_merge = d[:columns].group_by { |x| x[:name] }.map { |k, v| [k, v.count] }.find_all { |x| x[1] > 1 }
97
+ columns_that_failed_to_merge = d[:columns].group_by { |x| x[:name] }.map { |k, v| [k, v.count] }.select { |x| x[1] > 1 }
107
98
  fail "Columns #{columns_that_failed_to_merge} failed to merge. When merging columns with the same name they have to be identical." unless columns_that_failed_to_merge.empty?
108
99
  d
109
100
  end
110
101
  end
111
-
112
- class ProjectBlueprint
113
- attr_accessor :data
114
-
115
- def self.from_json(spec)
116
- if spec.is_a?(String)
117
- ProjectBlueprint.new(MultiJson.load(File.read(spec), :symbolize_keys => true))
118
- else
119
- ProjectBlueprint.new(spec)
120
- end
121
- end
122
-
123
- def change(&block)
124
- builder = ProjectBuilder.create_from_data(self)
125
- block.call(builder)
126
- builder
127
- @data = builder.to_hash
128
- self
129
- end
130
-
131
- def datasets
132
- data[:datasets].map { |d| SchemaBlueprint.new(d) }
133
- end
134
-
135
- def add_dataset(a_dataset, index=nil)
136
- if index.nil? || index > datasets.length
137
- data[:datasets] << a_dataset.to_hash
138
- else
139
- data[:datasets].insert(index, a_dataset.to_hash)
140
- end
141
- end
142
-
143
- def remove_dataset(dataset_name)
144
- x = data[:datasets].find { |d| d[:name] == dataset_name }
145
- index = data[:datasets].index(x)
146
- data[:datasets].delete_at(index)
147
- end
148
-
149
- def date_dimensions
150
- data[:date_dimensions]
151
- end
152
-
153
- def get_dataset(name)
154
- ds = data[:datasets].find { |d| d[:name] == name }
155
- SchemaBlueprint.new(ds) unless ds.nil?
156
- end
157
-
158
- def initialize(init_data)
159
- @data = init_data
160
- end
161
-
162
- def model_validate
163
- if datasets.count == 1
164
- []
165
- else
166
- x = datasets.reduce([]) { |memo, schema| schema.has_anchor? ? memo << [schema.name, schema.anchor[:name]] : memo }
167
- refs = datasets.reduce([]) do |memo, dataset|
168
- memo.concat(dataset.references)
169
- end
170
- refs.reduce([]) do |memo, ref|
171
- x.include?([ref[:dataset], ref[:reference]]) ? memo : memo.concat([ref])
172
- end
173
- end
174
- end
175
-
176
- def model_valid?
177
- errors = model_validate
178
- errors.empty? ? true : false
179
- end
180
-
181
- def merge!(a_blueprint)
182
- temp_blueprint = dup
183
- a_blueprint.datasets.each do |dataset|
184
- local_dataset = temp_blueprint.get_dataset(dataset.name)
185
- if local_dataset.nil?
186
- temp_blueprint.add_dataset(dataset.dup)
187
- else
188
- index = temp_blueprint.datasets.index(local_dataset)
189
- local_dataset.merge!(dataset)
190
- temp_blueprint.remove_dataset(local_dataset.name)
191
- temp_blueprint.add_dataset(local_dataset, index)
192
- end
193
- end
194
- @data = temp_blueprint.data
195
- self
196
- end
197
-
198
- def dup
199
- deep_copy = Marshal.load(Marshal.dump(data))
200
- ProjectBlueprint.new(deep_copy)
201
- end
202
-
203
- def title
204
- data[:title]
205
- end
206
-
207
- def to_wire_model
208
- {
209
- 'diffRequest' => {
210
- 'targetModel' => {
211
- 'projectModel' => {
212
- 'datasets' => datasets.map { |d| d.to_wire_model },
213
- 'dateDimensions' => date_dimensions.map { |d|
214
- {
215
- 'dateDimension' => {
216
- 'name' => d[:name],
217
- 'title' => d[:title] || d[:name].humanize
218
- }
219
- } }
220
- }}}}
221
- end
222
-
223
- def to_hash
224
- @data
225
- end
226
- end
227
-
228
- class SchemaBlueprint
229
- attr_accessor :data
230
-
231
- def change(&block)
232
- builder = SchemaBuilder.create_from_data(self)
233
- block.call(builder)
234
- builder
235
- @data = builder.to_hash
236
- self
237
- end
238
-
239
- def initialize(init_data)
240
- @data = init_data
241
- end
242
-
243
- def upload(source, options={})
244
- project = options[:project] || GoodData.project
245
- fail 'You have to specify a project into which you want to load.' if project.nil?
246
- mode = options[:load] || 'FULL'
247
- project.upload(source, to_schema, mode)
248
- end
249
-
250
- def merge!(a_blueprint)
251
- new_blueprint = GoodData::Model.merge_dataset_columns(self, a_blueprint)
252
- @data = new_blueprint
253
- self
254
- end
255
-
256
- def name
257
- data[:name]
258
- end
259
-
260
- def title
261
- data[:title]
262
- end
263
-
264
- def to_hash
265
- data
266
- end
267
-
268
- def columns
269
- data[:columns]
270
- end
271
-
272
- def has_anchor?
273
- columns.any? { |c| c[:type].to_s == 'anchor' }
274
- end
275
-
276
- def anchor
277
- find_column_by_type(:anchor, :first)
278
- end
279
-
280
- def references
281
- find_column_by_type(:reference)
282
- end
283
-
284
- def attributes
285
- find_column_by_type(:attribute)
286
- end
287
-
288
- def facts
289
- find_column_by_type(:fact)
290
- end
291
-
292
- def find_column_by_type(type, all=:all)
293
- type = type.to_s
294
- if all == :all
295
- columns.find_all { |c| c[:type].to_s == type }
296
- else
297
- columns.find { |c| c[:type].to_s == type }
298
- end
299
- end
300
-
301
- def find_column_by_name(type, all=:all)
302
- type = type.to_s
303
- if all == :all
304
- columns.find_all { |c| c[:name].to_s == type }
305
- else
306
- columns.find { |c| c[:name].to_s == type }
307
- end
308
- end
309
-
310
- def to_schema
311
- Schema.new(to_hash)
312
- end
313
-
314
- def to_manifest
315
- to_schema.to_manifest
316
- end
317
-
318
- def pretty_print(printer)
319
- printer.text "Schema <#{object_id}>:\n"
320
- printer.text " Name: #{name}\n"
321
- printer.text " Columns: \n"
322
- printer.text columns.map { |c| " #{c[:name]}: #{c[:type]}" }.join("\n")
323
- end
324
-
325
- def dup
326
- deep_copy = Marshal.load(Marshal.dump(data))
327
- SchemaBlueprint.new(deep_copy)
328
- end
329
-
330
- def to_wire_model
331
- to_schema.to_wire_model
332
- end
333
-
334
- def ==(other)
335
- to_hash == other.to_hash
336
- end
337
- end
338
-
339
- class ProjectBuilder
340
- attr_reader :title, :datasets, :reports, :metrics, :uploads, :users, :assert_report, :date_dimensions
341
-
342
- class << self
343
- def create_from_data(blueprint, title = 'Title')
344
- pb = ProjectBuilder.new(title)
345
- pb.data = blueprint.to_hash
346
- pb
347
- end
348
-
349
- def create(title, options={}, &block)
350
- pb = ProjectBuilder.new(title)
351
- block.call(pb)
352
- pb
353
- end
354
- end
355
-
356
- def initialize(title)
357
- @title = title
358
- @datasets = []
359
- @reports = []
360
- @assert_tests = []
361
- @metrics = []
362
- @uploads = []
363
- @users = []
364
- @dashboards = []
365
- @date_dimensions = []
366
- end
367
-
368
- def add_date_dimension(name, options = {})
369
- dimension = {
370
- urn: options[:urn],
371
- name: name,
372
- title: options[:title]
373
- }
374
-
375
- @date_dimensions << dimension
376
- end
377
-
378
- def add_dataset(name, &block)
379
- builder = GoodData::Model::SchemaBuilder.new(name)
380
- block.call(builder)
381
- if @datasets.any? { |item| item[:name] == name }
382
- ds = @datasets.find { |item| item[:name] == name }
383
- index = @datasets.index(ds)
384
- stuff = GoodData::Model.merge_dataset_columns(ds, builder.to_hash)
385
- @datasets.delete_at(index)
386
- @datasets.insert(index, stuff)
387
- else
388
- @datasets << builder.to_hash
389
- end
390
- end
391
-
392
- def add_report(title, options={})
393
- @reports << {:title => title}.merge(options)
394
- end
395
-
396
- def add_metric(title, options={})
397
- @metrics << {:title => title}.merge(options)
398
- end
399
-
400
- def add_dashboard(title, &block)
401
- db = DashboardBuilder.new(title)
402
- block.call(db)
403
- @dashboards << db.to_hash
404
- end
405
-
406
- def load_metrics(file)
407
- new_metrics = MultiJson.load(open(file).read, :symbolize_keys => true)
408
- @metrics = @metrics + new_metrics
409
- end
410
-
411
- def load_datasets(file)
412
- new_metrics = MultiJson.load(open(file).read, :symbolize_keys => true)
413
- @datasets = @datasets + new_metrics
414
- end
415
-
416
- def assert_report(report, result)
417
- @assert_tests << {:report => report, :result => result}
418
- end
419
-
420
- def upload(data, options={})
421
- mode = options[:mode] || 'FULL'
422
- dataset = options[:dataset]
423
- @uploads << {
424
- :source => data,
425
- :mode => mode,
426
- :dataset => dataset
427
- }
428
- end
429
-
430
- def add_users(users)
431
- @users << users
432
- end
433
-
434
- def to_json(options={})
435
- eliminate_empty = options[:eliminate_empty] || false
436
-
437
- if eliminate_empty
438
- JSON.pretty_generate(to_hash.reject { |k, v| v.is_a?(Enumerable) && v.empty? })
439
- else
440
- JSON.pretty_generate(to_hash)
441
- end
442
- end
443
-
444
- def to_hash
445
- {
446
- :title => @title,
447
- :datasets => @datasets,
448
- :uploads => @uploads,
449
- :dashboards => @dashboards,
450
- :metrics => @metrics,
451
- :reports => @reports,
452
- :users => @users,
453
- :assert_tests => @assert_tests,
454
- :date_dimensions => @date_dimensions
455
- }
456
- end
457
-
458
- def get_dataset(name)
459
- datasets.find { |d| d.name == name }
460
- end
461
- end
462
-
463
- class DashboardBuilder
464
- def initialize(title)
465
- @title = title
466
- @tabs = []
467
- end
468
-
469
- def add_tab(tab, &block)
470
- tb = TabBuilder.new(tab)
471
- block.call(tb)
472
- @tabs << tb
473
- tb
474
- end
475
-
476
- def to_hash
477
- {
478
- :name => @name,
479
- :tabs => @tabs.map { |tab| tab.to_hash }
480
- }
481
- end
482
- end
483
-
484
- class TabBuilder
485
- def initialize(title)
486
- @title = title
487
- @stuff = []
488
- end
489
-
490
- def add_report(options={})
491
- @stuff << {:type => :report}.merge(options)
492
- end
493
-
494
- def to_hash
495
- {
496
- :title => @title,
497
- :items => @stuff
498
- }
499
- end
500
- end
501
-
502
- class SchemaBuilder
503
- attr_accessor :data
504
-
505
- class << self
506
- def create_from_data(blueprint)
507
- sc = SchemaBuilder.new
508
- sc.data = blueprint.to_hash
509
- sc
510
- end
511
- end
512
-
513
- def initialize(name=nil)
514
- @data = {
515
- :name => name,
516
- :columns => []
517
- }
518
- end
519
-
520
- def name
521
- data[:name]
522
- end
523
-
524
- def columns
525
- data[:columns]
526
- end
527
-
528
- def add_column(column_def)
529
- columns.push(column_def)
530
- self
531
- end
532
-
533
- def add_anchor(name, options={})
534
- add_column({:type => :anchor, :name => name}.merge(options))
535
- self
536
- end
537
-
538
- def add_attribute(name, options={})
539
- add_column({:type => :attribute, :name => name}.merge(options))
540
- self
541
- end
542
-
543
- def add_fact(name, options={})
544
- add_column({:type => :fact, :name => name}.merge(options))
545
- self
546
- end
547
-
548
- def add_label(name, options={})
549
- add_column({:type => :label, :name => name}.merge(options))
550
- self
551
- end
552
-
553
- def add_date(name, options={})
554
- add_column({:type => :date, :name => name}.merge(options))
555
- end
556
-
557
- def add_reference(name, options={})
558
- add_column({:type => :reference, :name => name}.merge(options))
559
- end
560
-
561
- def to_json
562
- JSON.pretty_generate(to_hash)
563
- end
564
-
565
- def to_hash
566
- data
567
- end
568
-
569
- def to_schema
570
- Schema.new(to_hash)
571
- end
572
- end
573
-
574
- class ProjectCreator
575
- class << self
576
- def migrate(options={})
577
- spec = options[:spec] || fail('You need to provide spec for migration')
578
- spec = spec.to_hash
579
-
580
- token = options[:token]
581
- project = options[:project] || GoodData::Project.create(:title => spec[:title], :auth_token => token)
582
- fail('You need to specify token for project creation') if token.nil? && project.nil?
583
-
584
- begin
585
- GoodData.with_project(project) do |p|
586
- # migrate_date_dimensions(p, spec[:date_dimensions] || [])
587
- migrate_datasets(p, spec)
588
- load(p, spec)
589
- migrate_metrics(p, spec[:metrics] || [])
590
- migrate_reports(p, spec[:reports] || [])
591
- migrate_dashboards(p, spec[:dashboards] || [])
592
- migrate_users(p, spec[:users] || [])
593
- execute_tests(p, spec[:assert_tests] || [])
594
- p
595
- end
596
- end
597
- end
598
-
599
- def migrate_date_dimensions(project, spec)
600
- spec.each do |dd|
601
- Model.add_schema(DateDimension.new(dd), project)
602
- end
603
- end
604
-
605
- def migrate_datasets(project, spec)
606
- bp = ProjectBlueprint.new(spec)
607
- # schema = Schema.load(schema) unless schema.respond_to?(:to_maql_create)
608
- # project = GoodData.project unless project
609
- uri = "/gdc/projects/#{GoodData.project.pid}/model/diff"
610
- result = GoodData.post(uri, bp.to_wire_model)
611
- link = result['asyncTask']['link']['poll']
612
- response = GoodData.get(link, :process => false)
613
- # pp response
614
- while response.code != 200
615
- sleep 1
616
- GoodData.connection.retryable(:tries => 3, :on => RestClient::InternalServerError) do
617
- sleep 1
618
- response = GoodData.get(link, :process => false)
619
- # pp response
620
- end
621
- end
622
- response = GoodData.get(link)
623
- ldm_links = GoodData.get project.md[LDM_CTG]
624
- ldm_uri = Links.new(ldm_links)[LDM_MANAGE_CTG]
625
- chunks = response['projectModelDiff']['updateScripts'].find_all { |script| script['updateScript']['preserveData'] == true && script['updateScript']['cascadeDrops'] == false }.map { |x| x['updateScript']['maqlDdlChunks'] }.flatten
626
- chunks.each do |chunk|
627
- GoodData.post ldm_uri, {'manage' => {'maql' => chunk}}
628
- end
629
-
630
- bp.datasets.each do |ds|
631
- schema = ds.to_schema
632
- GoodData::ProjectMetadata["manifest_#{schema.name}"] = schema.to_manifest.to_json
633
- end
634
- end
635
-
636
- def migrate_reports(project, spec)
637
- spec.each do |report|
638
- project.add_report(report)
639
- end
640
- end
641
-
642
- def migrate_dashboards(project, spec)
643
- spec.each do |dash|
644
- project.add_dashboard(dash)
645
- end
646
- end
647
-
648
- def migrate_metrics(project, spec)
649
- spec.each do |metric|
650
- project.add_metric(metric)
651
- end
652
- end
653
-
654
- def migrate_users(project, spec)
655
- spec.each do |user|
656
- puts "Would migrate user #{user}"
657
- # project.add_user(user)
658
- end
659
- end
660
-
661
- def load(project, spec)
662
- if spec.has_key?(:uploads)
663
- spec[:uploads].each do |load|
664
- schema = GoodData::Model::Schema.new(spec[:datasets].detect { |d| d[:name] == load[:dataset] })
665
- project.upload(load[:source], schema, load[:mode])
666
- end
667
- end
668
- end
669
-
670
- def execute_tests(project, spec)
671
- spec.each do |assert|
672
- result = GoodData::ReportDefinition.execute(assert[:report])
673
- fail "Test did not pass. Got #{result.table.inspect}, expected #{assert[:result].inspect}" if result.table != assert[:result]
674
- end
675
- end
676
- end
677
- end
678
-
679
- class MdObject
680
- attr_accessor :name, :title
681
-
682
- def visual
683
- "TITLE \"#{title_esc}\""
684
- end
685
-
686
- def title_esc
687
- title.gsub(/"/, "\\\"")
688
- end
689
-
690
- ##
691
- # Generates an identifier from the object name by transliterating
692
- # non-Latin character and then dropping non-alphanumerical characters.
693
- #
694
- def identifier
695
- @identifier ||= "#{self.type_prefix}.#{name}"
696
- end
697
- end
698
-
699
- ##
700
- # Server-side representation of a local data set; includes connection point,
701
- # attributes and labels, facts, folders and corresponding pieces of physical
702
- # model abstractions.
703
- #
704
- class Schema < MdObject
705
- attr_reader :fields, :attributes, :facts, :folders, :references, :labels, :name, :title, :anchor
706
-
707
- def self.load(file)
708
- Schema.new JSON.load(open(file))
709
- end
710
-
711
- def initialize(config, name = 'Default Name', title = 'Default Title')
712
- super()
713
- @fields = []
714
- @attributes = []
715
- @facts = []
716
- @folders = {
717
- :facts => {},
718
- :attributes => {}
719
- }
720
- @references = []
721
- @labels = []
722
-
723
- config[:name] = name unless config[:name]
724
- config[:title] = config[:name] unless config[:title]
725
- config[:title] = title unless config[:title]
726
- config[:title] = config[:title].humanize
727
-
728
- fail 'Schema name not specified' unless config[:name]
729
- self.name = config[:name]
730
- self.title = config[:title]
731
- self.config = config
732
- end
733
-
734
- def config=(config)
735
- config[:columns].each do |c|
736
- case c[:type].to_s
737
- when 'attribute'
738
- add_attribute c
739
- when 'fact'
740
- add_fact c
741
- when 'date'
742
- add_date c
743
- when 'anchor'
744
- set_anchor c
745
- when 'label'
746
- add_label c
747
- when 'reference'
748
- add_reference c
749
- else
750
- fail "Unexpected type #{c[:type]} in #{c.inspect}"
751
- end
752
- end
753
- @anchor = Anchor.new(nil, self) unless @anchor
754
- end
755
-
756
- def type_prefix
757
- 'dataset'
758
- end
759
-
760
- ##
761
- # Underlying fact table name
762
- #
763
- def table
764
- @table ||= FACT_COLUMN_PREFIX + name
765
- end
766
-
767
- ##
768
- # Generates MAQL DDL script to drop this data set and included pieces
769
- #
770
- def to_maql_drop
771
- maql = ''
772
- [attributes, facts].each do |obj|
773
- maql += obj.to_maql_drop
774
- end
775
- maql += "DROP {#{self.identifier}};\n"
776
- end
777
-
778
- ##
779
- # Generates MAQL DDL script to create this data set and included pieces
780
- #
781
- def to_maql_create
782
- # TODO: Use template (.erb)
783
- maql = "# Create the '#{self.title}' data set\n"
784
- maql += "CREATE DATASET {#{self.identifier}} VISUAL (TITLE \"#{self.title}\");\n\n"
785
- [attributes, facts, {1 => @anchor}].each do |objects|
786
- objects.values.each do |obj|
787
- maql += "# Create '#{obj.title}' and add it to the '#{self.title}' data set.\n"
788
- maql += obj.to_maql_create
789
- maql += "ALTER DATASET {#{self.identifier}} ADD {#{obj.identifier}};\n\n"
790
- end
791
- end
792
-
793
- labels.each do |label|
794
- maql += "# Creating Labels\n"
795
- maql += label.to_maql_create
796
- end
797
-
798
- references.values.each do |ref|
799
- maql += "# Creating references\n"
800
- maql += ref.to_maql_create
801
- end
802
-
803
- folders_maql = "# Create folders\n"
804
- (folders[:attributes].values + folders[:facts].values).each { |folder| folders_maql += folder.to_maql_create }
805
- folders_maql + "\n" + maql + "SYNCHRONIZE {#{identifier}};\n"
806
- end
807
-
808
- def upload(path, project = nil, mode = 'FULL')
809
- if path =~ URI::regexp
810
- Tempfile.open('remote_file') do |temp|
811
- temp << open(path).read
812
- temp.flush
813
- upload_data(temp, mode)
814
- end
815
- else
816
- upload_data(path, mode)
817
- end
818
- end
819
-
820
- def upload_data(path, mode)
821
- GoodData::Model.upload_data(path, to_manifest(mode))
822
- end
823
-
824
- # Generates the SLI manifest describing the data loading
825
- #
826
- def to_manifest(mode = 'FULL')
827
- {
828
- 'dataSetSLIManifest' => {
829
- 'parts' => fields.reduce([]) { |memo, f| val = f.to_manifest_part(mode); memo << val unless val.nil?; memo },
830
- 'dataSet' => self.identifier,
831
- 'file' => 'data.csv', # should be configurable
832
- 'csvParams' => {
833
- 'quoteChar' => '"',
834
- 'escapeChar' => '"',
835
- 'separatorChar' => ',',
836
- 'endOfLine' => "\n"
837
- }
838
- }
839
- }
840
- end
841
-
842
- def to_wire_model
843
- {
844
- 'dataset' => {
845
- 'identifier' => identifier,
846
- 'title' => title,
847
- 'anchor' => @anchor.to_wire_model,
848
- 'facts' => facts.map { |f| f.to_wire_model },
849
- 'attributes' => attributes.map { |a| a.to_wire_model },
850
- 'references' => references.map { |r| r.is_a?(DateReference) ? r.schema_ref : type_prefix + '.' + r.schema_ref }}
851
- }
852
- end
853
-
854
- private
855
-
856
- def add_attribute(column)
857
- attribute = Attribute.new column, self
858
- fields << attribute
859
- attributes << attribute
860
- add_attribute_folder(attribute.folder)
861
- # folders[AttributeFolder.new(attribute.folder)] = 1 if attribute.folder
862
- end
863
-
864
- def add_attribute_folder(name)
865
- return if name.nil?
866
- return if folders[:attributes].has_key?(name)
867
- folders[:attributes][name] = AttributeFolder.new(name)
868
- end
869
-
870
- def add_fact(column)
871
- fact = Fact.new column, self
872
- fields << fact
873
- facts << fact
874
- add_fact_folder(fact.folder)
875
- # folders[FactFolder.new(fact.folder)] = 1 if fact.folder
876
- end
877
-
878
- def add_fact_folder(name)
879
- return if name.nil?
880
- return if folders[:facts].has_key?(name)
881
- folders[:facts][name] = FactFolder.new(name)
882
- end
883
-
884
- def add_label(column)
885
- label = Label.new(column, nil, self)
886
- labels << label
887
- fields << label
888
- end
889
-
890
- def add_reference(column)
891
- reference = Reference.new(column, self)
892
- fields << reference
893
- references << reference
894
- end
895
-
896
- def add_date(column)
897
- date = DateColumn.new column, self
898
- @fields << date
899
- date.parts.values.each { |p| @fields << p }
900
- date.facts.each { |f| facts << f }
901
- date.attributes.each { |a| attributes << a }
902
- date.references.each { |r| references << r }
903
- end
904
-
905
- def set_anchor(column)
906
- @anchor = Anchor.new column, self
907
- @fields << @anchor
908
- end
909
- end
910
-
911
- ##
912
- # This is a base class for server-side LDM elements such as attributes, labels and
913
- # facts
914
- #
915
- class Column < MdObject
916
- attr_accessor :folder, :name, :title, :schema
917
-
918
- def initialize(hash, schema)
919
- super()
920
- raise ArgumentError.new("Schema must be provided, got #{schema.class}") unless schema.is_a? Schema
921
- raise('Data set fields must have their names defined') if hash[:name].nil?
922
-
923
- @name = hash[:name]
924
- @title = hash[:title] || hash[:name].humanize
925
- @folder = hash[:folder]
926
- @schema = schema
927
- end
928
-
929
- ##
930
- # Generates an identifier from the object name by transliterating
931
- # non-Latin character and then dropping non-alphanumerical characters.
932
- #
933
- def identifier
934
- @identifier ||= "#{self.type_prefix}.#{@schema.name}.#{name}"
935
- end
936
-
937
- def to_maql_drop
938
- "DROP {#{self.identifier}};\n"
939
- end
940
-
941
- def visual
942
- visual = super
943
- visual += ", FOLDER {#{folder_prefix}.#{(folder)}}" if folder
944
- visual
945
- end
946
-
947
- def to_csv_header(row)
948
- name
949
- end
950
-
951
- def to_csv_data(headers, row)
952
- row[name]
953
- end
954
-
955
-
956
- # Overriden to prevent long strings caused by the @schema attribute
957
- #
958
- def inspect
959
- to_s.sub(/>$/, " @title=#{@title.inspect}, @name=#{@name.inspect}, @folder=#{@folder.inspect}," \
960
- " @schema=#{@schema.to_s.sub(/>$/, ' @title=' + @schema.name.inspect + '>')}" \
961
- ">")
962
- end
963
- end
964
-
965
- ##
966
- # GoodData attribute abstraction
967
- #
968
- class Attribute < Column
969
- attr_reader :primary_label, :labels
970
-
971
- def type_prefix;
972
- ATTRIBUTE_PREFIX;
973
- end
974
-
975
- def folder_prefix;
976
- ATTRIBUTE_FOLDER_PREFIX;
977
- end
978
-
979
- def initialize(hash, schema)
980
- super hash, schema
981
- @labels = []
982
- @primary_label = Label.new hash, self, schema
983
- end
984
-
985
- def table
986
- @table ||= 'd_' + @schema.name + '_' + name
987
- end
988
-
989
- def key;
990
- "#{@name}#{FK_SUFFIX}";
991
- end
992
-
993
- def to_maql_create
994
- maql = "CREATE ATTRIBUTE {#{identifier}} VISUAL (#{visual})" \
995
- + " AS KEYS {#{table}.#{Model::FIELD_PK}} FULLSET;\n"
996
- maql += @primary_label.to_maql_create if @primary_label
997
- maql
998
- end
999
-
1000
- def to_manifest_part(mode)
1001
- {
1002
- 'referenceKey' => 1,
1003
- 'populates' => [@primary_label.identifier],
1004
- 'mode' => mode,
1005
- 'columnName' => name
1006
- }
1007
- end
1008
-
1009
- def to_wire_model
1010
- {
1011
- 'attribute' => {
1012
- 'identifier' => identifier,
1013
- 'title' => title,
1014
- 'labels' => labels.map do |l|
1015
- {
1016
- 'label' => {
1017
- 'identifier' => l.identifier,
1018
- 'title' => l.title,
1019
- 'type' => 'GDC.text'
1020
- }
1021
- }
1022
- end
1023
- }
1024
- }
1025
- end
1026
- end
1027
-
1028
- ##
1029
- # GoodData display form abstraction. Represents a default representation
1030
- # of an attribute column or an additional representation defined in a LABEL
1031
- # field
1032
- #
1033
- class Label < Column
1034
- attr_accessor :attribute
1035
-
1036
- def type_prefix;
1037
- 'label';
1038
- end
1039
-
1040
- # def initialize(hash, schema)
1041
- def initialize(hash, attribute, schema)
1042
- super hash, schema
1043
- attribute = attribute.nil? ? schema.fields.find { |field| field.name === hash[:reference] } : attribute
1044
- @attribute = attribute
1045
- attribute.labels << self
1046
- end
1047
-
1048
- def to_maql_create
1049
- '# LABEL FROM LABEL'
1050
- "ALTER ATTRIBUTE {#{@attribute.identifier}} ADD LABELS {#{identifier}}" \
1051
- + " VISUAL (TITLE #{title.inspect}) AS {#{column}};\n"
1052
- end
1053
-
1054
- def to_manifest_part(mode)
1055
- {
1056
- 'populates' => [identifier],
1057
- 'mode' => mode,
1058
- 'columnName' => name
1059
- }
1060
- end
1061
-
1062
- def column
1063
- "#{@attribute.table}.#{LABEL_COLUMN_PREFIX}#{name}"
1064
- end
1065
-
1066
- alias :inspect_orig :inspect
1067
-
1068
- def inspect
1069
- inspect_orig.sub(/>$/, " @attribute=#{@attribute.to_s.sub(/>$/, " @name=#{@attribute.name}")}>")
1070
- end
1071
- end
1072
-
1073
- ##
1074
- # A GoodData attribute that represents a data set's connection point or a data set
1075
- # without a connection point
1076
- #
1077
- class Anchor < Attribute
1078
- def initialize(column, schema)
1079
- if column then
1080
- super
1081
- else
1082
- super({:type => 'anchor', :name => 'id'}, schema)
1083
- @labels = []
1084
- @primary_label = nil
1085
- end
1086
- end
1087
-
1088
- def table
1089
- @table ||= 'f_' + @schema.name
1090
- end
1091
-
1092
- def to_maql_create
1093
- maql = super
1094
- maql += "\n# Connect '#{self.title}' to all attributes of this data set\n"
1095
- @schema.attributes.values.each do |c|
1096
- maql += "ALTER ATTRIBUTE {#{c.identifier}} ADD KEYS " \
1097
- + "{#{table}.#{c.key}};\n"
1098
- end
1099
- maql
1100
- end
1101
- end
1102
-
1103
- ##
1104
- # GoodData fact abstraction
1105
- #
1106
- class Fact < Column
1107
- def type_prefix;
1108
- FACT_PREFIX;
1109
- end
1110
-
1111
- def column_prefix;
1112
- FACT_COLUMN_PREFIX;
1113
- end
1114
-
1115
- def folder_prefix;
1116
- FACT_FOLDER_PREFIX;
1117
- end
1118
-
1119
- def table
1120
- @schema.table
1121
- end
1122
-
1123
- def column
1124
- @column ||= table + '.' + column_prefix + name
1125
- end
1126
-
1127
- def to_maql_create
1128
- "CREATE FACT {#{self.identifier}} VISUAL (#{visual})" \
1129
- + " AS {#{column}};\n"
1130
- end
1131
-
1132
- def to_manifest_part(mode)
1133
- {
1134
- 'populates' => [identifier],
1135
- 'mode' => mode,
1136
- 'columnName' => name
1137
- }
1138
- end
1139
-
1140
- def to_wire_model
1141
- {
1142
- 'fact' => {
1143
- 'identifier' => identifier,
1144
- 'title' => title
1145
- }
1146
- }
1147
- end
1148
- end
1149
-
1150
- ##
1151
- # Reference to another data set
1152
- #
1153
- class Reference < Column
1154
- attr_accessor :reference, :schema_ref
1155
-
1156
- def initialize(column, schema)
1157
- super column, schema
1158
- # pp column
1159
- @name = column[:name]
1160
- @reference = column[:reference]
1161
- @schema_ref = column[:dataset]
1162
- @schema = schema
1163
- end
1164
-
1165
- ##
1166
- # Generates an identifier of the referencing attribute using the
1167
- # schema name derived from schemaReference and column name derived
1168
- # from the reference key.
1169
- #
1170
- def identifier
1171
- @identifier ||= "#{ATTRIBUTE_PREFIX}.#{@schema_ref}.#{@reference}"
1172
- end
1173
-
1174
- def key;
1175
- "#{@name}_id";
1176
- end
1177
-
1178
- def label_column
1179
- "#{LABEL_PREFIX}.#{@schema_ref}.#{@reference}"
1180
- end
1181
-
1182
- def to_maql_create
1183
- "ALTER ATTRIBUTE {#{self.identifier}} ADD KEYS {#{@schema.table}.#{key}};\n"
1184
- end
1185
-
1186
- def to_maql_drop
1187
- "ALTER ATTRIBUTE {#{self.identifier} DROP KEYS {#{@schema.table}.#{key}};\n"
1188
- end
1189
-
1190
- def to_manifest_part(mode)
1191
- {
1192
- 'populates' => [label_column],
1193
- 'mode' => mode,
1194
- 'columnName' => name,
1195
- 'referenceKey' => 1
1196
- }
1197
- end
1198
- end
1199
-
1200
- ##
1201
- # Date as a reference to a date dimension
1202
- #
1203
- class DateReference < Reference
1204
- attr_accessor :format, :output_format, :urn
1205
-
1206
- def initialize(column, schema)
1207
- super column, schema
1208
- @output_format = column['format'] || 'dd/MM/yyyy'
1209
- @format = @output_format.gsub('yyyy', '%Y').gsub('MM', '%m').gsub('dd', '%d')
1210
- @urn = column[:urn] || 'URN:GOODDATA:DATE'
1211
- end
1212
-
1213
- def identifier
1214
- @identifier ||= "#{@schema_ref}.#{DATE_ATTRIBUTE}"
1215
- end
1216
-
1217
- def to_manifest_part(mode)
1218
- {
1219
- 'populates' => ["#{identifier}.#{DATE_ATTRIBUTE_DEFAULT_DISPLAY_FORM}"],
1220
- 'mode' => mode,
1221
- 'constraints' => {'date' => output_format},
1222
- 'columnName' => name,
1223
- 'referenceKey' => 1
1224
- }
1225
- end
1226
-
1227
- # def to_maql_create
1228
- # # urn:chefs_warehouse_fiscal:date
1229
- # super_maql = super
1230
- # maql = ""
1231
- # # maql = "# Include date dimensions\n"
1232
- # # maql += "INCLUDE TEMPLATE \"#{urn}\" MODIFY (IDENTIFIER \"#{name}\", TITLE \"#{title || name}\");\n"
1233
- # maql += super_maql
1234
- # end
1235
- end
1236
-
1237
- ##
1238
- # Date field that's not connected to a date dimension
1239
- #
1240
- class DateAttribute < Attribute
1241
- def key;
1242
- "#{DATE_COLUMN_PREFIX}#{super}";
1243
- end
1244
-
1245
- def to_manifest_part(mode)
1246
- {
1247
- 'populates' => ['label.stuff.mmddyy'],
1248
- 'format' => 'unknown',
1249
- 'mode' => mode,
1250
- 'referenceKey' => 1
1251
- }
1252
- end
1253
- end
1254
-
1255
- ##
1256
- # Fact representation of a time of a day
1257
- #
1258
- class TimeFact < Fact
1259
- def column_prefix;
1260
- TIME_COLUMN_PREFIX;
1261
- end
1262
-
1263
- def type_prefix;
1264
- TIME_FACT_PREFIX;
1265
- end
1266
- end
1267
-
1268
- ##
1269
- # Time as a reference to a time-of-a-day dimension
1270
- #
1271
- class TimeReference < Reference
1272
- end
1273
-
1274
- ##
1275
- # Time field that's not connected to a time-of-a-day dimension
1276
- #
1277
- class TimeAttribute < Attribute
1278
- def type_prefix;
1279
- TIME_ATTRIBUTE_PREFIX;
1280
- end
1281
-
1282
- def key;
1283
- "#{TIME_COLUMN_PREFIX}#{super}";
1284
- end
1285
-
1286
- def table;
1287
- @table ||= "#{super}_tm";
1288
- end
1289
- end
1290
-
1291
- ##
1292
- # Date column. A container holding the following
1293
- # parts: date fact, a date reference or attribute and an optional time component
1294
- # that contains a time fact and a time reference or attribute.
1295
- #
1296
- class DateColumn < Column
1297
- attr_reader :parts, :facts, :attributes, :references
1298
-
1299
- def initialize(column, schema)
1300
- super column, schema
1301
- @parts = {}; @facts = []; @attributes = []; @references = []
1302
-
1303
- # @facts << @parts[:date_fact] = DateFact.new(column, schema)
1304
- if column[:dataset] then
1305
- @parts[:date_ref] = DateReference.new column, schema
1306
- @references << @parts[:date_ref]
1307
- else
1308
- @attributes << @parts[:date_attr] = DateAttribute.new(column, schema)
1309
- end
1310
- # if column['datetime'] then
1311
- # puts "*** datetime"
1312
- # @facts << @parts[:time_fact] = TimeFact.new(column, schema)
1313
- # if column['schema_reference'] then
1314
- # @parts[:time_ref] = TimeReference.new column, schema
1315
- # else
1316
- # @attributes << @parts[:time_attr] = TimeAttribute.new(column, schema)
1317
- # end
1318
- # end
1319
- end
1320
-
1321
- def to_maql_create
1322
- @parts.values.map { |v| v.to_maql_create }.join "\n"
1323
- end
1324
-
1325
- def to_maql_drop
1326
- @parts.values.map { |v| v.to_maql_drop }.join "\n"
1327
- end
1328
-
1329
- def to_csv_header(row)
1330
- SKIP_FIELD
1331
- end
1332
-
1333
- def to_csv_data(headers, row)
1334
- SKIP_FIELD
1335
- end
1336
-
1337
- def to_manifest_part(mode)
1338
- nil
1339
- end
1340
- end
1341
-
1342
- ##
1343
- # Base class for GoodData attribute and fact folder abstractions
1344
- #
1345
- class Folder < MdObject
1346
- def initialize(title)
1347
- # TODO: should a super be here?
1348
- # how to deal with name vs title?
1349
- @title = title
1350
- @name = GoodData::Helpers.sanitize_string(title)
1351
- end
1352
-
1353
- def to_maql_create
1354
- "CREATE FOLDER {#{type_prefix}.#{name}}" \
1355
- + " VISUAL (#{visual}) TYPE #{type};\n"
1356
- end
1357
- end
1358
-
1359
- ##
1360
- # GoodData attribute folder abstraction
1361
- #
1362
- class AttributeFolder < Folder
1363
- def type;
1364
- 'ATTRIBUTE'
1365
- end
1366
-
1367
- def type_prefix;
1368
- 'dim'
1369
- end
1370
- end
1371
-
1372
- ##
1373
- # GoodData fact folder abstraction
1374
- #
1375
- class FactFolder < Folder
1376
- def type;
1377
- 'FACT'
1378
- end
1379
-
1380
- def type_prefix;
1381
- 'ffld'
1382
- end
1383
- end
1384
-
1385
- class DateDimension < MdObject
1386
- def initialize(spec={})
1387
- super()
1388
- @name = spec[:name]
1389
- @title = spec[:title] || @name
1390
- @urn = spec[:urn] || 'URN:GOODDATA:DATE'
1391
- end
1392
-
1393
- def to_maql_create
1394
- # urn = "urn:chefs_warehouse_fiscal:date"
1395
- # title = "title"
1396
- # name = "name"
1397
-
1398
- maql = ''
1399
- maql += "INCLUDE TEMPLATE \"#{@urn}\" MODIFY (IDENTIFIER \"#{@name}\", TITLE \"#{@title}\");"
1400
- maql
1401
- end
1402
- end
1403
102
  end
1404
- end
103
+ end