gooddata-edge 0.6.27.edge

Sign up to get free protection for your applications and to get access to all the features.
Files changed (364) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.gitignore +36 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +89 -0
  6. data/.yardopts +22 -0
  7. data/CHANGELOG.md +196 -0
  8. data/CLI.md +439 -0
  9. data/DEPENDENCIES.md +817 -0
  10. data/Gemfile +4 -0
  11. data/Guardfile +5 -0
  12. data/LICENSE +22 -0
  13. data/LICENSE.rb +5 -0
  14. data/README.md +75 -0
  15. data/Rakefile +179 -0
  16. data/TODO.md +32 -0
  17. data/authors.sh +4 -0
  18. data/bin/gooddata +7 -0
  19. data/dependency_decisions.yml +104 -0
  20. data/gooddata +9 -0
  21. data/gooddata.gemspec +63 -0
  22. data/lib/gooddata.rb +31 -0
  23. data/lib/gooddata/app/app.rb +16 -0
  24. data/lib/gooddata/bricks/base_downloader.rb +86 -0
  25. data/lib/gooddata/bricks/brick.rb +38 -0
  26. data/lib/gooddata/bricks/bricks.rb +15 -0
  27. data/lib/gooddata/bricks/middleware/aws_middleware.rb +29 -0
  28. data/lib/gooddata/bricks/middleware/base_middleware.rb +56 -0
  29. data/lib/gooddata/bricks/middleware/bench_middleware.rb +24 -0
  30. data/lib/gooddata/bricks/middleware/bulk_salesforce_middleware.rb +37 -0
  31. data/lib/gooddata/bricks/middleware/decode_params_middleware.rb +20 -0
  32. data/lib/gooddata/bricks/middleware/fs_download_middleware.rb +48 -0
  33. data/lib/gooddata/bricks/middleware/fs_upload_middleware.rb +36 -0
  34. data/lib/gooddata/bricks/middleware/gooddata_middleware.rb +39 -0
  35. data/lib/gooddata/bricks/middleware/logger_middleware.rb +29 -0
  36. data/lib/gooddata/bricks/middleware/middleware.rb +12 -0
  37. data/lib/gooddata/bricks/middleware/restforce_middleware.rb +61 -0
  38. data/lib/gooddata/bricks/middleware/stdout_middleware.rb +23 -0
  39. data/lib/gooddata/bricks/middleware/twitter_middleware.rb +29 -0
  40. data/lib/gooddata/bricks/middleware/undot_params_middleware.rb +37 -0
  41. data/lib/gooddata/bricks/pipeline.rb +32 -0
  42. data/lib/gooddata/bricks/utils.rb +18 -0
  43. data/lib/gooddata/cli/cli.rb +27 -0
  44. data/lib/gooddata/cli/commands/auth_cmd.rb +29 -0
  45. data/lib/gooddata/cli/commands/domain_cmd.rb +28 -0
  46. data/lib/gooddata/cli/commands/project_cmd.rb +45 -0
  47. data/lib/gooddata/cli/hooks.rb +57 -0
  48. data/lib/gooddata/cli/shared.rb +61 -0
  49. data/lib/gooddata/cli/terminal.rb +20 -0
  50. data/lib/gooddata/client.rb +67 -0
  51. data/lib/gooddata/commands/api.rb +64 -0
  52. data/lib/gooddata/commands/auth.rb +107 -0
  53. data/lib/gooddata/commands/base.rb +12 -0
  54. data/lib/gooddata/commands/commands.rb +12 -0
  55. data/lib/gooddata/commands/datasets.rb +148 -0
  56. data/lib/gooddata/commands/datawarehouse.rb +20 -0
  57. data/lib/gooddata/commands/domain.rb +40 -0
  58. data/lib/gooddata/commands/process.rb +67 -0
  59. data/lib/gooddata/commands/project.rb +175 -0
  60. data/lib/gooddata/commands/projects.rb +20 -0
  61. data/lib/gooddata/commands/role.rb +36 -0
  62. data/lib/gooddata/commands/runners.rb +47 -0
  63. data/lib/gooddata/commands/scaffold.rb +69 -0
  64. data/lib/gooddata/commands/user.rb +39 -0
  65. data/lib/gooddata/connection.rb +127 -0
  66. data/lib/gooddata/core/core.rb +12 -0
  67. data/lib/gooddata/core/logging.rb +105 -0
  68. data/lib/gooddata/core/nil_logger.rb +23 -0
  69. data/lib/gooddata/core/project.rb +74 -0
  70. data/lib/gooddata/core/rest.rb +149 -0
  71. data/lib/gooddata/core/user.rb +20 -0
  72. data/lib/gooddata/data/data.rb +12 -0
  73. data/lib/gooddata/data/guesser.rb +122 -0
  74. data/lib/gooddata/exceptions/attr_element_not_found.rb +16 -0
  75. data/lib/gooddata/exceptions/command_failed.rb +11 -0
  76. data/lib/gooddata/exceptions/exceptions.rb +12 -0
  77. data/lib/gooddata/exceptions/execution_limit_exceeded.rb +13 -0
  78. data/lib/gooddata/exceptions/filter_maqlization.rb +16 -0
  79. data/lib/gooddata/exceptions/malformed_user.rb +15 -0
  80. data/lib/gooddata/exceptions/no_project_error.rb +15 -0
  81. data/lib/gooddata/exceptions/object_migration.rb +32 -0
  82. data/lib/gooddata/exceptions/project_not_found.rb +13 -0
  83. data/lib/gooddata/exceptions/segment_not_empty.rb +18 -0
  84. data/lib/gooddata/exceptions/uncomputable_report.rb +13 -0
  85. data/lib/gooddata/exceptions/user_in_different_domain.rb +15 -0
  86. data/lib/gooddata/exceptions/validation_error.rb +16 -0
  87. data/lib/gooddata/extensions/big_decimal.rb +17 -0
  88. data/lib/gooddata/extensions/enumerable.rb +39 -0
  89. data/lib/gooddata/extensions/extensions.rb +10 -0
  90. data/lib/gooddata/extensions/false.rb +15 -0
  91. data/lib/gooddata/extensions/hash.rb +38 -0
  92. data/lib/gooddata/extensions/nil.rb +15 -0
  93. data/lib/gooddata/extensions/numeric.rb +15 -0
  94. data/lib/gooddata/extensions/object.rb +27 -0
  95. data/lib/gooddata/extensions/symbol.rb +15 -0
  96. data/lib/gooddata/extensions/true.rb +15 -0
  97. data/lib/gooddata/extract.rb +21 -0
  98. data/lib/gooddata/goodzilla/goodzilla.rb +159 -0
  99. data/lib/gooddata/helpers/auth_helpers.rb +75 -0
  100. data/lib/gooddata/helpers/csv_helper.rb +61 -0
  101. data/lib/gooddata/helpers/data_helper.rb +116 -0
  102. data/lib/gooddata/helpers/global_helpers.rb +331 -0
  103. data/lib/gooddata/helpers/global_helpers_params.rb +172 -0
  104. data/lib/gooddata/helpers/helpers.rb +10 -0
  105. data/lib/gooddata/mixins/author.rb +26 -0
  106. data/lib/gooddata/mixins/content_getter.rb +15 -0
  107. data/lib/gooddata/mixins/content_property_reader.rb +17 -0
  108. data/lib/gooddata/mixins/content_property_writer.rb +17 -0
  109. data/lib/gooddata/mixins/contributor.rb +20 -0
  110. data/lib/gooddata/mixins/data_getter.rb +15 -0
  111. data/lib/gooddata/mixins/data_property_reader.rb +19 -0
  112. data/lib/gooddata/mixins/data_property_writer.rb +19 -0
  113. data/lib/gooddata/mixins/inspector.rb +53 -0
  114. data/lib/gooddata/mixins/is_attribute.rb +17 -0
  115. data/lib/gooddata/mixins/is_dimension.rb +17 -0
  116. data/lib/gooddata/mixins/is_fact.rb +17 -0
  117. data/lib/gooddata/mixins/is_label.rb +19 -0
  118. data/lib/gooddata/mixins/links.rb +15 -0
  119. data/lib/gooddata/mixins/md_finders.rb +77 -0
  120. data/lib/gooddata/mixins/md_grantees.rb +42 -0
  121. data/lib/gooddata/mixins/md_id_to_uri.rb +34 -0
  122. data/lib/gooddata/mixins/md_json.rb +15 -0
  123. data/lib/gooddata/mixins/md_lock.rb +87 -0
  124. data/lib/gooddata/mixins/md_object_id.rb +15 -0
  125. data/lib/gooddata/mixins/md_object_indexer.rb +64 -0
  126. data/lib/gooddata/mixins/md_object_query.rb +128 -0
  127. data/lib/gooddata/mixins/md_relations.rb +43 -0
  128. data/lib/gooddata/mixins/meta_getter.rb +17 -0
  129. data/lib/gooddata/mixins/meta_property_reader.rb +19 -0
  130. data/lib/gooddata/mixins/meta_property_writer.rb +19 -0
  131. data/lib/gooddata/mixins/mixins.rb +19 -0
  132. data/lib/gooddata/mixins/not_attribute.rb +17 -0
  133. data/lib/gooddata/mixins/not_exportable.rb +15 -0
  134. data/lib/gooddata/mixins/not_fact.rb +17 -0
  135. data/lib/gooddata/mixins/not_group.rb +17 -0
  136. data/lib/gooddata/mixins/not_label.rb +19 -0
  137. data/lib/gooddata/mixins/not_metric.rb +19 -0
  138. data/lib/gooddata/mixins/obj_id.rb +15 -0
  139. data/lib/gooddata/mixins/rest_getters.rb +17 -0
  140. data/lib/gooddata/mixins/rest_resource.rb +47 -0
  141. data/lib/gooddata/mixins/root_key_getter.rb +15 -0
  142. data/lib/gooddata/mixins/root_key_setter.rb +15 -0
  143. data/lib/gooddata/mixins/timestamps.rb +19 -0
  144. data/lib/gooddata/mixins/to_json.rb +11 -0
  145. data/lib/gooddata/mixins/uri_getter.rb +9 -0
  146. data/lib/gooddata/models/blueprint/anchor_field.rb +64 -0
  147. data/lib/gooddata/models/blueprint/attribute_field.rb +29 -0
  148. data/lib/gooddata/models/blueprint/blueprint.rb +11 -0
  149. data/lib/gooddata/models/blueprint/blueprint_field.rb +70 -0
  150. data/lib/gooddata/models/blueprint/dashboard_builder.rb +30 -0
  151. data/lib/gooddata/models/blueprint/dataset_blueprint.rb +449 -0
  152. data/lib/gooddata/models/blueprint/date_dimension.rb +14 -0
  153. data/lib/gooddata/models/blueprint/fact_field.rb +20 -0
  154. data/lib/gooddata/models/blueprint/label_field.rb +43 -0
  155. data/lib/gooddata/models/blueprint/project_blueprint.rb +746 -0
  156. data/lib/gooddata/models/blueprint/project_builder.rb +83 -0
  157. data/lib/gooddata/models/blueprint/reference_field.rb +43 -0
  158. data/lib/gooddata/models/blueprint/schema_blueprint.rb +160 -0
  159. data/lib/gooddata/models/blueprint/schema_builder.rb +89 -0
  160. data/lib/gooddata/models/blueprint/to_manifest.rb +181 -0
  161. data/lib/gooddata/models/blueprint/to_wire.rb +154 -0
  162. data/lib/gooddata/models/client.rb +182 -0
  163. data/lib/gooddata/models/client_synchronization_result.rb +31 -0
  164. data/lib/gooddata/models/client_synchronization_result_details.rb +41 -0
  165. data/lib/gooddata/models/datawarehouse.rb +92 -0
  166. data/lib/gooddata/models/domain.rb +479 -0
  167. data/lib/gooddata/models/execution.rb +115 -0
  168. data/lib/gooddata/models/execution_detail.rb +81 -0
  169. data/lib/gooddata/models/from_wire.rb +160 -0
  170. data/lib/gooddata/models/invitation.rb +75 -0
  171. data/lib/gooddata/models/links.rb +50 -0
  172. data/lib/gooddata/models/membership.rb +441 -0
  173. data/lib/gooddata/models/metadata.rb +272 -0
  174. data/lib/gooddata/models/metadata/attribute.rb +134 -0
  175. data/lib/gooddata/models/metadata/dashboard.rb +108 -0
  176. data/lib/gooddata/models/metadata/dashboard/dashboard_item.rb +76 -0
  177. data/lib/gooddata/models/metadata/dashboard/filter_apply_item.rb +37 -0
  178. data/lib/gooddata/models/metadata/dashboard/filter_item.rb +64 -0
  179. data/lib/gooddata/models/metadata/dashboard/geo_chart_item.rb +56 -0
  180. data/lib/gooddata/models/metadata/dashboard/headline_item.rb +56 -0
  181. data/lib/gooddata/models/metadata/dashboard/iframe_item.rb +46 -0
  182. data/lib/gooddata/models/metadata/dashboard/report_item.rb +92 -0
  183. data/lib/gooddata/models/metadata/dashboard/text_item.rb +55 -0
  184. data/lib/gooddata/models/metadata/dashboard_tab.rb +141 -0
  185. data/lib/gooddata/models/metadata/dataset.rb +64 -0
  186. data/lib/gooddata/models/metadata/dimension.rb +54 -0
  187. data/lib/gooddata/models/metadata/fact.rb +44 -0
  188. data/lib/gooddata/models/metadata/label.rb +128 -0
  189. data/lib/gooddata/models/metadata/metadata.rb +12 -0
  190. data/lib/gooddata/models/metadata/metric.rb +198 -0
  191. data/lib/gooddata/models/metadata/report.rb +247 -0
  192. data/lib/gooddata/models/metadata/report_definition.rb +264 -0
  193. data/lib/gooddata/models/metadata/scheduled_mail.rb +274 -0
  194. data/lib/gooddata/models/metadata/scheduled_mail/dashboard_attachment.rb +62 -0
  195. data/lib/gooddata/models/metadata/scheduled_mail/report_attachment.rb +64 -0
  196. data/lib/gooddata/models/metadata/variable.rb +91 -0
  197. data/lib/gooddata/models/model.rb +282 -0
  198. data/lib/gooddata/models/models.rb +12 -0
  199. data/lib/gooddata/models/module_constants.rb +31 -0
  200. data/lib/gooddata/models/process.rb +316 -0
  201. data/lib/gooddata/models/profile.rb +426 -0
  202. data/lib/gooddata/models/project.rb +1514 -0
  203. data/lib/gooddata/models/project_creator.rb +126 -0
  204. data/lib/gooddata/models/project_metadata.rb +67 -0
  205. data/lib/gooddata/models/project_role.rb +79 -0
  206. data/lib/gooddata/models/report_data_result.rb +266 -0
  207. data/lib/gooddata/models/schedule.rb +518 -0
  208. data/lib/gooddata/models/segment.rb +201 -0
  209. data/lib/gooddata/models/tab_builder.rb +27 -0
  210. data/lib/gooddata/models/user_filters/mandatory_user_filter.rb +76 -0
  211. data/lib/gooddata/models/user_filters/user_filter.rb +100 -0
  212. data/lib/gooddata/models/user_filters/user_filter_builder.rb +512 -0
  213. data/lib/gooddata/models/user_filters/user_filters.rb +13 -0
  214. data/lib/gooddata/models/user_filters/variable_user_filter.rb +31 -0
  215. data/lib/gooddata/models/user_group.rb +241 -0
  216. data/lib/gooddata/rest/README.md +37 -0
  217. data/lib/gooddata/rest/client.rb +389 -0
  218. data/lib/gooddata/rest/connection.rb +765 -0
  219. data/lib/gooddata/rest/object.rb +69 -0
  220. data/lib/gooddata/rest/object_factory.rb +76 -0
  221. data/lib/gooddata/rest/resource.rb +27 -0
  222. data/lib/gooddata/rest/rest.rb +24 -0
  223. data/lib/gooddata/version.rb +23 -0
  224. data/lib/templates/bricks/brick.rb.erb +7 -0
  225. data/lib/templates/bricks/main.rb.erb +5 -0
  226. data/lib/templates/project/Goodfile.erb +4 -0
  227. data/lib/templates/project/data/commits.csv +4 -0
  228. data/lib/templates/project/data/devs.csv +4 -0
  229. data/lib/templates/project/data/repos.csv +3 -0
  230. data/lib/templates/project/model/model.rb.erb +20 -0
  231. data/spec/bricks/bricks_spec.rb +112 -0
  232. data/spec/bricks/default-config.json +8 -0
  233. data/spec/data/.gooddata +4 -0
  234. data/spec/data/blueprints/additional_dataset_module.json +32 -0
  235. data/spec/data/blueprints/big_blueprint_not_pruned.json +2079 -0
  236. data/spec/data/blueprints/invalid_blueprint.json +103 -0
  237. data/spec/data/blueprints/m_n_model.json +104 -0
  238. data/spec/data/blueprints/model_module.json +25 -0
  239. data/spec/data/blueprints/test_blueprint.json +38 -0
  240. data/spec/data/blueprints/test_project_model_spec.json +106 -0
  241. data/spec/data/cc/data/source/commits.csv +4 -0
  242. data/spec/data/cc/data/source/devs.csv +4 -0
  243. data/spec/data/cc/data/source/repos.csv +3 -0
  244. data/spec/data/cc/devel.prm +0 -0
  245. data/spec/data/cc/graph/graph.grf +11 -0
  246. data/spec/data/cc/workspace.prm +19 -0
  247. data/spec/data/column_based_permissions.csv +7 -0
  248. data/spec/data/column_based_permissions2.csv +6 -0
  249. data/spec/data/gd_gse_data_blueprint.json +1371 -0
  250. data/spec/data/gd_gse_data_manifest.json +1424 -0
  251. data/spec/data/gd_gse_data_model.json +1772 -0
  252. data/spec/data/gooddata_version_process/gooddata_version.rb +9 -0
  253. data/spec/data/gooddata_version_process/gooddata_version.zip +0 -0
  254. data/spec/data/hello_world_process/hello_world.rb +9 -0
  255. data/spec/data/hello_world_process/hello_world.zip +0 -0
  256. data/spec/data/line_based_permissions.csv +3 -0
  257. data/spec/data/manifests/test_blueprint.json +32 -0
  258. data/spec/data/manifests/test_project.json +107 -0
  259. data/spec/data/reports/left_attr_report.json +108 -0
  260. data/spec/data/reports/metric_only_one_line.json +83 -0
  261. data/spec/data/reports/report_1.json +197 -0
  262. data/spec/data/reports/top_attr_report.json +108 -0
  263. data/spec/data/ruby_params_process/ruby_params.rb +9 -0
  264. data/spec/data/ruby_process/deep_files/deep_stuff.txt +1 -0
  265. data/spec/data/ruby_process/process.rb +8 -0
  266. data/spec/data/ruby_process/stuff.txt +1 -0
  267. data/spec/data/superfluous_titles_view.json +81 -0
  268. data/spec/data/test-ci-data.csv +2 -0
  269. data/spec/data/users.csv +12 -0
  270. data/spec/data/wire_models/model_view.json +1775 -0
  271. data/spec/data/wire_models/nu_model.json +3046 -0
  272. data/spec/data/wire_models/test_blueprint.json +63 -0
  273. data/spec/data/wire_test_project.json +150 -0
  274. data/spec/environment/default.rb +41 -0
  275. data/spec/environment/develop.rb +31 -0
  276. data/spec/environment/environment.rb +18 -0
  277. data/spec/environment/hotfix.rb +21 -0
  278. data/spec/environment/production.rb +35 -0
  279. data/spec/environment/release.rb +21 -0
  280. data/spec/environment/staging.rb +30 -0
  281. data/spec/environment/staging_3.rb +36 -0
  282. data/spec/helpers/blueprint_helper.rb +26 -0
  283. data/spec/helpers/cli_helper.rb +36 -0
  284. data/spec/helpers/connection_helper.rb +41 -0
  285. data/spec/helpers/crypto_helper.rb +17 -0
  286. data/spec/helpers/csv_helper.rb +18 -0
  287. data/spec/helpers/process_helper.rb +33 -0
  288. data/spec/helpers/project_helper.rb +59 -0
  289. data/spec/helpers/schedule_helper.rb +31 -0
  290. data/spec/helpers/spec_helper.rb +15 -0
  291. data/spec/integration/blueprint_updates_spec.rb +101 -0
  292. data/spec/integration/blueprint_with_grain_spec.rb +72 -0
  293. data/spec/integration/clients_spec.rb +134 -0
  294. data/spec/integration/command_datawarehouse_spec.rb +39 -0
  295. data/spec/integration/command_projects_spec.rb +32 -0
  296. data/spec/integration/create_from_template_spec.rb +22 -0
  297. data/spec/integration/create_project_spec.rb +24 -0
  298. data/spec/integration/date_dim_switch_spec.rb +142 -0
  299. data/spec/integration/deprecated_load_spec.rb +58 -0
  300. data/spec/integration/full_process_schedule_spec.rb +298 -0
  301. data/spec/integration/full_project_spec.rb +569 -0
  302. data/spec/integration/over_to_user_filters_spec.rb +94 -0
  303. data/spec/integration/partial_md_export_import_spec.rb +42 -0
  304. data/spec/integration/project_spec.rb +264 -0
  305. data/spec/integration/rest_spec.rb +213 -0
  306. data/spec/integration/schedule_spec.rb +626 -0
  307. data/spec/integration/segments_spec.rb +141 -0
  308. data/spec/integration/user_filters_spec.rb +290 -0
  309. data/spec/integration/user_group_spec.rb +127 -0
  310. data/spec/integration/variables_spec.rb +188 -0
  311. data/spec/logging_in_logging_out_spec.rb +93 -0
  312. data/spec/spec_helper.rb +95 -0
  313. data/spec/unit/bricks/bricks_spec.rb +35 -0
  314. data/spec/unit/bricks/middleware/aws_middelware_spec.rb +51 -0
  315. data/spec/unit/bricks/middleware/bench_middleware_spec.rb +15 -0
  316. data/spec/unit/bricks/middleware/bulk_salesforce_middleware_spec.rb +15 -0
  317. data/spec/unit/bricks/middleware/gooddata_middleware_spec.rb +15 -0
  318. data/spec/unit/bricks/middleware/logger_middleware_spec.rb +15 -0
  319. data/spec/unit/bricks/middleware/restforce_middleware_spec.rb +15 -0
  320. data/spec/unit/bricks/middleware/stdout_middleware_spec.rb +15 -0
  321. data/spec/unit/bricks/middleware/twitter_middleware_spec.rb +15 -0
  322. data/spec/unit/cli/cli_spec.rb +17 -0
  323. data/spec/unit/cli/commands/cmd_auth_spec.rb +17 -0
  324. data/spec/unit/commands/command_projects_spec.rb +22 -0
  325. data/spec/unit/core/connection_spec.rb +57 -0
  326. data/spec/unit/core/logging_spec.rb +133 -0
  327. data/spec/unit/core/nil_logger_spec.rb +13 -0
  328. data/spec/unit/core/project_spec.rb +54 -0
  329. data/spec/unit/extensions/hash_spec.rb +23 -0
  330. data/spec/unit/godzilla/goodzilla_spec.rb +78 -0
  331. data/spec/unit/helpers/csv_helper_spec.rb +22 -0
  332. data/spec/unit/helpers/data_helper_spec.rb +61 -0
  333. data/spec/unit/helpers/global_helpers_spec.rb +111 -0
  334. data/spec/unit/helpers_spec.rb +86 -0
  335. data/spec/unit/models/blueprint/attributes_spec.rb +29 -0
  336. data/spec/unit/models/blueprint/dataset_spec.rb +121 -0
  337. data/spec/unit/models/blueprint/labels_spec.rb +44 -0
  338. data/spec/unit/models/blueprint/project_blueprint_spec.rb +648 -0
  339. data/spec/unit/models/blueprint/reference_spec.rb +29 -0
  340. data/spec/unit/models/blueprint/schema_builder_spec.rb +38 -0
  341. data/spec/unit/models/blueprint/to_wire_spec.rb +174 -0
  342. data/spec/unit/models/domain_spec.rb +144 -0
  343. data/spec/unit/models/execution_spec.rb +108 -0
  344. data/spec/unit/models/from_wire_spec.rb +296 -0
  345. data/spec/unit/models/invitation_spec.rb +17 -0
  346. data/spec/unit/models/membership_spec.rb +132 -0
  347. data/spec/unit/models/metadata_spec.rb +104 -0
  348. data/spec/unit/models/metric_spec.rb +117 -0
  349. data/spec/unit/models/model_spec.rb +82 -0
  350. data/spec/unit/models/params_spec.rb +118 -0
  351. data/spec/unit/models/profile_spec.rb +215 -0
  352. data/spec/unit/models/project_creator_spec.rb +127 -0
  353. data/spec/unit/models/project_role_spec.rb +94 -0
  354. data/spec/unit/models/project_spec.rb +162 -0
  355. data/spec/unit/models/report_result_data_spec.rb +199 -0
  356. data/spec/unit/models/schedule_spec.rb +418 -0
  357. data/spec/unit/models/to_manifest_spec.rb +63 -0
  358. data/spec/unit/models/unit_project_spec.rb +125 -0
  359. data/spec/unit/models/user_filters_spec.rb +95 -0
  360. data/spec/unit/models/variable_spec.rb +265 -0
  361. data/spec/unit/rest/polling_spec.rb +89 -0
  362. data/spec/unit/rest/resource_spec.rb +10 -0
  363. data/yard-server.sh +3 -0
  364. metadata +1125 -0
@@ -0,0 +1,1514 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright (c) 2010-2015 GoodData Corporation. All rights reserved.
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ require 'csv'
8
+ require 'zip'
9
+ require 'fileutils'
10
+ require 'multi_json'
11
+ require 'pmap'
12
+ require 'zip'
13
+
14
+ require_relative '../exceptions/no_project_error'
15
+
16
+ require_relative '../helpers/auth_helpers'
17
+
18
+ require_relative '../rest/resource'
19
+ require_relative '../mixins/author'
20
+ require_relative '../mixins/contributor'
21
+ require_relative '../mixins/rest_resource'
22
+ require_relative '../mixins/uri_getter'
23
+
24
+ require_relative 'membership'
25
+ require_relative 'process'
26
+ require_relative 'project_role'
27
+ require_relative 'blueprint/blueprint'
28
+
29
+ require_relative 'metadata/scheduled_mail'
30
+ require_relative 'metadata/scheduled_mail/dashboard_attachment'
31
+ require_relative 'metadata/scheduled_mail/report_attachment'
32
+
33
+ module GoodData
34
+ class Project < Rest::Resource
35
+ USERSPROJECTS_PATH = '/gdc/account/profile/%s/projects'
36
+ PROJECTS_PATH = '/gdc/projects'
37
+ PROJECT_PATH = '/gdc/projects/%s'
38
+ SLIS_PATH = '/ldm/singleloadinterface'
39
+ DEFAULT_INVITE_MESSAGE = 'Join us!'
40
+ DEFAULT_ENVIRONMENT = 'PRODUCTION'
41
+
42
+ EMPTY_OBJECT = {
43
+ 'project' => {
44
+ 'meta' => {
45
+ 'summary' => 'No summary'
46
+ },
47
+ 'content' => {
48
+ 'guidedNavigation' => 1,
49
+ 'driver' => 'Pg',
50
+ 'environment' => GoodData::Helpers::AuthHelper.read_environment
51
+ }
52
+ }
53
+ }
54
+
55
+ attr_accessor :connection, :json
56
+
57
+ include Mixin::Author
58
+ include Mixin::Contributor
59
+ include Mixin::UriGetter
60
+
61
+ class << self
62
+ # Returns an array of all projects accessible by
63
+ # current user
64
+ def all(opts = { client: GoodData.connection })
65
+ c = client(opts)
66
+ c.user.projects
67
+ end
68
+
69
+ # Returns a Project object identified by given string
70
+ # The following identifiers are accepted
71
+ # - /gdc/md/<id>
72
+ # - /gdc/projects/<id>
73
+ # - <id>
74
+ #
75
+ def [](id, opts = { client: GoodData.connection })
76
+ return id if id.instance_of?(GoodData::Project) || id.respond_to?(:project?) && id.project?
77
+
78
+ if id == :all
79
+ Project.all({ client: GoodData.connection }.merge(opts))
80
+ else
81
+ if id.to_s !~ %r{^(\/gdc\/(projects|md)\/)?[a-zA-Z\d]+$}
82
+ fail(ArgumentError, 'wrong type of argument. Should be either project ID or path')
83
+ end
84
+
85
+ id = id.match(/[a-zA-Z\d]+$/)[0] if id =~ %r{/}
86
+
87
+ c = client(opts)
88
+ fail ArgumentError, 'No :client specified' if c.nil?
89
+
90
+ response = c.get(PROJECT_PATH % id)
91
+ c.factory.create(Project, response)
92
+ end
93
+ end
94
+
95
+ # Clones project along with etl and schedules
96
+ #
97
+ # @param project [Project] Project to be cloned from
98
+ # @param [options] Options that are passed into project.clone
99
+ # @return [GoodData::Project] New cloned project
100
+ def clone_with_etl(project, options = {})
101
+ a_clone = project.clone(options)
102
+ GoodData::Project.transfer_etl(project.client, project, a_clone)
103
+ a_clone
104
+ end
105
+
106
+ def create_object(data = {})
107
+ c = client(data)
108
+ new_data = GoodData::Helpers.deep_dup(EMPTY_OBJECT).tap do |d|
109
+ d['project']['meta']['title'] = data[:title]
110
+ d['project']['meta']['summary'] = data[:summary] if data[:summary]
111
+ d['project']['meta']['projectTemplate'] = data[:template] if data[:template]
112
+ d['project']['content']['guidedNavigation'] = data[:guided_navigation] if data[:guided_navigation]
113
+
114
+ token = data[:auth_token] || data[:token]
115
+
116
+ d['project']['content']['authorizationToken'] = token if token
117
+ d['project']['content']['driver'] = data[:driver] if data[:driver]
118
+ d['project']['content']['environment'] = data[:environment] if data[:environment]
119
+ end
120
+ c.create(Project, new_data)
121
+ end
122
+
123
+ # Create a project from a given attributes
124
+ # Expected keys:
125
+ # - :title (mandatory)
126
+ # - :summary
127
+ # - :template (default /projects/blank)
128
+ #
129
+ def create(opts = { :client => GoodData.connection }, &block)
130
+ GoodData.logger.info "Creating project #{opts[:title]}"
131
+
132
+ c = client(opts)
133
+ fail ArgumentError, 'No :client specified' if c.nil?
134
+
135
+ opts = { :auth_token => Helpers::AuthHelper.read_token }.merge(opts)
136
+ auth_token = opts[:auth_token] || opts[:token]
137
+ fail ArgumentError, 'You have to provide your token for creating projects as :auth_token parameter' if auth_token.nil? || auth_token.empty?
138
+
139
+ project = create_object(opts)
140
+ project.save
141
+ # until it is enabled or deleted, recur. This should still end if there is a exception thrown out from RESTClient. This sometimes happens from WebApp when request is too long
142
+ while project.state.to_s != 'enabled'
143
+ if project.deleted?
144
+ # if project is switched to deleted state, fail. This is usually problem of creating a template which is invalid.
145
+ fail 'Project was marked as deleted during creation. This usually means you were trying to create from template and it failed.'
146
+ end
147
+ sleep(3)
148
+ project.reload!
149
+ end
150
+
151
+ if block
152
+ GoodData.with_project(project) do |p|
153
+ block.call(p)
154
+ end
155
+ end
156
+ sleep 3
157
+ project
158
+ end
159
+
160
+ def find(_opts = {}, client = GoodData::Rest::Client.client)
161
+ user = client.user
162
+ user.projects['projects'].map do |project|
163
+ client.create(GoodData::Project, project)
164
+ end
165
+ end
166
+
167
+ def create_from_blueprint(blueprint, options = {})
168
+ GoodData::Model::ProjectCreator.migrate(options.merge(spec: blueprint, client: GoodData.connection))
169
+ end
170
+
171
+ # Takes one CSV line and creates hash from data extracted
172
+ #
173
+ # @param row CSV row
174
+ def user_csv_import(row)
175
+ {
176
+ 'user' => {
177
+ 'content' => {
178
+ 'email' => row[0],
179
+ 'login' => row[1],
180
+ 'firstname' => row[2],
181
+ 'lastname' => row[3]
182
+ },
183
+ 'meta' => {}
184
+ }
185
+ }
186
+ end
187
+
188
+ # Clones project along with etl and schedules.
189
+ #
190
+ # @param client [GoodData::Rest::Client] GoodData client to be used for connection
191
+ # @param from_project [GoodData::Project | GoodData::Segment | GoodData:Client | String] Object to be cloned from. Can be either segment in which case we take the master, client in which case we take its project, string in which case we treat is as an project object or directly project
192
+ def transfer_etl(client, from_project, to_project)
193
+ from_project = case from_project
194
+ when GoodData::Client
195
+ from_project.project
196
+ when GoodData::Segment
197
+ from_project.master_project
198
+ else
199
+ client.projects(from_project)
200
+ end
201
+
202
+ to_project = case to_project
203
+ when GoodData::Client
204
+ to_project.project
205
+ when GoodData::Segment
206
+ to_project.master_project
207
+ else
208
+ client.projects(to_project)
209
+ end
210
+
211
+ from_project.processes.each do |process|
212
+ Dir.mktmpdir('etl_transfer') do |dir|
213
+ dir = Pathname(dir)
214
+ filename = dir + 'process.zip'
215
+ File.open(filename, 'w') do |f|
216
+ f << process.download
217
+ end
218
+ to_process = to_project.processes.find { |p| p.name == process.name }
219
+ to_process ? to_process.deploy(filename, type: process.type, name: process.name) : to_project.deploy_process(filename, type: process.type, name: process.name)
220
+ end
221
+ end
222
+ res = (from_project.processes + to_project.processes).map { |p| [p, p.name, p.type] }
223
+ res.group_by { |x| [x[1], x[2]] }
224
+ .select { |_, procs| procs.length == 1 }
225
+ .flat_map { |_, procs| procs.select { |p| p[0].project.pid == to_project.pid }.map { |p| p[0] } }
226
+ .peach(&:delete)
227
+ transfer_schedules(from_project, to_project)
228
+ end
229
+
230
+ # Clones project along with etl and schedules.
231
+ #
232
+ # @param client [GoodData::Rest::Client] GoodData client to be used for connection
233
+ # @param from_project [GoodData::Project | GoodData::Segment | GoodData:Client | String] Object to be cloned from. Can be either segment in which case we take the master, client in which case we take its project, string in which case we treat is as an project object or directly project
234
+ def transfer_schedules(from_project, to_project)
235
+ cache = to_project.processes.sort_by(&:name).zip(from_project.processes.sort_by(&:name)).flat_map { |remote, local| local.schedules.map { |schedule| [remote, local, schedule] } }
236
+
237
+ remote_schedules = to_project.schedules
238
+ remote_stuff = remote_schedules.map do |s|
239
+ v = s.to_hash
240
+ after_schedule = remote_schedules.find { |s2| s.trigger_id == s2.obj_id }
241
+ v[:after] = s.trigger_id && after_schedule && after_schedule.name
242
+ v[:remote_schedule] = s
243
+ v[:params] = v[:params].except("EXECUTABLE", "PROCESS_ID")
244
+ v.compact
245
+ end
246
+
247
+ local_schedules = from_project.schedules
248
+ local_stuff = local_schedules.map do |s|
249
+ v = s.to_hash
250
+ after_schedule = local_schedules.find { |s2| s.trigger_id == s2.obj_id }
251
+ v[:after] = s.trigger_id && after_schedule && after_schedule.name
252
+ v[:remote_schedule] = s
253
+ v[:params] = v[:params].except("EXECUTABLE", "PROCESS_ID")
254
+ v.compact
255
+ end
256
+
257
+ diff = GoodData::Helpers.diff(remote_stuff, local_stuff, key: :name, fields: [:name, :cron, :after, :params, :hidden_params, :reschedule])
258
+ stack = diff[:added].map { |x| [:added, x] } + diff[:changed].map { |x| [:changed, x] }
259
+ schedule_cache = remote_schedules.reduce({}) do |a, e|
260
+ a[e.name] = e
261
+ a
262
+ end
263
+ messages = []
264
+ loop do
265
+ break if stack.empty?
266
+ state, changed_schedule = stack.shift
267
+ if state == :added
268
+ schedule_spec = changed_schedule
269
+ if schedule_spec[:after] && !schedule_cache[schedule_spec[:after]]
270
+ stack << [state, schedule_spec]
271
+ next
272
+ end
273
+ remote_process, process_spec = cache.find { |_remote, _local, schedule| schedule.name == schedule_spec[:name] }
274
+ messages << { message: "Creating schedule #{schedule_spec[:name]} for process #{remote_process.name}" }
275
+ executable = schedule_spec[:executable] || (process_spec["process_type"] == 'ruby' ? 'main.rb' : 'main.grf')
276
+ params = {
277
+ params: schedule_spec[:params].merge('PROJECT_ID' => to_project.pid),
278
+ hidden_params: schedule_spec[:hidden_params],
279
+ name: schedule_spec[:name],
280
+ reschedule: schedule_spec[:reschedule]
281
+ }
282
+ created_schedule = remote_process.create_schedule(schedule_spec[:cron] || schedule_cache[schedule_spec[:after]], executable, params)
283
+ schedule_cache[created_schedule.name] = created_schedule
284
+ else
285
+ schedule_spec = changed_schedule[:new_obj]
286
+ if schedule_spec[:after] && !schedule_cache[schedule_spec[:after]]
287
+ stack << [state, schedule_spec]
288
+ next
289
+ end
290
+ remote_process, process_spec = cache.find { |i| i[2].name == schedule_spec[:name] }
291
+ schedule = changed_schedule[:old_obj][:remote_schedule]
292
+ messages << { message: "Updating schedule #{schedule_spec[:name]} for process #{remote_process.name}" }
293
+ schedule.params = (schedule_spec[:params] || {})
294
+ schedule.cron = schedule_spec[:cron] if schedule_spec[:cron]
295
+ schedule.after = schedule_cache[schedule_spec[:after]] if schedule_spec[:after]
296
+ schedule.hidden_params = schedule_spec[:hidden_params] || {}
297
+ schedule.executable = schedule_spec[:executable] || (process_spec["process_type"] == 'ruby' ? 'main.rb' : 'main.grf')
298
+ schedule.reschedule = schedule_spec[:reschedule]
299
+ schedule.name = schedule_spec[:name]
300
+ schedule.save
301
+ schedule_cache[schedule.name] = schedule
302
+ end
303
+ end
304
+
305
+ diff[:removed].each do |removed_schedule|
306
+ messages << { message: "Removing schedule #{removed_schedule[:name]}" }
307
+ removed_schedule[:remote_schedule].delete
308
+ end
309
+ messages
310
+ # messages.map {|m| m.merge({custom_project_id: custom_project_id})}
311
+ end
312
+ end
313
+
314
+ def add_dashboard(dashboard)
315
+ GoodData::Dashboard.create(dashboard, :client => client, :project => self)
316
+ end
317
+
318
+ alias_method :create_dashboard, :add_dashboard
319
+
320
+ def add_user_group(data)
321
+ g = GoodData::UserGroup.create(data.merge(project: self))
322
+
323
+ begin
324
+ g.save
325
+ rescue RestClient::Conflict
326
+ user_groups(data[:name])
327
+ end
328
+ end
329
+
330
+ alias_method :create_group, :add_user_group
331
+
332
+ # Creates a metric in a project
333
+ #
334
+ # @param [options] Optional report options
335
+ # @return [GoodData::Report] Instance of new report
336
+ def add_metric(metric, options = {})
337
+ default = { client: client, project: self }
338
+ if metric.is_a?(String)
339
+ GoodData::Metric.xcreate(metric, options.merge(default))
340
+ else
341
+ GoodData::Metric.xcreate(options[:expression], metric.merge(options.merge(default)))
342
+ end
343
+ end
344
+
345
+ alias_method :create_metric, :add_metric
346
+
347
+ alias_method :add_measure, :add_metric
348
+ alias_method :create_measure, :add_metric
349
+
350
+ # Creates new instance of report in context of project
351
+ #
352
+ # @param [options] Optional report options
353
+ # @return [GoodData::Report] Instance of new report
354
+ def add_report(options = {})
355
+ report = GoodData::Report.create(options.merge(client: client, project: self))
356
+ report.save
357
+ end
358
+
359
+ alias_method :create_report, :add_report
360
+
361
+ # Creates new instance of report definition in context of project
362
+ # This report definition can be used for creating of GoodData::Report
363
+ #
364
+ # @param [json] Raw report definition json
365
+ # @return [GoodData::ReportDefinition] Instance of new report definition
366
+ def add_report_definition(json)
367
+ rd = GoodData::ReportDefinition.new(json)
368
+ rd.client = client
369
+ rd.project = self
370
+ rd.save
371
+ end
372
+
373
+ alias_method :create_report_definition, :add_report_definition
374
+
375
+ # Returns an indication whether current user is admin in this project
376
+ #
377
+ # @return [Boolean] True if user has admin role in the project, false otherwise.
378
+ def am_i_admin?
379
+ user_has_role?(client.user, 'admin')
380
+ end
381
+
382
+ # Helper for getting attributes of a project
383
+ #
384
+ # @param [String | Number | Object] Anything that you can pass to GoodData::Attribute[id]
385
+ # @return [GoodData::Attribute | Array<GoodData::Attribute>] fact instance or list
386
+ def attributes(id = :all)
387
+ GoodData::Attribute[id, project: self, client: client]
388
+ end
389
+
390
+ def attribute_by_identifier(identifier)
391
+ GoodData::Attribute.find_first_by_identifier(identifier, project: self, client: client)
392
+ end
393
+
394
+ def attributes_by_identifier(identifier)
395
+ GoodData::Attribute.find_by_identifier(identifier, project: self, client: client)
396
+ end
397
+
398
+ def attribute_by_title(title)
399
+ GoodData::Attribute.find_first_by_title(title, project: self, client: client)
400
+ end
401
+
402
+ def attributes_by_title(title)
403
+ GoodData::Attribute.find_by_title(title, project: self, client: client)
404
+ end
405
+
406
+ # Gets project blueprint from the server
407
+ #
408
+ # @return [GoodData::ProjectRole] Project role if found
409
+ def blueprint(options = {})
410
+ result = client.get("/gdc/projects/#{pid}/model/view", params: { includeDeprecated: true, includeGrain: true })
411
+ polling_url = result['asyncTask']['link']['poll']
412
+ model = client.poll_on_code(polling_url, options)
413
+ bp = GoodData::Model::FromWire.from_wire(model)
414
+ bp.title = title
415
+ bp
416
+ end
417
+
418
+ # Returns web interface URI of project
419
+ #
420
+ # @return [String] Project URL
421
+ def browser_uri(options = {})
422
+ grey = options[:grey]
423
+ server = client.connection.server_url
424
+ if grey
425
+ "#{server}#{uri}"
426
+ else
427
+ "#{server}/#s=#{uri}"
428
+ end
429
+ end
430
+
431
+ # Clones project
432
+ #
433
+ # @param options [Hash] Export options
434
+ # @option options [Boolean] :data Clone project with data
435
+ # @option options [Boolean] :users Clone project with users
436
+ # @option options [String] :authorized_users Comma separated logins of authorized users. Users that can use the export
437
+ # @return [GoodData::Project] Newly created project
438
+ def clone(options = {})
439
+ a_title = options[:title] || "Clone of #{title}"
440
+
441
+ begin
442
+ # Create the project first so we know that it is passing.
443
+ # What most likely is wrong is the token and the export actaully takes majority of the time
444
+ new_project = GoodData::Project.create(options.merge(:title => a_title, :client => client, :driver => content[:driver]))
445
+ export_token = export_clone(options)
446
+ new_project.import_clone(export_token)
447
+ rescue
448
+ new_project.delete if new_project
449
+ raise
450
+ end
451
+ end
452
+
453
+ # Gives you list of datasets. These are not blueprint datasets but model datasets coming from meta
454
+ # data server.
455
+ #
456
+ # @param id [Symbol | String | GoodData::MdObject] Export options
457
+ # @return [Array<GoodData::Dataset> | GoodData::Dataset] Dataset or list of datasets in the project
458
+ def datasets(id = :all)
459
+ GoodData::Dataset[id, project: self, client: client]
460
+ end
461
+
462
+ def dimensions(id = :all)
463
+ GoodData::Dimension[id, client: client, project: self]
464
+ end
465
+
466
+ # Export a clone from a project to be later imported.
467
+ # If you do not want to do anything special and you do not need fine grained
468
+ # controle use clone method which does all the heavy lifting for you.
469
+ #
470
+ # @param options [Hash] Export options
471
+ # @option options [Boolean] :data Clone project with data
472
+ # @option options [Boolean] :users Clone project with users
473
+ # @option options [String] :authorized_users Comma separated logins of authorized users. Users that can use the export
474
+ # @return [String] token of the export
475
+ def export_clone(options = {})
476
+ with_data = options[:data].nil? ? true : options[:data]
477
+ with_users = options[:users].nil? ? false : options[:users]
478
+
479
+ export = {
480
+ :exportProject => {
481
+ :exportUsers => with_users ? 1 : 0,
482
+ :exportData => with_data ? 1 : 0
483
+ }
484
+ }
485
+ export[:exportProject][:authorizedUsers] = options[:authorized_users] if options[:authorized_users]
486
+
487
+ result = client.post("/gdc/md/#{obj_id}/maintenance/export", export)
488
+ status_url = result['exportArtifact']['status']['uri']
489
+ client.poll_on_response(status_url) do |body|
490
+ body['taskState']['status'] == 'RUNNING'
491
+ end
492
+ result['exportArtifact']['token']
493
+ end
494
+
495
+ def user_groups(id = :all, options = {})
496
+ GoodData::UserGroup[id, options.merge(project: self)]
497
+ end
498
+
499
+ # Imports a clone into current project. The project has to be freshly
500
+ # created.
501
+ #
502
+ # @param export_token [String] Export token of the package to be imported
503
+ # @return [Project] current project
504
+ def import_clone(export_token, options = {})
505
+ import = {
506
+ :importProject => {
507
+ :token => export_token
508
+ }
509
+ }
510
+
511
+ result = client.post("/gdc/md/#{obj_id}/maintenance/import", import)
512
+ status_url = result['uri']
513
+ client.poll_on_response(status_url, options) do |body|
514
+ body['taskState']['status'] == 'RUNNING'
515
+ end
516
+ self
517
+ end
518
+
519
+ def compute_report(spec = {})
520
+ GoodData::ReportDefinition.execute(spec.merge(client: client, project: self))
521
+ end
522
+
523
+ def compute_metric(expression)
524
+ GoodData::Metric.xexecute(expression, client: client, project: self)
525
+ end
526
+
527
+ alias_method :compute_measure, :compute_metric
528
+
529
+ def create_schedule(process, date, executable, options = {})
530
+ s = GoodData::Schedule.create(process, date, executable, options.merge(client: client, project: self))
531
+ s.save
532
+ end
533
+
534
+ def create_variable(data)
535
+ GoodData::Variable.create(data, client: client, project: self)
536
+ end
537
+
538
+ # Helper for getting dashboards of a project
539
+ #
540
+ # @param id [String | Number | Object] Anything that you can pass to GoodData::Dashboard[id]
541
+ # @return [GoodData::Dashboard | Array<GoodData::Dashboard>] dashboard instance or list
542
+ def dashboards(id = :all)
543
+ GoodData::Dashboard[id, project: self, client: client]
544
+ end
545
+
546
+ def data_permissions(id = :all)
547
+ GoodData::MandatoryUserFilter[id, client: client, project: self]
548
+ end
549
+
550
+ # Deletes project
551
+ def delete
552
+ fail "Project '#{title}' with id #{uri} is already deleted" if deleted?
553
+ client.delete(uri)
554
+ end
555
+
556
+ # Returns true if project is in deleted state
557
+ #
558
+ # @return [Boolean] Returns true if object deleted. False otherwise.
559
+ def deleted?
560
+ state == :deleted
561
+ end
562
+
563
+ # Helper for getting rid of all data in the project
564
+ #
565
+ # @option options [Boolean] :force has to be added otherwise the operation is not performed
566
+ # @return [Array] Result of executing MAQLs
567
+ def delete_all_data(options = {})
568
+ return false unless options[:force]
569
+ datasets.pmap(&:delete_data)
570
+ end
571
+
572
+ # Deletes dashboards for project
573
+ def delete_dashboards
574
+ Dashboard.all.map { |data| Dashboard[data['link']] }.each(&:delete)
575
+ end
576
+
577
+ def deploy_process(path, options = {})
578
+ GoodData::Process.deploy(path, options.merge(client: client, project: self))
579
+ end
580
+
581
+ # Executes DML expression. See (https://developer.gooddata.com/article/deleting-records-from-datasets)
582
+ # for some examples and explanations
583
+ #
584
+ # @param dml [String] DML expression
585
+ # @return [Hash] Result of executing DML
586
+ def execute_dml(dml, options = {})
587
+ uri = "/gdc/md/#{pid}/dml/manage"
588
+ result = client.post(uri, manage: { maql: dml })
589
+ polling_uri = result['uri']
590
+
591
+ client.poll_on_response(polling_uri, options) do |body|
592
+ body && body['taskState'] && body['taskState']['status'] == 'WAIT'
593
+ end
594
+ end
595
+
596
+ # Executes MAQL expression and waits for it to be finished.
597
+ #
598
+ # @param maql [String] MAQL expression
599
+ # @return [Hash] Result of executing MAQL
600
+ def execute_maql(maql, options = {})
601
+ ldm_links = client.get(md[GoodData::Model::LDM_CTG])
602
+ ldm_uri = Links.new(ldm_links)[GoodData::Model::LDM_MANAGE_CTG]
603
+ response = client.post(ldm_uri, manage: { maql: maql })
604
+ polling_uri = response['entries'].first['link']
605
+
606
+ client.poll_on_response(polling_uri, options) do |body|
607
+ body && body['wTaskStatus'] && body['wTaskStatus']['status'] == 'RUNNING'
608
+ end
609
+ end
610
+
611
+ # Helper for getting facts of a project
612
+ #
613
+ # @param [String | Number | Object] Anything that you can pass to GoodData::Fact[id]
614
+ # @return [GoodData::Fact | Array<GoodData::Fact>] fact instance or list
615
+ def facts(id = :all)
616
+ GoodData::Fact[id, project: self, client: client]
617
+ end
618
+
619
+ def fact_by_title(title)
620
+ GoodData::Fact.find_first_by_title(title, project: self, client: client)
621
+ end
622
+
623
+ def facts_by_title(title)
624
+ GoodData::Fact.find_by_title(title, project: self, client: client)
625
+ end
626
+
627
+ def find_attribute_element_value(uri)
628
+ GoodData::Attribute.find_element_value(uri, client: client, project: self)
629
+ end
630
+
631
+ # Get WebDav directory for project data
632
+ # @return [String]
633
+ def project_webdav_path
634
+ client.project_webdav_path(:project => self)
635
+ end
636
+
637
+ # Gets project role by its identifier
638
+ #
639
+ # @param [String] role_name Title of role to look for
640
+ # @return [GoodData::ProjectRole] Project role if found
641
+ def get_role_by_identifier(role_name, role_list = roles)
642
+ role_name = role_name.downcase.gsub(/role$/, '')
643
+ role_list.each do |role|
644
+ tmp_role_name = role.identifier.downcase.gsub(/role$/, '')
645
+ return role if tmp_role_name == role_name
646
+ end
647
+ nil
648
+ end
649
+
650
+ # Gets project role byt its summary
651
+ #
652
+ # @param [String] role_summary Summary of role to look for
653
+ # @return [GoodData::ProjectRole] Project role if found
654
+ def get_role_by_summary(role_summary, role_list = roles)
655
+ role_list.each do |role|
656
+ return role if role.summary.downcase == role_summary.downcase
657
+ end
658
+ nil
659
+ end
660
+
661
+ # Gets project role by its name
662
+ #
663
+ # @param [String] role_title Title of role to look for
664
+ # @return [GoodData::ProjectRole] Project role if found
665
+ def get_role_by_title(role_title, role_list = roles)
666
+ role_list.each do |role|
667
+ return role if role.title.downcase == role_title.downcase
668
+ end
669
+ nil
670
+ end
671
+
672
+ # Gets project role
673
+ #
674
+ # @param [String] role_title Title of role to look for
675
+ # @return [GoodData::ProjectRole] Project role if found
676
+ def get_role(role_name, role_list = roles)
677
+ return role_name if role_name.is_a? GoodData::ProjectRole
678
+
679
+ role_name.downcase!
680
+ role_list.each do |role|
681
+ return role if role.uri == role_name ||
682
+ role.identifier.downcase == role_name ||
683
+ role.identifier.downcase.gsub(/role$/, '') == role_name ||
684
+ role.title.downcase == role_name ||
685
+ role.summary.downcase == role_name
686
+ end
687
+ nil
688
+ end
689
+
690
+ # Gets user by its login or uri in various shapes
691
+ # It does not find by other information because that is not unique. If you want to search by name or email please
692
+ # use fuzzy_get_user.
693
+ #
694
+ # @param [String] name Name to look for
695
+ # @param [Array<GoodData::User>]user_list Optional cached list of users used for look-ups
696
+ # @return [GoodDta::Membership] User
697
+ def get_user(slug, user_list = users)
698
+ search_crit = if slug.respond_to?(:login)
699
+ slug.login || slug.uri
700
+ elsif slug.is_a?(Hash)
701
+ slug[:login] || slug[:uri]
702
+ else
703
+ slug
704
+ end
705
+ return nil unless search_crit
706
+ user_list.find do |user|
707
+ user.uri == search_crit.downcase ||
708
+ user.login.downcase == search_crit.downcase
709
+ end
710
+ end
711
+
712
+ def upload_file(file, options = {})
713
+ GoodData.upload_to_project_webdav(file, options.merge(project: self))
714
+ end
715
+
716
+ def download_file(file, where)
717
+ GoodData.download_from_project_webdav(file, where, project: self)
718
+ end
719
+
720
+ def environment
721
+ json['project']['content']['environment']
722
+ end
723
+
724
+ # Gets user by its email, full_name, login or uri
725
+ alias_method :member, :get_user
726
+
727
+ # Gets user by its email, full_name, login or uri.
728
+ #
729
+ # @param [String] name Name to look for
730
+ # @param [Array<GoodData::User>]user_list Optional cached list of users used for look-ups
731
+ # @return [GoodDta::Membership] User
732
+ def fuzzy_get_user(name, user_list = users)
733
+ return name if name.instance_of?(GoodData::Membership)
734
+ return member(name) if name.instance_of?(GoodData::Profile)
735
+ name = name.is_a?(Hash) ? name[:login] || name[:uri] : name
736
+ return nil unless name
737
+ name.downcase!
738
+ user_list.select do |user|
739
+ user.uri.downcase == name ||
740
+ user.login.downcase == name ||
741
+ user.email.downcase == name
742
+ end
743
+ nil
744
+ end
745
+
746
+ # Checks whether user has particular role in given proejct
747
+ #
748
+ # @param user [GoodData::Profile | GoodData::Membership | String] User in question. Can be passed by login (String), profile or membershi objects
749
+ # @param role_name [String || GoodData::ProjectRole] Project role cna be given by either string or GoodData::ProjectRole object
750
+ # @return [Boolean] Tru if user has role_name
751
+ def user_has_role?(user, role_name)
752
+ member = get_user(user)
753
+ role = get_role(role_name)
754
+ member.roles.include?(role)
755
+ rescue
756
+ false
757
+ end
758
+
759
+ # Initializes object instance from raw wire JSON
760
+ #
761
+ # @param json Json used for initialization
762
+ def initialize(json)
763
+ super
764
+ @json = json
765
+ end
766
+
767
+ # Invites new user to project
768
+ #
769
+ # @param email [String] User to be invited
770
+ # @param role [String] Role URL or Role ID to be used
771
+ # @param msg [String] Optional invite message
772
+ #
773
+ # TODO: Return invite object
774
+ def invite(email, role, msg = DEFAULT_INVITE_MESSAGE)
775
+ puts "Inviting #{email}, role: #{role}"
776
+
777
+ role_url = nil
778
+ if role.index('/gdc/') != 0
779
+ tmp = get_role(role)
780
+ role_url = tmp.uri if tmp
781
+ else
782
+ role_url = role if role_url.nil?
783
+ end
784
+
785
+ data = {
786
+ :invitations => [
787
+ {
788
+ :invitation => {
789
+ :content => {
790
+ :email => email,
791
+ :role => role_url,
792
+ :action => {
793
+ :setMessage => msg
794
+ }
795
+ }
796
+ }
797
+ }
798
+ ]
799
+ }
800
+
801
+ url = "/gdc/projects/#{pid}/invitations"
802
+ client.post(url, data)
803
+ end
804
+
805
+ # Returns invitations to project
806
+ #
807
+ # @return [Array<GoodData::Invitation>] List of invitations
808
+ def invitations
809
+ invitations = client.get @json['project']['links']['invitations']
810
+ invitations['invitations'].pmap do |invitation|
811
+ client.create GoodData::Invitation, invitation
812
+ end
813
+ end
814
+
815
+ # Returns project related links
816
+ #
817
+ # @return [Hash] Project related links
818
+ def links
819
+ data['links']
820
+ end
821
+
822
+ # Helper for getting labels of a project
823
+ #
824
+ # @param [String | Number | Object] Anything that you can pass to
825
+ # GoodData::Label[id] + it supports :all as welll
826
+ # @return [GoodData::Fact | Array<GoodData::Fact>] fact instance or list
827
+ def labels(id = :all, opts = {})
828
+ if id == :all
829
+ attributes.pmapcat(&:labels).uniq
830
+ else
831
+ GoodData::Label[id, opts.merge(project: self, client: client)]
832
+ end
833
+ end
834
+
835
+ def md
836
+ @md ||= client.create(Links, client.get(data['links']['metadata']))
837
+ end
838
+
839
+ # Get data from project specific metadata storage
840
+ #
841
+ # @param [Symbol | String] :all or nothing for all keys or a string for value of specific key
842
+ # @return [Hash] key Hash of stored data
843
+ def metadata(key = :all)
844
+ GoodData::ProjectMetadata[key, client: client, project: self]
845
+ end
846
+
847
+ # Set data for specific key in project specific metadata storage
848
+ #
849
+ # @param [String] key key of the value to be stored
850
+ # @return [String] val value to be stored
851
+ def set_metadata(key, val)
852
+ GoodData::ProjectMetadata[key, client: client, project: self] = val
853
+ end
854
+
855
+ # Helper for getting metrics of a project
856
+ #
857
+ # @return [Array<GoodData::Metric>] matric instance or list
858
+ def metrics(id = :all, opts = { :full => true })
859
+ GoodData::Metric[id, opts.merge(project: self, client: client)]
860
+ end
861
+
862
+ alias_method :measures, :metrics
863
+
864
+ def metric_by_title(title)
865
+ GoodData::Metric.find_first_by_title(title, project: self, client: client)
866
+ end
867
+
868
+ alias_method :measure_by_title, :metric_by_title
869
+
870
+ def metrics_by_title(title)
871
+ GoodData::Metric.find_by_title(title, project: self, client: client)
872
+ end
873
+
874
+ alias_method :measures_by_title, :metrics_by_title
875
+
876
+ # Checks if the profile is member of project
877
+ #
878
+ # @param [GoodData::Profile] profile - Profile to be checked
879
+ # @param [Array<GoodData::Membership>] list Optional list of members to check against
880
+ # @return [Boolean] true if is member else false
881
+ def member?(profile, list = members)
882
+ !member(profile, list).nil?
883
+ end
884
+
885
+ def members?(profiles, list = members)
886
+ profiles.map { |p| member?(p, list) }
887
+ end
888
+
889
+ # Gets raw resource ID
890
+ #
891
+ # @return [String] Raw resource ID
892
+ def obj_id
893
+ uri.split('/').last
894
+ end
895
+
896
+ alias_method :pid, :obj_id
897
+
898
+ # Helper for getting objects of a project
899
+ #
900
+ # @return [Array<GoodData::MdObject>] object instance or list
901
+ def objects(id, opts = {})
902
+ GoodData::MdObject[id, opts.merge(project: self, client: client)]
903
+ end
904
+
905
+ # Transfer objects from one project to another
906
+ #
907
+ # @param [Array<GoodData::MdObject | String>, String, GoodData::MdObject] objs Any representation of the object or a list of those
908
+ # @param [Hash] options The options to migration.
909
+ # @option options [Number] :time_limit Time in seconds before the blocking call will fail. See GoodData::Rest::Client.poll_on_response for additional details
910
+ # @option options [Number] :sleep_interval Interval between polls on the status of the migration.
911
+ # @return [String] Returns token that you can use as input for object_import
912
+ def objects_export(objs, options = {})
913
+ fail 'Nothing to migrate. You have to pass list of objects, ids or uris that you would like to migrate' if objs.nil?
914
+ objs = Array(objs)
915
+ fail 'Nothing to migrate. The list you provided is empty' if objs.empty?
916
+
917
+ objs = objs.pmap { |obj| [obj, objects(obj)] }
918
+ fail ObjectsExportError, "Exporting objects failed with messages. Object #{objs.select { |_, obj| obj.nil? }.map { |o, _| o }.join(', ')} could not be found." if objs.any? { |_, obj| obj.nil? }
919
+ export_payload = {
920
+ :partialMDExport => {
921
+ :uris => objs.map { |_, obj| obj.uri }
922
+ }
923
+ }
924
+ result = client.post("#{md['maintenance']}/partialmdexport", export_payload)
925
+ polling_url = result['partialMDArtifact']['status']['uri']
926
+ token = result['partialMDArtifact']['token']
927
+
928
+ polling_result = client.poll_on_response(polling_url, options) do |body|
929
+ body['wTaskStatus'] && body['wTaskStatus']['status'] == 'RUNNING'
930
+ end
931
+ if polling_result['wTaskStatus'] && polling_result['wTaskStatus']['status'] == 'ERROR'
932
+ messages = GoodData::Helpers.interpolate_error_messages(polling_result['wTaskStatus']['messages']).join(' ')
933
+ fail ObjectsExportError, "Exporting objects failed with messages. #{messages}"
934
+ end
935
+ token
936
+ end
937
+
938
+ # Import objects from import token. If you do not need specifically this method what you are probably looking for is transfer_objects. This is a lower level method.
939
+ #
940
+ # @param [String] token Migration token ID
941
+ # @param [Hash] options The options to migration.
942
+ # @option options [Number] :time_limit Time in seconds before the blocking call will fail. See GoodData::Rest::Client.poll_on_response for additional details
943
+ # @option options [Number] :sleep_interval Interval between polls on the status of the migration.
944
+ # @return [Boolean] Returns true if it succeeds or throws exceoption
945
+ def objects_import(token, options = {})
946
+ fail 'You need to provide a token for object import' if token.blank?
947
+
948
+ import_payload = {
949
+ :partialMDImport => {
950
+ :token => token,
951
+ :overwriteNewer => '1',
952
+ :updateLDMObjects => '0'
953
+ }
954
+ }
955
+
956
+ result = client.post("#{md['maintenance']}/partialmdimport", import_payload)
957
+ polling_url = result['uri']
958
+
959
+ polling_result = client.poll_on_response(polling_url, options) do |body|
960
+ body['wTaskStatus'] && body['wTaskStatus']['status'] == 'RUNNING'
961
+ end
962
+
963
+ if polling_result['wTaskStatus']['status'] == 'ERROR'
964
+ messages = GoodData::Helpers.interpolate_error_messages(polling_result['wTaskStatus']['messages']).join(' ')
965
+ fail ObjectsImportError, "Importing objects failed with messages. #{messages}"
966
+ end
967
+ true
968
+ end
969
+
970
+ # Transfer objects from one project to another
971
+ #
972
+ # @param [Array<GoodData::MdObject | String>, String, GoodData::MdObject] objects Any representation of the object or a list of those
973
+ # @param [Hash] options The options to migration.
974
+ # @option options [GoodData::Project | String | Array<String> | Array<GoodData::Project>] :project Project(s) to migrate to
975
+ # @option options [Number] :batch_size Number of projects that are migrated at the same time. Default is 10
976
+ #
977
+ # @return [Boolean | Array<Hash>] Return either true or throws exception if you passed only one project. If you provided an array returns list of hashes signifying sucees or failure. Take note that in case of list of projects it does not throw exception
978
+ def partial_md_export(objects, options = {})
979
+ projects = options[:project]
980
+ batch_size = options[:batch_size] || 10
981
+ token = objects_export(objects)
982
+
983
+ if projects.is_a?(Array)
984
+ projects.each_slice(batch_size).flat_map do |batch|
985
+ batch.pmap do |proj|
986
+ begin
987
+ target_project = client.projects(proj)
988
+ target_project.objects_import(token, options)
989
+ {
990
+ project: target_project,
991
+ result: true
992
+ }
993
+ rescue RestClient::Exception => e
994
+ {
995
+ project: proj,
996
+ exception: e,
997
+ result: false,
998
+ reason: GoodData::Helpers.interpolate_error_message(MultiJson.load(e.response))
999
+ }
1000
+ rescue GoodData::ObjectsImportError => e
1001
+ {
1002
+ project: target_project,
1003
+ result: false,
1004
+ reason: e.message
1005
+ }
1006
+ end
1007
+ end
1008
+ end
1009
+ else
1010
+ target_project = client.projects(projects)
1011
+ target_project.objects_import(token, options)
1012
+ end
1013
+ end
1014
+
1015
+ alias_method :transfer_objects, :partial_md_export
1016
+
1017
+ # Helper for getting processes of a project
1018
+ #
1019
+ # @param [String | Number | Object] Anything that you can pass to GoodData::Report[id]
1020
+ # @return [GoodData::Report | Array<GoodData::Report>] report instance or list
1021
+ def processes(id = :all)
1022
+ GoodData::Process[id, project: self, client: client]
1023
+ end
1024
+
1025
+ # Checks if this object instance is project
1026
+ #
1027
+ # @return [Boolean] Return true for all instances
1028
+ def project?
1029
+ true
1030
+ end
1031
+
1032
+ def info
1033
+ results = blueprint.datasets.pmap do |ds|
1034
+ [ds, ds.count(self)]
1035
+ end
1036
+ puts title
1037
+ puts GoodData::Helpers.underline(title)
1038
+ puts
1039
+ puts "Datasets - #{results.count}"
1040
+ puts
1041
+ results.each do |x|
1042
+ dataset, count = x
1043
+ dataset.title.tap do |t|
1044
+ puts t
1045
+ puts GoodData::Helpers.underline(t)
1046
+ puts "Size - #{count} rows"
1047
+ puts "#{dataset.attributes_and_anchors.count} attributes, #{dataset.facts.count} facts, #{dataset.references.count} references"
1048
+ puts
1049
+ end
1050
+ end
1051
+ nil
1052
+ end
1053
+
1054
+ # Forces project to reload
1055
+ def reload!
1056
+ if saved?
1057
+ response = client.get(uri)
1058
+ @json = response
1059
+ end
1060
+ self
1061
+ end
1062
+
1063
+ # Method used for walking through objects in project and trying to replace all occurences of some object for another object. This is typically used as a means for exchanging Date dimensions.
1064
+ #
1065
+ # @param mapping [Array<Array>] Mapping specifying what should be exchanged for what. As mapping should be used output of GoodData::Helpers.prepare_mapping.
1066
+ def replace_from_mapping(mapping, opts = {})
1067
+ default = {
1068
+ :purge => false,
1069
+ :dry_run => false
1070
+ }
1071
+ opts = default.merge(opts)
1072
+ dry_run = opts[:dry_run]
1073
+
1074
+ if opts[:purge]
1075
+ GoodData.logger.info 'Purging old project definitions'
1076
+ reports.peach(&:purge_report_of_unused_definitions!)
1077
+ end
1078
+
1079
+ fail ArgumentError, 'No mapping specified' if mapping.blank?
1080
+ rds = report_definitions
1081
+
1082
+ {
1083
+ # data_permissions: data_permissions,
1084
+ variables: variables,
1085
+ dashboards: dashboards,
1086
+ metrics: metrics,
1087
+ report_definitions: rds
1088
+ }.each do |key, collection|
1089
+ puts "Replacing #{key}"
1090
+ collection.peach do |item|
1091
+ new_item = item.replace(mapping)
1092
+ if new_item.json != item.json
1093
+ if dry_run
1094
+ GoodData.logger.info "Would save #{new_item.uri}. Running in dry run mode"
1095
+ else
1096
+ GoodData.logger.info "Saving #{new_item.uri}"
1097
+ new_item.save
1098
+ end
1099
+ end
1100
+ end
1101
+ end
1102
+
1103
+ GoodData.logger.info 'Replacing hidden metrics'
1104
+ local_metrics = rds.pmapcat { |rd| rd.using('metric') }.select { |m| m['deprecated'] == '1' }
1105
+ puts "Found #{local_metrics.count} metrics"
1106
+ local_metrics.pmap { |m| metrics(m['link']) }.peach do |item|
1107
+ new_item = item.replace(mapping)
1108
+ if new_item.json != item.json
1109
+ if dry_run
1110
+ GoodData.logger.info "Would save #{new_item.uri}. Running in dry run mode"
1111
+ else
1112
+ GoodData.logger.info "Saving #{new_item.uri}"
1113
+ new_item.save
1114
+ end
1115
+ end
1116
+ end
1117
+
1118
+ GoodData.logger.info 'Replacing variable values'
1119
+ variables.each do |var|
1120
+ var.values.peach do |val|
1121
+ val.replace(mapping).save unless dry_run
1122
+ end
1123
+ end
1124
+ nil
1125
+ end
1126
+
1127
+ # Helper for getting reports of a project
1128
+ #
1129
+ # @param [String | Number | Object] Anything that you can pass to GoodData::Report[id]
1130
+ # @return [GoodData::Report | Array<GoodData::Report>] report instance or list
1131
+ def reports(id = :all)
1132
+ GoodData::Report[id, project: self, client: client]
1133
+ end
1134
+
1135
+ # Helper for getting report definitions of a project
1136
+ #
1137
+ # @param [String | Number | Object] Anything that you can pass to GoodData::ReportDefinition[id]
1138
+ # @return [GoodData::ReportDefinition | Array<GoodData::ReportDefinition>] report definition instance or list
1139
+ def report_definitions(id = :all, options = {})
1140
+ GoodData::ReportDefinition[id, options.merge(project: self, client: client)]
1141
+ end
1142
+
1143
+ # Gets the list or project roles
1144
+ #
1145
+ # @return [Array<GoodData::ProjectRole>] List of roles
1146
+ def roles
1147
+ url = "/gdc/projects/#{pid}/roles"
1148
+
1149
+ tmp = client.get(url)
1150
+ tmp['projectRoles']['roles'].pmap do |role_url|
1151
+ json = client.get role_url
1152
+ client.create(GoodData::ProjectRole, json, project: self)
1153
+ end
1154
+ end
1155
+
1156
+ # Saves project
1157
+ def save
1158
+ data_to_send = GoodData::Helpers.deep_dup(raw_data)
1159
+ data_to_send['project']['content'].delete('cluster')
1160
+ data_to_send['project']['content'].delete('isPublic')
1161
+ data_to_send['project']['content'].delete('state')
1162
+ response = if uri
1163
+ client.post(PROJECT_PATH % pid, data_to_send)
1164
+ client.get uri
1165
+ else
1166
+ result = client.post(PROJECTS_PATH, data_to_send)
1167
+ client.get result['uri']
1168
+ end
1169
+ @json = response
1170
+ self
1171
+ end
1172
+
1173
+ # Schedules an email with dashboard or report content
1174
+ def schedule_mail(options = GoodData::ScheduledMail::DEFAULT_OPTS)
1175
+ GoodData::ScheduledMail.create(options.merge(client: client, project: self))
1176
+ end
1177
+
1178
+ def scheduled_mails(options = { :full => false })
1179
+ GoodData::ScheduledMail[:all, options.merge(project: self, client: client)]
1180
+ end
1181
+
1182
+ # @param [String | Number | Object] Anything that you can pass to GoodData::Schedule[id]
1183
+ # @return [GoodData::Schedule | Array<GoodData::Schedule>] schedule instance or list
1184
+ def schedules(id = :all)
1185
+ GoodData::Schedule[id, project: self, client: client]
1186
+ end
1187
+
1188
+ # Gets SLIs data
1189
+ #
1190
+ # @return [GoodData::Metadata] SLI Metadata
1191
+ def slis
1192
+ link = "#{data['links']['metadata']}#{SLIS_PATH}"
1193
+
1194
+ # FIXME: Review what to do with passed extra argument
1195
+ Metadata.new client.get(link)
1196
+ end
1197
+
1198
+ # Gets project state
1199
+ #
1200
+ # @return [String] Project state
1201
+ def state
1202
+ data['content']['state'].downcase.to_sym if data['content'] && data['content']['state']
1203
+ end
1204
+
1205
+ Project.metadata_property_reader :summary, :title
1206
+
1207
+ # Gets project title
1208
+ #
1209
+ # @return [String] Project title
1210
+ def title=(a_title)
1211
+ data['meta']['title'] = a_title if data['meta']
1212
+ end
1213
+
1214
+ # Uploads file to project
1215
+ #
1216
+ # @param file File to be uploaded
1217
+ # @param schema Schema to be used
1218
+ def upload(data, blueprint, dataset_name, options = {})
1219
+ GoodData::Model.upload_data(data, blueprint, dataset_name, options.merge(client: client, project: self))
1220
+ end
1221
+
1222
+ def upload_multiple(data, blueprint, options = {})
1223
+ GoodData::Model.upload_multiple_data(data, blueprint, options.merge(client: client, project: self))
1224
+ end
1225
+
1226
+ def uri
1227
+ data['links']['self'] if data && data['links'] && data['links']['self']
1228
+ end
1229
+
1230
+ # List of user filters within this project
1231
+ #
1232
+ # @return [Array<GoodData::MandatoryUserFilter>] List of mandatory user
1233
+ def user_filters
1234
+ url = "/gdc/md/#{pid}/userfilters"
1235
+
1236
+ tmp = client.get(url)
1237
+ tmp['userFilters']['items'].pmap do |filter|
1238
+ client.create(GoodData::MandatoryUserFilter, filter, project: self)
1239
+ end
1240
+ end
1241
+
1242
+ # List of users in project
1243
+ #
1244
+ #
1245
+ # @return [Array<GoodData::User>] List of users
1246
+ def users(opts = {})
1247
+ client = client(opts)
1248
+ Enumerator.new do |y|
1249
+ offset = opts[:offset] || 0
1250
+ limit = opts[:limit] || 1_000
1251
+ loop do
1252
+ tmp = client.get("/gdc/projects/#{pid}/users", params: { offset: offset, limit: limit })
1253
+ tmp['users'].each do |user_data|
1254
+ user = client.create(GoodData::Membership, user_data, project: self)
1255
+ y << user if opts[:all] || user && user.enabled?
1256
+ end
1257
+ break if tmp['users'].count < limit
1258
+ offset += limit
1259
+ end
1260
+ end
1261
+ end
1262
+
1263
+ alias_method :members, :users
1264
+
1265
+ def whitelist_users(new_users, users_list, whitelist, mode = :exclude)
1266
+ return [new_users, users_list] unless whitelist
1267
+
1268
+ new_whitelist_proc = proc do |user|
1269
+ whitelist.any? { |wl| wl.is_a?(Regexp) ? user[:login] =~ wl : user[:login].include?(wl) }
1270
+ end
1271
+
1272
+ whitelist_proc = proc do |user|
1273
+ whitelist.any? { |wl| wl.is_a?(Regexp) ? user.login =~ wl : user.login.include?(wl) }
1274
+ end
1275
+
1276
+ if mode == :include
1277
+ [new_users.select(&new_whitelist_proc), users_list.select(&whitelist_proc)]
1278
+ elsif mode == :exclude
1279
+ [new_users.reject(&new_whitelist_proc), users_list.reject(&whitelist_proc)]
1280
+ end
1281
+ end
1282
+
1283
+ # Imports users
1284
+ def import_users(new_users, options = {})
1285
+ role_list = roles
1286
+ users_list = users(all: true)
1287
+ new_users = new_users.map { |x| (x.is_a?(Hash) && x[:user] && x[:user].to_hash.merge(role: x[:role])) || x.to_hash }
1288
+
1289
+ GoodData.logger.warn("Importing users to project (#{pid})")
1290
+
1291
+ whitelisted_new_users, whitelisted_users = whitelist_users(new_users.map(&:to_hash), users_list, options[:whitelists])
1292
+
1293
+ # First check that if groups are provided we have them set up
1294
+ check_groups(new_users.map(&:to_hash).flat_map { |u| u[:user_group] || [] }.uniq)
1295
+
1296
+ # conform the role on list of new users so we can diff them with the users coming from the project
1297
+ diffable_new_with_default_role = whitelisted_new_users.map do |u|
1298
+ u[:role] = Array(u[:role] || u[:roles] || 'readOnlyUser')
1299
+ u
1300
+ end
1301
+
1302
+ intermediate_new = diffable_new_with_default_role.map do |u|
1303
+ u[:role] = u[:role].map do |r|
1304
+ role = get_role(r, role_list)
1305
+ role && role.uri
1306
+ end
1307
+
1308
+ if u[:role].all?(&:nil?)
1309
+ u[:type] = :error
1310
+ u[:reason] = 'Invalid role(s) specified'
1311
+ else
1312
+ u[:type] = :ok
1313
+ end
1314
+
1315
+ u[:status] = 'ENABLED'
1316
+ u
1317
+ end
1318
+
1319
+ intermediate_new_by_type = intermediate_new.group_by { |i| i[:type] }
1320
+ diffable_new = intermediate_new_by_type[:ok] || []
1321
+
1322
+ # Diff users. Only login and role is important for the diff
1323
+ diff = GoodData::Helpers.diff(whitelisted_users, diffable_new, key: :login, fields: [:login, :role, :status])
1324
+
1325
+ # Create new users
1326
+ u = diff[:added].map { |x| { user: x, role: x[:role] } }
1327
+
1328
+ results = []
1329
+ GoodData.logger.warn("Creating #{diff[:added].count} users in project (#{pid})")
1330
+ results.concat(create_users(u, roles: role_list, project_users: whitelisted_users))
1331
+
1332
+ # # Update existing users
1333
+ GoodData.logger.warn("Updating #{diff[:changed].count} users in project (#{pid})")
1334
+ list = diff[:changed].map { |x| { user: x[:new_obj], role: x[:new_obj][:role] || x[:new_obj][:roles] } }
1335
+ results.concat(set_users_roles(list, roles: role_list, project_users: whitelisted_users))
1336
+
1337
+ # Remove old users
1338
+ to_remove = diff[:removed].reject { |user| user[:status] == 'DISABLED' || user[:status] == :disabled }
1339
+ GoodData.logger.warn("Removing #{to_remove.count} users from project (#{pid})")
1340
+ results.concat(disable_users(to_remove))
1341
+
1342
+ # reassign to groups
1343
+ mappings = new_users.map(&:to_hash).flat_map do |user|
1344
+ groups = user[:user_group] || []
1345
+ groups.map { |g| [user[:login], g] }
1346
+ end
1347
+ unless mappings.empty?
1348
+ users_lookup = users.reduce({}) do |a, e|
1349
+ a[e.login] = e
1350
+ a
1351
+ end
1352
+ mappings.group_by { |_, g| g }.each do |g, mapping|
1353
+ # find group + set users
1354
+ # CARE YOU DO NOT KNOW URI
1355
+ user_groups(g).set_members(mapping.map { |user, _| user }.map { |login| users_lookup[login] && users_lookup[login].uri })
1356
+ end
1357
+ mentioned_groups = mappings.map(&:last).uniq
1358
+ groups_to_cleanup = user_groups.reject { |g| mentioned_groups.include?(g.name) }
1359
+ # clean all groups not mentioned with exception of whitelisted users
1360
+ groups_to_cleanup.each do |g|
1361
+ g.set_members(whitelist_users(g.members.map(&:to_hash), [], options[:whitelists], :include).first.map { |x| x[:uri] })
1362
+ end
1363
+ end
1364
+ results
1365
+ end
1366
+
1367
+ def disable_users(list)
1368
+ list = list.map(&:to_hash)
1369
+ url = "#{uri}/users"
1370
+ payloads = list.map do |u|
1371
+ generate_user_payload(u[:uri], 'DISABLED')
1372
+ end
1373
+
1374
+ payloads.each_slice(100).mapcat do |payload|
1375
+ result = client.post(url, 'users' => payload)
1376
+ result['projectUsersUpdateResult'].mapcat { |k, v| v.map { |x| { type: k.to_sym, uri: x } } }
1377
+ end
1378
+ end
1379
+
1380
+ def check_groups(specified_groups)
1381
+ groups = user_groups.map(&:name)
1382
+ missing_groups = specified_groups - groups
1383
+ fail "All groups have to be specified before you try to import users. Groups that are currently in project are #{groups.join(',')} and you asked for #{missing_groups.join(',')}" unless missing_groups.empty?
1384
+ end
1385
+
1386
+ # Update user
1387
+ #
1388
+ # @param user User to be updated
1389
+ # @param desired_roles Roles to be assigned to user
1390
+ # @param role_list Optional cached list of roles used for lookups
1391
+ def set_user_roles(login, desired_roles, options = {})
1392
+ user_uri, roles = resolve_roles(login, desired_roles, options)
1393
+ url = "#{uri}/users"
1394
+ payload = generate_user_payload(user_uri, 'ENABLED', roles)
1395
+ res = client.post(url, payload)
1396
+ failure = GoodData::Helpers.get_path(res, %w(projectUsersUpdateResult failed))
1397
+ fail ArgumentError, "User #{user_uri} could not be aded. #{failure.first['message']}" unless failure.blank?
1398
+ res
1399
+ end
1400
+ alias_method :add_user, :set_user_roles
1401
+
1402
+ # Update list of users
1403
+ #
1404
+ # @param list List of users to be updated
1405
+ # @param role_list Optional list of cached roles to prevent unnecessary server round-trips
1406
+ def set_users_roles(list, options = {})
1407
+ return [] if list.empty?
1408
+ role_list = options[:roles] || roles
1409
+ project_users = options[:project_users] || users
1410
+
1411
+ intermediate_users = list.flat_map do |user_hash|
1412
+ user = user_hash[:user] || user_hash[:login]
1413
+ desired_roles = user_hash[:role] || user_hash[:roles] || 'readOnlyUser'
1414
+ begin
1415
+ login, roles = resolve_roles(user, desired_roles, options.merge(project_users: project_users, roles: role_list))
1416
+ [{ :type => :successful, user: login, roles: roles }]
1417
+ rescue => e
1418
+ [{ :type => :failed, :reason => e.message, user: login, roles: roles }]
1419
+ end
1420
+ end
1421
+
1422
+ # User can fail pre sending to API during resolving roles. We add only users that passed that step.
1423
+ users_by_type = intermediate_users.group_by { |u| u[:type] }
1424
+ users_to_add = users_by_type[:successful] || []
1425
+
1426
+ payloads = users_to_add.map { |u| generate_user_payload(u[:user], 'ENABLED', u[:roles]) }
1427
+ results = payloads.each_slice(100).map do |payload|
1428
+ client.post("#{uri}/users", 'users' => payload)
1429
+ end
1430
+ # this ugly line turns the hash of errors into list of errors with types so we can process them easily
1431
+ typed_results = results.flat_map { |x| x['projectUsersUpdateResult'].flat_map { |k, v| v.map { |v_2| v_2.is_a?(String) ? { type: k.to_sym, user: v_2 } : GoodData::Helpers.symbolize_keys(v_2).merge(type: k.to_sym) } } }
1432
+ # we have to concat errors from role resolution and API result
1433
+ typed_results + (users_by_type[:failed] || [])
1434
+ end
1435
+
1436
+ alias_method :add_users, :set_users_roles
1437
+ alias_method :create_users, :set_users_roles
1438
+
1439
+ def add_data_permissions(filters, options = {})
1440
+ GoodData::UserFilterBuilder.execute_mufs(filters, { client: client, project: self }.merge(options))
1441
+ end
1442
+
1443
+ def add_variable_permissions(filters, var, options = {})
1444
+ GoodData::UserFilterBuilder.execute_variables(filters, var, { client: client, project: self }.merge(options))
1445
+ end
1446
+
1447
+ # Run validation on project
1448
+ # Valid settins for validation are (default all):
1449
+ # ldm - Checks the consistency of LDM objects.
1450
+ # pdm Checks LDM to PDM mapping consistency, also checks PDM reference integrity.
1451
+ # metric_filter - Checks metadata for inconsistent metric filters.
1452
+ # invalid_objects - Checks metadata for invalid/corrupted objects.
1453
+ # asyncTask response
1454
+ def validate(filters = %w(ldm pdm metric_filter invalid_objects), options = {})
1455
+ response = client.post "#{md['validate-project']}", 'validateProject' => filters
1456
+ polling_link = response['asyncTask']['link']['poll']
1457
+ client.poll_on_response(polling_link, options) do |body|
1458
+ body['wTaskStatus'] && body['wTaskStatus']['status'] == 'RUNNING'
1459
+ end
1460
+ end
1461
+
1462
+ def variables(id = :all, options = { client: client, project: self })
1463
+ GoodData::Variable[id, options]
1464
+ end
1465
+
1466
+ def update_from_blueprint(blueprint, options = {})
1467
+ GoodData::Model::ProjectCreator.migrate(options.merge(spec: blueprint, token: options[:auth_token], client: client, project: self))
1468
+ end
1469
+
1470
+ def resolve_roles(login, desired_roles, options = {})
1471
+ user = if login.is_a?(String) && login.include?('@')
1472
+ '/gdc/account/profile/' + login
1473
+ elsif login.is_a?(String)
1474
+ login
1475
+ elsif login.is_a?(Hash) && login[:login]
1476
+ '/gdc/account/profile/' + login[:login]
1477
+ elsif login.is_a?(Hash) && login[:uri]
1478
+ login[:uri]
1479
+ elsif login.respond_to?(:uri) && login.uri
1480
+ login.uri
1481
+ elsif login.respond_to?(:login) && login.login
1482
+ '/gdc/account/profile/' + login.login
1483
+ else
1484
+ fail "Unsupported user specification #{login}"
1485
+ end
1486
+
1487
+ role_list = options[:roles] || roles
1488
+ desired_roles = Array(desired_roles)
1489
+ roles = desired_roles.map do |role_name|
1490
+ role = get_role(role_name, role_list)
1491
+ fail ArgumentError, "Invalid role '#{role_name}' specified for user '#{user.email}'" if role.nil?
1492
+ role.uri
1493
+ end
1494
+ [user, roles]
1495
+ end
1496
+
1497
+ private
1498
+
1499
+ def generate_user_payload(user_uri, status = 'ENABLED', roles_uri = nil)
1500
+ payload = {
1501
+ 'user' => {
1502
+ 'content' => {
1503
+ 'status' => status
1504
+ },
1505
+ 'links' => {
1506
+ 'self' => user_uri
1507
+ }
1508
+ }
1509
+ }
1510
+ payload['user']['content']['userRoles'] = roles_uri if roles_uri
1511
+ payload
1512
+ end
1513
+ end
1514
+ end