gooddata 1.0.0-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (517) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.editorconfig +12 -0
  4. data/.flayignore +6 -0
  5. data/.gitignore +38 -0
  6. data/.pronto.yml +3 -0
  7. data/.rspec +5 -0
  8. data/.rubocop.yml +101 -0
  9. data/.travis.yml +9 -0
  10. data/.yardopts +22 -0
  11. data/CHANGELOG.md +272 -0
  12. data/CLI.md +435 -0
  13. data/CONTRIBUTING.md +38 -0
  14. data/DEPENDENCIES.md +880 -0
  15. data/Dockerfile.jruby +17 -0
  16. data/Dockerfile.ruby +19 -0
  17. data/Gemfile +4 -0
  18. data/Guardfile +5 -0
  19. data/LICENSE +22 -0
  20. data/LICENSE.rb +5 -0
  21. data/README.md +78 -0
  22. data/Rakefile +204 -0
  23. data/TODO.md +32 -0
  24. data/authors.sh +4 -0
  25. data/bin/gooddata +7 -0
  26. data/ci.rake +47 -0
  27. data/dependency_decisions.yml +104 -0
  28. data/docker-compose.yml +34 -0
  29. data/gooddata +9 -0
  30. data/gooddata.gemspec +72 -0
  31. data/lib/gooddata.rb +34 -0
  32. data/lib/gooddata/app/app.rb +16 -0
  33. data/lib/gooddata/bricks/base_downloader.rb +86 -0
  34. data/lib/gooddata/bricks/brick.rb +37 -0
  35. data/lib/gooddata/bricks/bricks.rb +17 -0
  36. data/lib/gooddata/bricks/middleware/aws_middleware.rb +41 -0
  37. data/lib/gooddata/bricks/middleware/base_middleware.rb +57 -0
  38. data/lib/gooddata/bricks/middleware/bench_middleware.rb +25 -0
  39. data/lib/gooddata/bricks/middleware/bulk_salesforce_middleware.rb +37 -0
  40. data/lib/gooddata/bricks/middleware/decode_params_middleware.rb +21 -0
  41. data/lib/gooddata/bricks/middleware/dwh_middleware.rb +41 -0
  42. data/lib/gooddata/bricks/middleware/fs_download_middleware.rb +48 -0
  43. data/lib/gooddata/bricks/middleware/fs_upload_middleware.rb +36 -0
  44. data/lib/gooddata/bricks/middleware/gooddata_middleware.rb +112 -0
  45. data/lib/gooddata/bricks/middleware/logger_middleware.rb +33 -0
  46. data/lib/gooddata/bricks/middleware/middleware.rb +12 -0
  47. data/lib/gooddata/bricks/middleware/restforce_middleware.rb +58 -0
  48. data/lib/gooddata/bricks/middleware/stdout_middleware.rb +23 -0
  49. data/lib/gooddata/bricks/middleware/twitter_middleware.rb +29 -0
  50. data/lib/gooddata/bricks/middleware/undot_params_middleware.rb +37 -0
  51. data/lib/gooddata/bricks/pipeline.rb +25 -0
  52. data/lib/gooddata/bricks/utils.rb +18 -0
  53. data/lib/gooddata/cli/cli.rb +27 -0
  54. data/lib/gooddata/cli/commands/auth_cmd.rb +29 -0
  55. data/lib/gooddata/cli/commands/domain_cmd.rb +28 -0
  56. data/lib/gooddata/cli/commands/project_cmd.rb +28 -0
  57. data/lib/gooddata/cli/hooks.rb +56 -0
  58. data/lib/gooddata/cli/shared.rb +66 -0
  59. data/lib/gooddata/cli/terminal.rb +20 -0
  60. data/lib/gooddata/client.rb +67 -0
  61. data/lib/gooddata/commands/api.rb +64 -0
  62. data/lib/gooddata/commands/auth.rb +107 -0
  63. data/lib/gooddata/commands/base.rb +12 -0
  64. data/lib/gooddata/commands/commands.rb +12 -0
  65. data/lib/gooddata/commands/datasets.rb +148 -0
  66. data/lib/gooddata/commands/datawarehouse.rb +20 -0
  67. data/lib/gooddata/commands/domain.rb +40 -0
  68. data/lib/gooddata/commands/process.rb +67 -0
  69. data/lib/gooddata/commands/project.rb +144 -0
  70. data/lib/gooddata/commands/projects.rb +20 -0
  71. data/lib/gooddata/commands/role.rb +36 -0
  72. data/lib/gooddata/commands/runners.rb +47 -0
  73. data/lib/gooddata/commands/scaffold.rb +69 -0
  74. data/lib/gooddata/commands/user.rb +39 -0
  75. data/lib/gooddata/connection.rb +127 -0
  76. data/lib/gooddata/core/core.rb +12 -0
  77. data/lib/gooddata/core/logging.rb +105 -0
  78. data/lib/gooddata/core/nil_logger.rb +32 -0
  79. data/lib/gooddata/core/project.rb +74 -0
  80. data/lib/gooddata/core/rest.rb +149 -0
  81. data/lib/gooddata/core/user.rb +20 -0
  82. data/lib/gooddata/data/data.rb +12 -0
  83. data/lib/gooddata/data/guesser.rb +122 -0
  84. data/lib/gooddata/exceptions/attr_element_not_found.rb +16 -0
  85. data/lib/gooddata/exceptions/command_failed.rb +11 -0
  86. data/lib/gooddata/exceptions/exceptions.rb +12 -0
  87. data/lib/gooddata/exceptions/execution_limit_exceeded.rb +13 -0
  88. data/lib/gooddata/exceptions/export_clone.rb +4 -0
  89. data/lib/gooddata/exceptions/filter_maqlization.rb +16 -0
  90. data/lib/gooddata/exceptions/import_clone.rb +4 -0
  91. data/lib/gooddata/exceptions/malformed_user.rb +15 -0
  92. data/lib/gooddata/exceptions/maql_execution.rb +16 -0
  93. data/lib/gooddata/exceptions/no_project_error.rb +19 -0
  94. data/lib/gooddata/exceptions/object_migration.rb +32 -0
  95. data/lib/gooddata/exceptions/project_not_found.rb +13 -0
  96. data/lib/gooddata/exceptions/segment_not_empty.rb +18 -0
  97. data/lib/gooddata/exceptions/uncomputable_report.rb +13 -0
  98. data/lib/gooddata/exceptions/user_in_different_domain.rb +15 -0
  99. data/lib/gooddata/exceptions/validation_error.rb +16 -0
  100. data/lib/gooddata/extensions/big_decimal.rb +17 -0
  101. data/lib/gooddata/extensions/class.rb +11 -0
  102. data/lib/gooddata/extensions/enumerable.rb +39 -0
  103. data/lib/gooddata/extensions/extensions.rb +10 -0
  104. data/lib/gooddata/extensions/false.rb +23 -0
  105. data/lib/gooddata/extensions/hash.rb +49 -0
  106. data/lib/gooddata/extensions/integer.rb +5 -0
  107. data/lib/gooddata/extensions/nil.rb +19 -0
  108. data/lib/gooddata/extensions/numeric.rb +15 -0
  109. data/lib/gooddata/extensions/object.rb +31 -0
  110. data/lib/gooddata/extensions/string.rb +7 -0
  111. data/lib/gooddata/extensions/symbol.rb +15 -0
  112. data/lib/gooddata/extensions/true.rb +23 -0
  113. data/lib/gooddata/extract.rb +21 -0
  114. data/lib/gooddata/goodzilla/goodzilla.rb +160 -0
  115. data/lib/gooddata/helpers/auth_helpers.rb +75 -0
  116. data/lib/gooddata/helpers/csv_helper.rb +61 -0
  117. data/lib/gooddata/helpers/data_helper.rb +129 -0
  118. data/lib/gooddata/helpers/erb_helper.rb +23 -0
  119. data/lib/gooddata/helpers/global_helpers.rb +266 -0
  120. data/lib/gooddata/helpers/global_helpers_params.rb +292 -0
  121. data/lib/gooddata/helpers/helpers.rb +10 -0
  122. data/lib/gooddata/lcm/actions/actions.rb +12 -0
  123. data/lib/gooddata/lcm/actions/apply_custom_maql.rb +80 -0
  124. data/lib/gooddata/lcm/actions/associate_clients.rb +87 -0
  125. data/lib/gooddata/lcm/actions/base_action.rb +23 -0
  126. data/lib/gooddata/lcm/actions/collect_ca_metrics.rb +53 -0
  127. data/lib/gooddata/lcm/actions/collect_client_projects.rb +78 -0
  128. data/lib/gooddata/lcm/actions/collect_clients.rb +128 -0
  129. data/lib/gooddata/lcm/actions/collect_data_product.rb +57 -0
  130. data/lib/gooddata/lcm/actions/collect_dynamic_schedule_params.rb +62 -0
  131. data/lib/gooddata/lcm/actions/collect_ldm_objects.rb +56 -0
  132. data/lib/gooddata/lcm/actions/collect_meta.rb +88 -0
  133. data/lib/gooddata/lcm/actions/collect_segment_clients.rb +113 -0
  134. data/lib/gooddata/lcm/actions/collect_segments.rb +72 -0
  135. data/lib/gooddata/lcm/actions/collect_tagged_objects.rb +80 -0
  136. data/lib/gooddata/lcm/actions/collect_users_brick_users.rb +46 -0
  137. data/lib/gooddata/lcm/actions/create_segment_masters.rb +160 -0
  138. data/lib/gooddata/lcm/actions/ensure_data_product.rb +53 -0
  139. data/lib/gooddata/lcm/actions/ensure_release_table.rb +53 -0
  140. data/lib/gooddata/lcm/actions/ensure_segments.rb +32 -0
  141. data/lib/gooddata/lcm/actions/ensure_technical_users_domain.rb +70 -0
  142. data/lib/gooddata/lcm/actions/ensure_technical_users_project.rb +83 -0
  143. data/lib/gooddata/lcm/actions/execute_schedules.rb +128 -0
  144. data/lib/gooddata/lcm/actions/hello_world.rb +41 -0
  145. data/lib/gooddata/lcm/actions/import_object_collections.rb +60 -0
  146. data/lib/gooddata/lcm/actions/print_actions.rb +58 -0
  147. data/lib/gooddata/lcm/actions/print_modes.rb +69 -0
  148. data/lib/gooddata/lcm/actions/print_types.rb +52 -0
  149. data/lib/gooddata/lcm/actions/provision_clients.rb +89 -0
  150. data/lib/gooddata/lcm/actions/purge_clients.rb +58 -0
  151. data/lib/gooddata/lcm/actions/rename_existing_client_projects.rb +70 -0
  152. data/lib/gooddata/lcm/actions/segments_filter.rb +50 -0
  153. data/lib/gooddata/lcm/actions/synchronize_attribute_drillpaths.rb +64 -0
  154. data/lib/gooddata/lcm/actions/synchronize_cas.rb +72 -0
  155. data/lib/gooddata/lcm/actions/synchronize_clients.rb +94 -0
  156. data/lib/gooddata/lcm/actions/synchronize_color_palette.rb +67 -0
  157. data/lib/gooddata/lcm/actions/synchronize_etls_in_segment.rb +155 -0
  158. data/lib/gooddata/lcm/actions/synchronize_label_types.rb +64 -0
  159. data/lib/gooddata/lcm/actions/synchronize_ldm.rb +82 -0
  160. data/lib/gooddata/lcm/actions/synchronize_meta.rb +52 -0
  161. data/lib/gooddata/lcm/actions/synchronize_new_segments.rb +61 -0
  162. data/lib/gooddata/lcm/actions/synchronize_processes.rb +66 -0
  163. data/lib/gooddata/lcm/actions/synchronize_schedules.rb +91 -0
  164. data/lib/gooddata/lcm/actions/synchronize_tag_objects.rb +64 -0
  165. data/lib/gooddata/lcm/actions/synchronize_user_filters.rb +210 -0
  166. data/lib/gooddata/lcm/actions/synchronize_user_groups.rb +53 -0
  167. data/lib/gooddata/lcm/actions/synchronize_users.rb +336 -0
  168. data/lib/gooddata/lcm/actions/update_release_table.rb +77 -0
  169. data/lib/gooddata/lcm/data/create_lcm_release.sql.erb +6 -0
  170. data/lib/gooddata/lcm/data/insert_into_lcm_release.sql.erb +11 -0
  171. data/lib/gooddata/lcm/data/select_from_lcm_release.sql.erb +8 -0
  172. data/lib/gooddata/lcm/data/update_lcm_release.sql.erb +7 -0
  173. data/lib/gooddata/lcm/dsl/dsl.rb +50 -0
  174. data/lib/gooddata/lcm/dsl/params_dsl.rb +61 -0
  175. data/lib/gooddata/lcm/dsl/type_dsl.rb +62 -0
  176. data/lib/gooddata/lcm/helpers/check_helper.rb +41 -0
  177. data/lib/gooddata/lcm/helpers/helpers.rb +12 -0
  178. data/lib/gooddata/lcm/helpers/tags_helper.rb +36 -0
  179. data/lib/gooddata/lcm/lcm.rb +325 -0
  180. data/lib/gooddata/lcm/lcm2.rb +400 -0
  181. data/lib/gooddata/lcm/types/base_type.rb +29 -0
  182. data/lib/gooddata/lcm/types/class/ads_client.rb +35 -0
  183. data/lib/gooddata/lcm/types/class/class.rb +21 -0
  184. data/lib/gooddata/lcm/types/class/gd_client.rb +35 -0
  185. data/lib/gooddata/lcm/types/class/types.rb +12 -0
  186. data/lib/gooddata/lcm/types/complex/complex.rb +33 -0
  187. data/lib/gooddata/lcm/types/complex/release_query.rb +35 -0
  188. data/lib/gooddata/lcm/types/complex/segment.rb +40 -0
  189. data/lib/gooddata/lcm/types/complex/synchronization_info.rb +35 -0
  190. data/lib/gooddata/lcm/types/complex/tokens.rb +31 -0
  191. data/lib/gooddata/lcm/types/complex/types.rb +12 -0
  192. data/lib/gooddata/lcm/types/complex/update_preference.rb +38 -0
  193. data/lib/gooddata/lcm/types/complex/users_brick_config.rb +32 -0
  194. data/lib/gooddata/lcm/types/param.rb +16 -0
  195. data/lib/gooddata/lcm/types/scalar/bool.rb +22 -0
  196. data/lib/gooddata/lcm/types/scalar/hash.rb +22 -0
  197. data/lib/gooddata/lcm/types/scalar/integer.rb +22 -0
  198. data/lib/gooddata/lcm/types/scalar/object.rb +22 -0
  199. data/lib/gooddata/lcm/types/scalar/string.rb +22 -0
  200. data/lib/gooddata/lcm/types/scalar/types.rb +12 -0
  201. data/lib/gooddata/lcm/types/special/array.rb +33 -0
  202. data/lib/gooddata/lcm/types/special/enum.rb +15 -0
  203. data/lib/gooddata/lcm/types/special/types.rb +12 -0
  204. data/lib/gooddata/lcm/types/types.rb +12 -0
  205. data/lib/gooddata/lcm/user_bricks_helper.rb +32 -0
  206. data/lib/gooddata/mixins/author.rb +26 -0
  207. data/lib/gooddata/mixins/content_getter.rb +15 -0
  208. data/lib/gooddata/mixins/content_property_reader.rb +17 -0
  209. data/lib/gooddata/mixins/content_property_writer.rb +17 -0
  210. data/lib/gooddata/mixins/contributor.rb +20 -0
  211. data/lib/gooddata/mixins/data_getter.rb +15 -0
  212. data/lib/gooddata/mixins/data_property_reader.rb +19 -0
  213. data/lib/gooddata/mixins/data_property_writer.rb +19 -0
  214. data/lib/gooddata/mixins/inspector.rb +53 -0
  215. data/lib/gooddata/mixins/is_attribute.rb +17 -0
  216. data/lib/gooddata/mixins/is_dimension.rb +17 -0
  217. data/lib/gooddata/mixins/is_fact.rb +17 -0
  218. data/lib/gooddata/mixins/is_folder.rb +11 -0
  219. data/lib/gooddata/mixins/is_label.rb +19 -0
  220. data/lib/gooddata/mixins/links.rb +15 -0
  221. data/lib/gooddata/mixins/md_finders.rb +79 -0
  222. data/lib/gooddata/mixins/md_grantees.rb +42 -0
  223. data/lib/gooddata/mixins/md_id_to_uri.rb +39 -0
  224. data/lib/gooddata/mixins/md_json.rb +15 -0
  225. data/lib/gooddata/mixins/md_lock.rb +87 -0
  226. data/lib/gooddata/mixins/md_object_id.rb +15 -0
  227. data/lib/gooddata/mixins/md_object_indexer.rb +74 -0
  228. data/lib/gooddata/mixins/md_object_query.rb +134 -0
  229. data/lib/gooddata/mixins/md_relations.rb +43 -0
  230. data/lib/gooddata/mixins/meta_getter.rb +17 -0
  231. data/lib/gooddata/mixins/meta_property_reader.rb +19 -0
  232. data/lib/gooddata/mixins/meta_property_writer.rb +19 -0
  233. data/lib/gooddata/mixins/mixins.rb +19 -0
  234. data/lib/gooddata/mixins/not_attribute.rb +17 -0
  235. data/lib/gooddata/mixins/not_exportable.rb +15 -0
  236. data/lib/gooddata/mixins/not_fact.rb +17 -0
  237. data/lib/gooddata/mixins/not_group.rb +17 -0
  238. data/lib/gooddata/mixins/not_label.rb +19 -0
  239. data/lib/gooddata/mixins/not_metric.rb +19 -0
  240. data/lib/gooddata/mixins/obj_id.rb +15 -0
  241. data/lib/gooddata/mixins/rest_getters.rb +17 -0
  242. data/lib/gooddata/mixins/rest_resource.rb +47 -0
  243. data/lib/gooddata/mixins/root_key_getter.rb +15 -0
  244. data/lib/gooddata/mixins/root_key_setter.rb +15 -0
  245. data/lib/gooddata/mixins/timestamps.rb +19 -0
  246. data/lib/gooddata/mixins/to_json.rb +11 -0
  247. data/lib/gooddata/mixins/uri_getter.rb +9 -0
  248. data/lib/gooddata/models/ads_output_stage.rb +85 -0
  249. data/lib/gooddata/models/automated_data_distribution.rb +36 -0
  250. data/lib/gooddata/models/blueprint/anchor_field.rb +65 -0
  251. data/lib/gooddata/models/blueprint/attribute_field.rb +45 -0
  252. data/lib/gooddata/models/blueprint/blueprint.rb +11 -0
  253. data/lib/gooddata/models/blueprint/blueprint_field.rb +70 -0
  254. data/lib/gooddata/models/blueprint/dashboard_builder.rb +30 -0
  255. data/lib/gooddata/models/blueprint/dataset_blueprint.rb +455 -0
  256. data/lib/gooddata/models/blueprint/date_dimension.rb +20 -0
  257. data/lib/gooddata/models/blueprint/fact_field.rb +20 -0
  258. data/lib/gooddata/models/blueprint/label_field.rb +47 -0
  259. data/lib/gooddata/models/blueprint/project_blueprint.rb +791 -0
  260. data/lib/gooddata/models/blueprint/project_builder.rb +103 -0
  261. data/lib/gooddata/models/blueprint/reference_field.rb +43 -0
  262. data/lib/gooddata/models/blueprint/schema_blueprint.rb +160 -0
  263. data/lib/gooddata/models/blueprint/schema_builder.rb +89 -0
  264. data/lib/gooddata/models/blueprint/to_manifest.rb +185 -0
  265. data/lib/gooddata/models/blueprint/to_wire.rb +173 -0
  266. data/lib/gooddata/models/channel_configuration.rb +112 -0
  267. data/lib/gooddata/models/client.rb +236 -0
  268. data/lib/gooddata/models/client_synchronization_result.rb +31 -0
  269. data/lib/gooddata/models/client_synchronization_result_details.rb +41 -0
  270. data/lib/gooddata/models/data_product.rb +149 -0
  271. data/lib/gooddata/models/datawarehouse.rb +114 -0
  272. data/lib/gooddata/models/domain.rb +505 -0
  273. data/lib/gooddata/models/execution.rb +115 -0
  274. data/lib/gooddata/models/execution_detail.rb +81 -0
  275. data/lib/gooddata/models/from_wire.rb +173 -0
  276. data/lib/gooddata/models/invitation.rb +75 -0
  277. data/lib/gooddata/models/links.rb +50 -0
  278. data/lib/gooddata/models/membership.rb +441 -0
  279. data/lib/gooddata/models/metadata.rb +324 -0
  280. data/lib/gooddata/models/metadata/attribute.rb +155 -0
  281. data/lib/gooddata/models/metadata/dashboard.rb +120 -0
  282. data/lib/gooddata/models/metadata/dashboard/dashboard_item.rb +76 -0
  283. data/lib/gooddata/models/metadata/dashboard/filter_apply_item.rb +37 -0
  284. data/lib/gooddata/models/metadata/dashboard/filter_item.rb +64 -0
  285. data/lib/gooddata/models/metadata/dashboard/geo_chart_item.rb +56 -0
  286. data/lib/gooddata/models/metadata/dashboard/headline_item.rb +56 -0
  287. data/lib/gooddata/models/metadata/dashboard/iframe_item.rb +46 -0
  288. data/lib/gooddata/models/metadata/dashboard/report_item.rb +92 -0
  289. data/lib/gooddata/models/metadata/dashboard/text_item.rb +55 -0
  290. data/lib/gooddata/models/metadata/dashboard_tab.rb +141 -0
  291. data/lib/gooddata/models/metadata/dataset.rb +67 -0
  292. data/lib/gooddata/models/metadata/dimension.rb +57 -0
  293. data/lib/gooddata/models/metadata/fact.rb +51 -0
  294. data/lib/gooddata/models/metadata/folder.rb +49 -0
  295. data/lib/gooddata/models/metadata/label.rb +128 -0
  296. data/lib/gooddata/models/metadata/metadata.rb +12 -0
  297. data/lib/gooddata/models/metadata/metric.rb +206 -0
  298. data/lib/gooddata/models/metadata/report.rb +268 -0
  299. data/lib/gooddata/models/metadata/report_definition.rb +272 -0
  300. data/lib/gooddata/models/metadata/scheduled_mail.rb +277 -0
  301. data/lib/gooddata/models/metadata/scheduled_mail/dashboard_attachment.rb +62 -0
  302. data/lib/gooddata/models/metadata/scheduled_mail/report_attachment.rb +64 -0
  303. data/lib/gooddata/models/metadata/variable.rb +96 -0
  304. data/lib/gooddata/models/model.rb +293 -0
  305. data/lib/gooddata/models/models.rb +12 -0
  306. data/lib/gooddata/models/module_constants.rb +31 -0
  307. data/lib/gooddata/models/notification_rule.rb +113 -0
  308. data/lib/gooddata/models/process.rb +371 -0
  309. data/lib/gooddata/models/profile.rb +451 -0
  310. data/lib/gooddata/models/project.rb +2030 -0
  311. data/lib/gooddata/models/project_creator.rb +209 -0
  312. data/lib/gooddata/models/project_log_formatter.rb +204 -0
  313. data/lib/gooddata/models/project_metadata.rb +67 -0
  314. data/lib/gooddata/models/project_role.rb +79 -0
  315. data/lib/gooddata/models/report_data_result.rb +270 -0
  316. data/lib/gooddata/models/schedule.rb +538 -0
  317. data/lib/gooddata/models/segment.rb +274 -0
  318. data/lib/gooddata/models/style_setting.rb +62 -0
  319. data/lib/gooddata/models/subscription.rb +188 -0
  320. data/lib/gooddata/models/tab_builder.rb +27 -0
  321. data/lib/gooddata/models/user_filters/mandatory_user_filter.rb +76 -0
  322. data/lib/gooddata/models/user_filters/user_filter.rb +101 -0
  323. data/lib/gooddata/models/user_filters/user_filter_builder.rb +553 -0
  324. data/lib/gooddata/models/user_filters/user_filters.rb +13 -0
  325. data/lib/gooddata/models/user_filters/variable_user_filter.rb +33 -0
  326. data/lib/gooddata/models/user_group.rb +250 -0
  327. data/lib/gooddata/rest/README.md +37 -0
  328. data/lib/gooddata/rest/client.rb +396 -0
  329. data/lib/gooddata/rest/connection.rb +776 -0
  330. data/lib/gooddata/rest/object.rb +69 -0
  331. data/lib/gooddata/rest/object_factory.rb +51 -0
  332. data/lib/gooddata/rest/resource.rb +27 -0
  333. data/lib/gooddata/rest/rest.rb +24 -0
  334. data/lib/gooddata/version.rb +23 -0
  335. data/lib/templates/bricks/brick.rb.erb +7 -0
  336. data/lib/templates/bricks/main.rb.erb +5 -0
  337. data/lib/templates/project/Goodfile.erb +4 -0
  338. data/lib/templates/project/data/commits.csv +4 -0
  339. data/lib/templates/project/data/devs.csv +4 -0
  340. data/lib/templates/project/data/repos.csv +3 -0
  341. data/lib/templates/project/model/model.rb.erb +20 -0
  342. data/spec/.rubocop.yml +16 -0
  343. data/spec/bricks/bricks_spec.rb +110 -0
  344. data/spec/bricks/default-config.json +8 -0
  345. data/spec/data/.gooddata +4 -0
  346. data/spec/data/blueprints/additional_dataset_module.json +32 -0
  347. data/spec/data/blueprints/attribute_sort_order_blueprint.json +72 -0
  348. data/spec/data/blueprints/big_blueprint_not_pruned.json +2079 -0
  349. data/spec/data/blueprints/invalid_blueprint.json +103 -0
  350. data/spec/data/blueprints/m_n_model.json +104 -0
  351. data/spec/data/blueprints/model_module.json +25 -0
  352. data/spec/data/blueprints/test_blueprint.json +39 -0
  353. data/spec/data/blueprints/test_project_model_spec.json +106 -0
  354. data/spec/data/cc/data/source/commits.csv +4 -0
  355. data/spec/data/cc/data/source/devs.csv +4 -0
  356. data/spec/data/cc/data/source/repos.csv +3 -0
  357. data/spec/data/cc/devel.prm +0 -0
  358. data/spec/data/cc/graph/graph.grf +11 -0
  359. data/spec/data/cc/workspace.prm +19 -0
  360. data/spec/data/column_based_permissions.csv +7 -0
  361. data/spec/data/column_based_permissions2.csv +6 -0
  362. data/spec/data/dynamic_schedule_params_table.csv +7 -0
  363. data/spec/data/gd_gse_data_blueprint.json +1371 -0
  364. data/spec/data/gd_gse_data_manifest.json +1424 -0
  365. data/spec/data/gd_gse_data_model.json +1772 -0
  366. data/spec/data/gooddata_version_process/gooddata_version.rb +9 -0
  367. data/spec/data/gooddata_version_process/gooddata_version.zip +0 -0
  368. data/spec/data/hello_world_process/hello_world.rb +9 -0
  369. data/spec/data/hello_world_process/hello_world.zip +0 -0
  370. data/spec/data/line_based_permissions.csv +3 -0
  371. data/spec/data/manifests/test_blueprint.json +32 -0
  372. data/spec/data/manifests/test_project.json +107 -0
  373. data/spec/data/reports/left_attr_report.json +108 -0
  374. data/spec/data/reports/metric_only_one_line.json +83 -0
  375. data/spec/data/reports/report_1.json +197 -0
  376. data/spec/data/reports/top_attr_report.json +108 -0
  377. data/spec/data/ruby_params_process/ruby_params.rb +9 -0
  378. data/spec/data/ruby_process/deep_files/deep_stuff.txt +1 -0
  379. data/spec/data/ruby_process/process.rb +8 -0
  380. data/spec/data/ruby_process/stuff.txt +1 -0
  381. data/spec/data/superfluous_titles_view.json +81 -0
  382. data/spec/data/test-ci-data.csv +2 -0
  383. data/spec/data/users.csv +12 -0
  384. data/spec/data/wire_models/attribute_sort_by_model.json +73 -0
  385. data/spec/data/wire_models/model_view.json +1775 -0
  386. data/spec/data/wire_models/nu_model.json +3046 -0
  387. data/spec/data/wire_models/test_blueprint.json +66 -0
  388. data/spec/data/wire_test_project.json +150 -0
  389. data/spec/data/workspace_table.csv +3 -0
  390. data/spec/environment/default.rb +43 -0
  391. data/spec/environment/development.rb +32 -0
  392. data/spec/environment/environment.rb +38 -0
  393. data/spec/environment/production.rb +27 -0
  394. data/spec/environment/staging.rb +33 -0
  395. data/spec/environment/testing.rb +32 -0
  396. data/spec/helpers/blueprint_helper.rb +27 -0
  397. data/spec/helpers/cli_helper.rb +38 -0
  398. data/spec/helpers/connection_helper.rb +43 -0
  399. data/spec/helpers/crypto_helper.rb +19 -0
  400. data/spec/helpers/csv_helper.rb +20 -0
  401. data/spec/helpers/process_helper.rb +35 -0
  402. data/spec/helpers/project_helper.rb +74 -0
  403. data/spec/helpers/schedule_helper.rb +33 -0
  404. data/spec/helpers/spec_helper.rb +17 -0
  405. data/spec/integration/ads_output_stage_spec.rb +45 -0
  406. data/spec/integration/blueprint_updates_spec.rb +107 -0
  407. data/spec/integration/blueprint_with_ca_spec.rb +56 -0
  408. data/spec/integration/blueprint_with_grain_spec.rb +74 -0
  409. data/spec/integration/channel_configuration_spec.rb +67 -0
  410. data/spec/integration/clients_spec.rb +164 -0
  411. data/spec/integration/command_datawarehouse_spec.rb +45 -0
  412. data/spec/integration/command_projects_spec.rb +32 -0
  413. data/spec/integration/commands/command_projects_spec.rb +22 -0
  414. data/spec/integration/core/connection_spec.rb +56 -0
  415. data/spec/integration/core/logging_spec.rb +130 -0
  416. data/spec/integration/core/project_spec.rb +54 -0
  417. data/spec/integration/create_from_template_spec.rb +29 -0
  418. data/spec/integration/create_project_spec.rb +27 -0
  419. data/spec/integration/date_dim_switch_spec.rb +150 -0
  420. data/spec/integration/deprecated_load_spec.rb +60 -0
  421. data/spec/integration/full_process_schedule_spec.rb +367 -0
  422. data/spec/integration/full_project_spec.rb +592 -0
  423. data/spec/integration/helpers_spec.rb +16 -0
  424. data/spec/integration/lcm_spec.rb +54 -0
  425. data/spec/integration/mixins/id_to_uri_spec.rb +44 -0
  426. data/spec/integration/models/data_product_spec.rb +71 -0
  427. data/spec/integration/models/domain_spec.rb +162 -0
  428. data/spec/integration/models/invitation_spec.rb +17 -0
  429. data/spec/integration/models/membership_spec.rb +127 -0
  430. data/spec/integration/models/metadata/report_spec.rb +54 -0
  431. data/spec/integration/models/params_spec.rb +118 -0
  432. data/spec/integration/models/profile_spec.rb +210 -0
  433. data/spec/integration/models/project_role_spec.rb +94 -0
  434. data/spec/integration/models/project_spec.rb +225 -0
  435. data/spec/integration/models/schedule_spec.rb +485 -0
  436. data/spec/integration/models/unit_project_spec.rb +130 -0
  437. data/spec/integration/over_to_user_filters_spec.rb +98 -0
  438. data/spec/integration/partial_md_export_import_spec.rb +41 -0
  439. data/spec/integration/project_spec.rb +381 -0
  440. data/spec/integration/rest_spec.rb +214 -0
  441. data/spec/integration/schedule_spec.rb +613 -0
  442. data/spec/integration/segments_spec.rb +100 -0
  443. data/spec/integration/subscription_spec.rb +88 -0
  444. data/spec/integration/urn_date_dim_spec.rb +53 -0
  445. data/spec/integration/user_filters_spec.rb +306 -0
  446. data/spec/integration/user_group_spec.rb +147 -0
  447. data/spec/integration/variables_spec.rb +189 -0
  448. data/spec/logging_in_logging_out_spec.rb +91 -0
  449. data/spec/spec_helper.rb +66 -0
  450. data/spec/unit/actions/associate_clients_spec.rb +47 -0
  451. data/spec/unit/actions/collect_client_projects_spec.rb +47 -0
  452. data/spec/unit/actions/collect_clients_spec.rb +65 -0
  453. data/spec/unit/actions/collect_data_product_spec.rb +56 -0
  454. data/spec/unit/actions/collect_dynamic_schedule_params_spec.rb +56 -0
  455. data/spec/unit/actions/collect_meta_spec.rb +88 -0
  456. data/spec/unit/actions/collect_segment_clients_spec.rb +81 -0
  457. data/spec/unit/actions/collect_tagged_objects_spec.rb +126 -0
  458. data/spec/unit/actions/collect_users_brick_users_spec.rb +36 -0
  459. data/spec/unit/actions/create_segment_masters_spec.rb +64 -0
  460. data/spec/unit/actions/ensure_data_product_spec.rb +38 -0
  461. data/spec/unit/actions/ensure_technical_users_domain_spec.rb +51 -0
  462. data/spec/unit/actions/ensure_technical_users_project_spec.rb +72 -0
  463. data/spec/unit/actions/execute_schedules_spec.rb +94 -0
  464. data/spec/unit/actions/provision_clients_spec.rb +45 -0
  465. data/spec/unit/actions/purge_clients_spec.rb +47 -0
  466. data/spec/unit/actions/rename_existing_client_projects_spec.rb +54 -0
  467. data/spec/unit/actions/segments_filter_spec.rb +46 -0
  468. data/spec/unit/actions/shared_examples_for_user_actions.rb +26 -0
  469. data/spec/unit/actions/synchronize_cas_spec.rb +58 -0
  470. data/spec/unit/actions/synchronize_etls_in_segment_spec.rb +212 -0
  471. data/spec/unit/actions/synchronize_ldm_spec.rb +57 -0
  472. data/spec/unit/actions/synchronize_user_filters_spec.rb +146 -0
  473. data/spec/unit/actions/synchronize_user_groups_spec.rb +49 -0
  474. data/spec/unit/actions/synchronize_users_spec.rb +134 -0
  475. data/spec/unit/bricks/bricks_spec.rb +34 -0
  476. data/spec/unit/bricks/middleware/aws_middelware_spec.rb +98 -0
  477. data/spec/unit/bricks/middleware/bench_middleware_spec.rb +15 -0
  478. data/spec/unit/bricks/middleware/bulk_salesforce_middleware_spec.rb +15 -0
  479. data/spec/unit/bricks/middleware/gooddata_middleware_spec.rb +15 -0
  480. data/spec/unit/bricks/middleware/logger_middleware_spec.rb +30 -0
  481. data/spec/unit/bricks/middleware/restforce_middleware_spec.rb +15 -0
  482. data/spec/unit/bricks/middleware/stdout_middleware_spec.rb +15 -0
  483. data/spec/unit/bricks/middleware/twitter_middleware_spec.rb +15 -0
  484. data/spec/unit/cli/cli_spec.rb +17 -0
  485. data/spec/unit/cli/commands/cmd_auth_spec.rb +17 -0
  486. data/spec/unit/core/nil_logger_spec.rb +13 -0
  487. data/spec/unit/extensions/hash_spec.rb +22 -0
  488. data/spec/unit/godzilla/goodzilla_spec.rb +74 -0
  489. data/spec/unit/helpers/csv_helper_spec.rb +22 -0
  490. data/spec/unit/helpers/data_helper_spec.rb +67 -0
  491. data/spec/unit/helpers/global_helpers_spec.rb +264 -0
  492. data/spec/unit/helpers_spec.rb +254 -0
  493. data/spec/unit/lcm/user_bricks_helper_spec.rb +58 -0
  494. data/spec/unit/models/blueprint/attribute_sort_by_spec.rb +20 -0
  495. data/spec/unit/models/blueprint/attributes_spec.rb +28 -0
  496. data/spec/unit/models/blueprint/dataset_spec.rb +120 -0
  497. data/spec/unit/models/blueprint/labels_spec.rb +43 -0
  498. data/spec/unit/models/blueprint/project_blueprint_spec.rb +642 -0
  499. data/spec/unit/models/blueprint/reference_spec.rb +28 -0
  500. data/spec/unit/models/blueprint/schema_builder_spec.rb +36 -0
  501. data/spec/unit/models/blueprint/to_wire_spec.rb +195 -0
  502. data/spec/unit/models/execution_spec.rb +109 -0
  503. data/spec/unit/models/from_wire_spec.rb +301 -0
  504. data/spec/unit/models/metadata_spec.rb +140 -0
  505. data/spec/unit/models/metric_spec.rb +129 -0
  506. data/spec/unit/models/model_spec.rb +77 -0
  507. data/spec/unit/models/project_creator_spec.rb +90 -0
  508. data/spec/unit/models/project_spec.rb +94 -0
  509. data/spec/unit/models/report_result_data_spec.rb +194 -0
  510. data/spec/unit/models/to_manifest_spec.rb +136 -0
  511. data/spec/unit/models/user_filters/user_filter_builder_spec.rb +110 -0
  512. data/spec/unit/models/user_filters_spec.rb +95 -0
  513. data/spec/unit/models/variable_spec.rb +280 -0
  514. data/spec/unit/rest/polling_spec.rb +101 -0
  515. data/spec/unit/rest/resource_spec.rb +10 -0
  516. data/yard-server.sh +3 -0
  517. metadata +1207 -0
@@ -0,0 +1,2030 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright (c) 2010-2017 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
+ require 'net/smtp'
14
+
15
+ require_relative '../exceptions/no_project_error'
16
+
17
+ require_relative '../helpers/auth_helpers'
18
+
19
+ require_relative '../rest/resource'
20
+ require_relative '../mixins/author'
21
+ require_relative '../mixins/contributor'
22
+ require_relative '../mixins/rest_resource'
23
+ require_relative '../mixins/uri_getter'
24
+
25
+ require_relative 'membership'
26
+ require_relative 'process'
27
+ require_relative 'project_log_formatter'
28
+ require_relative 'project_role'
29
+ require_relative 'blueprint/blueprint'
30
+
31
+ require_relative 'metadata/scheduled_mail'
32
+ require_relative 'metadata/scheduled_mail/dashboard_attachment'
33
+ require_relative 'metadata/scheduled_mail/report_attachment'
34
+
35
+ module GoodData
36
+ class Project < Rest::Resource
37
+ USERSPROJECTS_PATH = '/gdc/account/profile/%s/projects'
38
+ PROJECTS_PATH = '/gdc/projects'
39
+ PROJECT_PATH = '/gdc/projects/%s'
40
+ SLIS_PATH = '/ldm/singleloadinterface'
41
+ DEFAULT_INVITE_MESSAGE = 'Join us!'
42
+ DEFAULT_ENVIRONMENT = 'PRODUCTION'
43
+
44
+ EMPTY_OBJECT = {
45
+ 'project' => {
46
+ 'meta' => {
47
+ 'summary' => 'No summary'
48
+ },
49
+ 'content' => {
50
+ 'guidedNavigation' => 1,
51
+ 'driver' => 'Pg',
52
+ 'environment' => GoodData::Helpers::AuthHelper.read_environment
53
+ }
54
+ }
55
+ }
56
+
57
+ attr_accessor :connection, :json
58
+
59
+ include Mixin::Author
60
+ include Mixin::Contributor
61
+ include Mixin::UriGetter
62
+
63
+ class << self
64
+ # Returns an array of all projects accessible by
65
+ # current user
66
+ def all(opts = { client: GoodData.connection })
67
+ c = GoodData.get_client(opts)
68
+ c.user.projects
69
+ end
70
+
71
+ # Returns a Project object identified by given string
72
+ # The following identifiers are accepted
73
+ # - /gdc/md/<id>
74
+ # - /gdc/projects/<id>
75
+ # - <id>
76
+ #
77
+ def [](id, opts = { client: GoodData.connection })
78
+ return id if id.instance_of?(GoodData::Project) || id.respond_to?(:project?) && id.project?
79
+
80
+ if id == :all
81
+ Project.all({ client: GoodData.connection }.merge(opts))
82
+ else
83
+ fail(ArgumentError, 'wrong type of argument. Should be either project ID or path') unless project_id_or_path?(id)
84
+
85
+ id = id.match(/[a-zA-Z\d]+$/)[0] if id =~ %r{/}
86
+
87
+ c = GoodData.get_client(opts)
88
+ response = c.get(PROJECT_PATH % id)
89
+ c.factory.create(Project, response)
90
+ end
91
+ end
92
+
93
+ def project_id_or_path?(id)
94
+ id.to_s =~ %r{^(\/gdc\/(projects|md)\/)?[a-zA-Z\d]+$}
95
+ end
96
+
97
+ # Clones project along with etl and schedules
98
+ #
99
+ # @param project [Project] Project to be cloned from
100
+ # @param [options] Options that are passed into project.clone
101
+ # @return [GoodData::Project] New cloned project
102
+ def clone_with_etl(project, options = {})
103
+ a_clone = project.clone(options)
104
+ GoodData::Project.transfer_etl(project.client, project, a_clone)
105
+ a_clone
106
+ end
107
+
108
+ def create_object(data = {})
109
+ c = GoodData.get_client(data)
110
+ new_data = GoodData::Helpers.deep_dup(EMPTY_OBJECT).tap do |d|
111
+ d['project']['meta']['title'] = data[:title]
112
+ d['project']['meta']['summary'] = data[:summary] if data[:summary]
113
+ d['project']['meta']['projectTemplate'] = data[:template] if data[:template]
114
+ d['project']['content']['guidedNavigation'] = data[:guided_navigation] if data[:guided_navigation]
115
+
116
+ token = data[:auth_token] || data[:token]
117
+
118
+ d['project']['content']['authorizationToken'] = token if token
119
+ d['project']['content']['driver'] = data[:driver] if data[:driver]
120
+ d['project']['content']['environment'] = data[:environment] if data[:environment]
121
+ end
122
+ c.create(Project, new_data)
123
+ end
124
+
125
+ # Create a project from a given attributes
126
+ # Expected keys:
127
+ # - :title (mandatory)
128
+ # - :summary
129
+ # - :template (default /projects/blank)
130
+ #
131
+ def create(opts = { client: GoodData.connection }, &block)
132
+ GoodData.logger.info "Creating project #{opts[:title]}"
133
+
134
+ auth_token = opts[:auth_token] || opts[:token]
135
+ if auth_token.nil? || auth_token.empty?
136
+ opts = { auth_token: Helpers::AuthHelper.read_token }.merge(opts)
137
+ auth_token = opts[:auth_token]
138
+ end
139
+ fail ArgumentError, 'You have to provide your token for creating projects as :auth_token or :token parameter' if auth_token.nil? || auth_token.empty?
140
+
141
+ project = create_object(opts)
142
+ project.save
143
+ # until it is enabled or deleted, recur. This should still end if there
144
+ # is a exception thrown out from RESTClient. This sometimes happens from
145
+ # WebApp when request is too long
146
+ while project.state.to_s != 'enabled'
147
+ if project.deleted?
148
+ # if project is switched to deleted state, fail. This is usually problem of creating a template which is invalid.
149
+ fail 'Project was marked as deleted during creation. This usually means you were trying to create from template and it failed.'
150
+ end
151
+ sleep(3)
152
+ project.reload!
153
+ end
154
+
155
+ if block
156
+ GoodData.with_project(project) do |p|
157
+ block.call(p)
158
+ end
159
+ end
160
+ sleep 3
161
+ project
162
+ end
163
+
164
+ def find(opts = { client: GoodData.connection })
165
+ c = GoodData.get_client(opts)
166
+ user = c.user
167
+ user.projects['projects'].map do |project|
168
+ c.create(GoodData::Project, project)
169
+ end
170
+ end
171
+
172
+ def create_from_blueprint(blueprint, options = {})
173
+ GoodData::Model::ProjectCreator.migrate(options.merge(spec: blueprint, client: client))
174
+ end
175
+
176
+ # Takes one CSV line and creates hash from data extracted
177
+ #
178
+ # @param row CSV row
179
+ def user_csv_import(row)
180
+ {
181
+ 'user' => {
182
+ 'content' => {
183
+ 'email' => row[0],
184
+ 'login' => row[1],
185
+ 'firstname' => row[2],
186
+ 'lastname' => row[3]
187
+ },
188
+ 'meta' => {}
189
+ }
190
+ }
191
+ end
192
+
193
+ def transfer_output_stage(from_project, to_project, options)
194
+ if from_project.processes.any? { |p| p.type == :dataload }
195
+ if to_project.processes.any? { |p| p.type == :dataload }
196
+ to_project.add.output_stage.schema = from_project.add.output_stage.schema
197
+ to_project.add.output_stage.output_stage_prefix = from_project.add.output_stage.output_stage_prefix
198
+ to_project.add.output_stage.save
199
+ else
200
+ from_prj_output_stage = from_project.add.output_stage
201
+ from_server = from_project.client.connection.server.url
202
+ to_server = to_project.client.connection.server.url
203
+ if from_server != to_server && options[:ads_output_stage_uri].nil?
204
+ raise "Cannot transfer output stage from #{from_server} to #{to_server}. " \
205
+ 'It is not possible to transfer output stages between ' \
206
+ 'different domains. Please specify an address of an output ' \
207
+ 'stage that is in the same domain as the target project ' \
208
+ 'using the "ads_output_stage_uri" parameter.'
209
+ end
210
+
211
+ to_project.add.output_stage = GoodData::AdsOutputStage.create(
212
+ client: to_project.client,
213
+ ads: options[:ads_output_stage_uri] || from_prj_output_stage.schema,
214
+ client_id: from_prj_output_stage.client_id,
215
+ output_stage_prefix: from_prj_output_stage.output_stage_prefix,
216
+ project: to_project
217
+ )
218
+ end
219
+ end
220
+ end
221
+
222
+ # Clones project along with etl and schedules.
223
+ #
224
+ # @param client [GoodData::Rest::Client] GoodData client to be used for connection
225
+ # @param from_project [GoodData::Project | GoodData::Segment | GoodData:Client | String]
226
+ # Object to be cloned from. Can be either segment in which case we
227
+ # take the master, client in which case we take its project, string
228
+ # in which case we treat is as an project object or directly project
229
+ # @param to_project [GoodData::Project | GoodData::Segment | GoodData:Client | String]
230
+ def transfer_etl(client, from_project, to_project)
231
+ from_project = case from_project
232
+ when GoodData::Client
233
+ from_project.project
234
+ when GoodData::Segment
235
+ from_project.master_project
236
+ else
237
+ client.projects(from_project)
238
+ end
239
+
240
+ to_project = case to_project
241
+ when GoodData::Client
242
+ to_project.project
243
+ when GoodData::Segment
244
+ to_project.master_project
245
+ else
246
+ client.projects(to_project)
247
+ end
248
+ transfer_processes(from_project, to_project)
249
+ transfer_schedules(from_project, to_project)
250
+ end
251
+
252
+ # @param from_project The source project
253
+ # @param to_project The target project
254
+ # @param options Optional parameters
255
+ # @option ads_output_stage_uri Uri of the source output stage. It must be in the same domain as the target project.
256
+ def transfer_processes(from_project, to_project, options = {})
257
+ options = GoodData::Helpers.symbolize_keys(options)
258
+ to_project_processes = to_project.processes
259
+ result = from_project.processes.uniq(&:name).map do |process|
260
+ fail "The process name #{process.name} must be unique in transfered project #{to_project}" if to_project_processes.count { |p| p.name == process.name } > 1
261
+ next if process.type == :dataload
262
+ to_process = to_project_processes.find { |p| p.name == process.name }
263
+
264
+ to_process = if process.path
265
+ to_process.delete if to_process
266
+ GoodData::Process.deploy_from_appstore(process.path, name: process.name, client: to_project.client, project: to_project)
267
+ else
268
+ Dir.mktmpdir('etl_transfer') do |dir|
269
+ dir = Pathname(dir)
270
+ filename = dir + 'process.zip'
271
+ File.open(filename, 'w') do |f|
272
+ f << process.download
273
+ end
274
+
275
+ if to_process
276
+ to_process.deploy(filename, type: process.type, name: process.name)
277
+ else
278
+ to_project.deploy_process(filename, type: process.type, name: process.name)
279
+ end
280
+ end
281
+ end
282
+
283
+ {
284
+ from: from_project.pid,
285
+ to: to_project.pid,
286
+ name: process.name,
287
+ status: to_process ? 'successful' : 'failed'
288
+ }
289
+ end
290
+
291
+ transfer_output_stage(from_project, to_project, options)
292
+ result << {
293
+ from: from_project.pid,
294
+ to: to_project.pid,
295
+ name: 'Automated Data Distribution',
296
+ status: 'successful'
297
+ }
298
+
299
+ res = (from_project.processes + to_project.processes).map { |p| [p, p.name, p.type] }
300
+ res.group_by { |x| [x[1], x[2]] }
301
+ .select { |_, procs| procs.length == 1 && procs[2] != :dataload }
302
+ .flat_map { |_, procs| procs.select { |p| p[0].project.pid == to_project.pid }.map { |p| p[0] } }
303
+ .peach(&:delete)
304
+
305
+ result.compact
306
+ end
307
+
308
+ def transfer_user_groups(from_project, to_project)
309
+ from_project.user_groups.map do |ug|
310
+ # migrate groups
311
+ new_group = to_project.user_groups.select { |group| group.name == ug.name }.first
312
+ new_group_status = new_group ? 'modified' : 'created'
313
+ new_group ||= UserGroup.create(:name => ug.name, :description => ug.description, :project => to_project)
314
+ new_group.project = to_project
315
+ new_group.description = ug.description
316
+ new_group.save
317
+ # migrate dashboard "grantees"
318
+ dashboards = from_project.dashboards
319
+ dashboards.each do |dashboard|
320
+ new_dashboard = to_project.dashboards.select { |dash| dash.title == dashboard.title }.first
321
+ next unless new_dashboard
322
+ grantee = dashboard.grantees['granteeURIs']['items'].select { |item| item['aclEntryURI']['grantee'].split('/').last == ug.links['self'].split('/').last }.first
323
+ next unless grantee
324
+ permission = grantee['aclEntryURI']['permission']
325
+ new_dashboard.grant(:member => new_group, :permission => permission)
326
+ end
327
+
328
+ {
329
+ from: from_project.pid,
330
+ to: to_project.pid,
331
+ user_group: new_group.name,
332
+ status: new_group_status
333
+ }
334
+ end
335
+ end
336
+
337
+ # Clones project along with etl and schedules.
338
+ #
339
+ # @param client [GoodData::Rest::Client] GoodData client to be used for connection
340
+ # @param from_project [GoodData::Project | GoodData::Segment | GoodData:Client | String]
341
+ # Object to be cloned from. Can be either segment in which case we take
342
+ # the master, client in which case we take its project, string in which
343
+ # case we treat is as an project object or directly project.
344
+ def transfer_schedules(from_project, to_project)
345
+ to_project_processes = to_project.processes.sort_by(&:name)
346
+ from_project_processes = from_project.processes.sort_by(&:name)
347
+
348
+ GoodData.logger.debug("Processes in from project #{from_project.pid}: #{from_project_processes.map(&:name).join(', ')}")
349
+ GoodData.logger.debug("Processes in to project #{to_project.pid}: #{to_project_processes.map(&:name).join(', ')}")
350
+
351
+ cache = to_project_processes
352
+ .zip(from_project_processes)
353
+ .flat_map do |remote, local|
354
+ local.schedules.map do |schedule|
355
+ [remote, local, schedule]
356
+ end
357
+ end
358
+
359
+ remote_schedules = to_project.schedules
360
+ remote_stuff = remote_schedules.map do |s|
361
+ v = s.to_hash
362
+ after_schedule = remote_schedules.find { |s2| s.trigger_id == s2.obj_id }
363
+ v[:after] = s.trigger_id && after_schedule && after_schedule.name
364
+ v[:remote_schedule] = s
365
+ v[:params] = v[:params].except("EXECUTABLE", "PROCESS_ID")
366
+ v.compact
367
+ end
368
+
369
+ local_schedules = from_project.schedules
370
+ local_stuff = local_schedules.map do |s|
371
+ v = s.to_hash
372
+ after_schedule = local_schedules.find { |s2| s.trigger_id == s2.obj_id }
373
+ v[:after] = s.trigger_id && after_schedule && after_schedule.name
374
+ v[:remote_schedule] = s
375
+ v[:params] = v[:params].except("EXECUTABLE", "PROCESS_ID")
376
+ v.compact
377
+ end
378
+
379
+ diff = GoodData::Helpers.diff(remote_stuff, local_stuff, key: :name, fields: [:name, :cron, :after, :params, :hidden_params, :reschedule, :state])
380
+ stack = diff[:added].map do |x|
381
+ [:added, x]
382
+ end
383
+
384
+ stack += diff[:changed].map do |x|
385
+ [:changed, x]
386
+ end
387
+
388
+ schedule_cache = remote_schedules.reduce({}) do |a, e|
389
+ a[e.name] = e
390
+ a
391
+ end
392
+
393
+ results = []
394
+ loop do # rubocop:disable Metrics/BlockLength
395
+ break if stack.empty?
396
+ state, changed_schedule = stack.shift
397
+ if state == :added
398
+ schedule_spec = changed_schedule
399
+ if schedule_spec[:after] && !schedule_cache[schedule_spec[:after]]
400
+ stack << [state, schedule_spec]
401
+ next
402
+ end
403
+ remote_process, process_spec = cache.find do |_remote, local, schedule|
404
+ (schedule_spec[:process_id] == local.process_id) && (schedule.name == schedule_spec[:name])
405
+ end
406
+
407
+ GoodData.logger.info("Creating schedule #{schedule_spec[:name]} for process #{remote_process.name}")
408
+
409
+ executable = nil
410
+ if process_spec.type != :dataload
411
+ executable = schedule_spec[:executable] || (process_spec.type == :ruby ? 'main.rb' : 'main.grf')
412
+ end
413
+ params = schedule_parameters(schedule_spec)
414
+ created_schedule = remote_process.create_schedule(schedule_spec[:cron] || schedule_cache[schedule_spec[:after]], executable, params)
415
+ schedule_cache[created_schedule.name] = created_schedule
416
+
417
+ results << {
418
+ state: :added,
419
+ process: remote_process,
420
+ schedule: created_schedule
421
+ }
422
+ else
423
+ schedule_spec = changed_schedule[:new_obj]
424
+ if schedule_spec[:after] && !schedule_cache[schedule_spec[:after]]
425
+ stack << [state, schedule_spec]
426
+ next
427
+ end
428
+
429
+ remote_process, process_spec = cache.find do |i|
430
+ i[2].name == schedule_spec[:name]
431
+ end
432
+
433
+ schedule = changed_schedule[:old_obj][:remote_schedule]
434
+
435
+ GoodData.logger.info("Updating schedule #{schedule_spec[:name]} for process #{remote_process.name}")
436
+
437
+ schedule.params = (schedule_spec[:params] || {})
438
+ schedule.cron = schedule_spec[:cron] if schedule_spec[:cron]
439
+ schedule.after = schedule_cache[schedule_spec[:after]] if schedule_spec[:after]
440
+ schedule.hidden_params = schedule_spec[:hidden_params] || {}
441
+ if process_spec.type != :dataload
442
+ schedule.executable = schedule_spec[:executable] || (process_spec.type == :ruby ? 'main.rb' : 'main.grf')
443
+ end
444
+
445
+ schedule.reschedule = schedule_spec[:reschedule]
446
+ schedule.name = schedule_spec[:name]
447
+ schedule.state = schedule_spec[:state]
448
+ schedule.save
449
+ schedule_cache[schedule.name] = schedule
450
+
451
+ results << {
452
+ state: :changed,
453
+ process: remote_process,
454
+ schedule: schedule
455
+ }
456
+ end
457
+ end
458
+
459
+ diff[:removed].each do |removed_schedule|
460
+ GoodData.logger.info("Removing schedule #{removed_schedule[:name]}")
461
+
462
+ removed_schedule[:remote_schedule].delete
463
+
464
+ results << {
465
+ state: :removed,
466
+ process: removed_schedule.process,
467
+ schedule: removed_schedule
468
+ }
469
+ end
470
+
471
+ results
472
+ end
473
+
474
+ def transfer_tagged_stuff(from_project, to_project, tag)
475
+ puts "Transferring tagged stuff - #{tag}"
476
+
477
+ objects = from_project.find_by_tag(tag)
478
+
479
+ if objects.any?
480
+ puts JSON.pretty_generate(objects)
481
+ from_project.partial_md_export(objects, project: to_project)
482
+ else
483
+ GoodData.logger.info('No tagged objects to transfer')
484
+ end
485
+ end
486
+
487
+ def transfer_color_palette(from_project, to_project)
488
+ colors = from_project.current_color_palette
489
+ to_project.create_custom_color_palette(colors) unless colors.empty?
490
+ end
491
+
492
+ private
493
+
494
+ def schedule_parameters(schedule_spec)
495
+ {
496
+ params: schedule_spec[:params],
497
+ hidden_params: schedule_spec[:hidden_params],
498
+ name: schedule_spec[:name],
499
+ reschedule: schedule_spec[:reschedule],
500
+ state: schedule_spec[:state]
501
+ }
502
+ end
503
+ end
504
+
505
+ def add_dashboard(dashboard)
506
+ GoodData::Dashboard.create(dashboard, :client => client, :project => self)
507
+ end
508
+
509
+ alias_method :create_dashboard, :add_dashboard
510
+
511
+ def add_user_group(data)
512
+ g = GoodData::UserGroup.create(data.merge(project: self))
513
+
514
+ begin
515
+ g.save
516
+ rescue RestClient::Conflict
517
+ user_groups(data[:name])
518
+ end
519
+ end
520
+
521
+ alias_method :create_group, :add_user_group
522
+
523
+ # Creates a metric in a project
524
+ #
525
+ # @param [options] Optional report options
526
+ # @return [GoodData::Report] Instance of new report
527
+ def add_metric(metric, options = {})
528
+ default = { client: client, project: self }
529
+ if metric.is_a?(String)
530
+ GoodData::Metric.xcreate(metric, options.merge(default))
531
+ else
532
+ GoodData::Metric.xcreate(options[:expression], metric.merge(options.merge(default)))
533
+ end
534
+ end
535
+
536
+ alias_method :create_metric, :add_metric
537
+
538
+ alias_method :add_measure, :add_metric
539
+ alias_method :create_measure, :add_metric
540
+
541
+ # Creates new instance of report in context of project
542
+ #
543
+ # @param [options] Optional report options
544
+ # @return [GoodData::Report] Instance of new report
545
+ def add_report(options = {})
546
+ report = GoodData::Report.create(options.merge(client: client, project: self))
547
+ report.save
548
+ end
549
+
550
+ alias_method :create_report, :add_report
551
+
552
+ # Creates new instance of report definition in context of project
553
+ # This report definition can be used for creating of GoodData::Report
554
+ #
555
+ # @param [json] Raw report definition json
556
+ # @return [GoodData::ReportDefinition] Instance of new report definition
557
+ def add_report_definition(json)
558
+ rd = GoodData::ReportDefinition.new(json)
559
+ rd.client = client
560
+ rd.project = self
561
+ rd.save
562
+ end
563
+
564
+ alias_method :create_report_definition, :add_report_definition
565
+
566
+ # Returns an indication whether current user is admin in this project
567
+ #
568
+ # @return [Boolean] True if user has admin role in the project, false otherwise.
569
+ def am_i_admin?
570
+ user_has_role?(client.user, 'admin')
571
+ end
572
+
573
+ # Helper for getting attributes of a project
574
+ #
575
+ # @param [String | Number | Object] Anything that you can pass to GoodData::Attribute[id]
576
+ # @return [GoodData::Attribute | Array<GoodData::Attribute>] fact instance or list
577
+ def attributes(id = :all)
578
+ GoodData::Attribute[id, project: self, client: client]
579
+ end
580
+
581
+ def computed_attributes(id = :all)
582
+ attrs = attributes(id)
583
+ if attrs.is_a?(GoodData::Attribute)
584
+ attrs.computed_attribute? ? attrs : nil
585
+ else
586
+ attrs.select(&:computed_attribute?)
587
+ end
588
+ end
589
+
590
+ def attribute_by_identifier(identifier)
591
+ GoodData::Attribute.find_first_by_identifier(identifier, project: self, client: client)
592
+ end
593
+
594
+ def attributes_by_identifier(identifier)
595
+ GoodData::Attribute.find_by_identifier(identifier, project: self, client: client)
596
+ end
597
+
598
+ def attribute_by_title(title)
599
+ GoodData::Attribute.find_first_by_title(title, project: self, client: client)
600
+ end
601
+
602
+ def attributes_by_title(title)
603
+ GoodData::Attribute.find_by_title(title, project: self, client: client)
604
+ end
605
+
606
+ # Gets project blueprint from the server
607
+ #
608
+ # @return [GoodData::ProjectRole] Project role if found
609
+ def blueprint(options = {})
610
+ options = { include_ca: true }.merge(options)
611
+ result = client.get("/gdc/projects/#{pid}/model/view", params: { includeDeprecated: true, includeGrain: true, includeCA: options[:include_ca] })
612
+ polling_url = result['asyncTask']['link']['poll']
613
+ model = client.poll_on_code(polling_url, options)
614
+ bp = GoodData::Model::FromWire.from_wire(model, options)
615
+ bp.title = title
616
+ bp
617
+ end
618
+
619
+ # Returns web interface URI of project
620
+ #
621
+ # @return [String] Project URL
622
+ def browser_uri(options = {})
623
+ grey = options[:grey]
624
+ server = client.connection.server_url
625
+ if grey
626
+ "#{server}#{uri}"
627
+ else
628
+ "#{server}/#s=#{uri}"
629
+ end
630
+ end
631
+
632
+ # Clones project
633
+ #
634
+ # @param options [Hash] Export options
635
+ # @option options [Boolean] :data Clone project with data
636
+ # @option options [Boolean] :users Clone project with users
637
+ # @option options [Boolean] :exclude_schedules Specifies whether to include scheduled emails
638
+ # @return [GoodData::Project] Newly created project
639
+ def clone(options = {})
640
+ a_title = options[:title] || "Clone of #{title}"
641
+
642
+ begin
643
+ # Create the project first so we know that it is passing.
644
+ # What most likely is wrong is the token and the export actaully takes majority of the time
645
+ new_project = GoodData::Project.create(options.merge(:title => a_title, :client => client, :driver => content[:driver]))
646
+ export_token = export_clone(options)
647
+ new_project.import_clone(export_token)
648
+ rescue
649
+ new_project.delete if new_project
650
+ raise
651
+ end
652
+ end
653
+
654
+ # Gives you list of datasets. These are not blueprint datasets but model datasets coming from meta
655
+ # data server.
656
+ #
657
+ # @param id [Symbol | String | GoodData::MdObject] Export options
658
+ # @return [Array<GoodData::Dataset> | GoodData::Dataset] Dataset or list of datasets in the project
659
+ def datasets(id = :all)
660
+ GoodData::Dataset[id, project: self, client: client]
661
+ end
662
+
663
+ def dimensions(id = :all)
664
+ GoodData::Dimension[id, client: client, project: self]
665
+ end
666
+
667
+ # Export a clone from a project to be later imported.
668
+ # If you do not want to do anything special and you do not need fine grained
669
+ # controle use clone method which does all the heavy lifting for you.
670
+ #
671
+ # @param options [Hash] Export options
672
+ # @option options [Boolean] :data Clone project with data
673
+ # @option options [Boolean] :users Clone project with users
674
+ # @option options [String] :authorized_users Comma separated logins of authorized users. Users that can use the export
675
+ # @option options [Boolean] :exclude_schedules Specifies whether to include scheduled notifications in the export
676
+ # @option options [Boolean] :cross_data_center_export Specifies whether export can be used in any data center
677
+ # @return [String] token of the export
678
+ def export_clone(options = {})
679
+ with_data = options[:data].nil? ? true : options[:data]
680
+ with_users = options[:users].nil? ? false : options[:users]
681
+
682
+ export = {
683
+ :exportProject => {
684
+ :exportUsers => with_users ? 1 : 0,
685
+ :exportData => with_data ? 1 : 0
686
+ }
687
+ }
688
+ export[:exportProject][:authorizedUsers] = options[:authorized_users] if options[:authorized_users]
689
+ if options[:exclude_schedules]
690
+ exclude_notifications = options[:exclude_schedules] ? 1 : 0
691
+ export[:exportProject][:excludeSchedules] = exclude_notifications
692
+ end
693
+ if options[:cross_data_center_export]
694
+ cross_data_center = options[:cross_data_center_export] ? 1 : 0
695
+ export[:exportProject][:crossDataCenterExport] = cross_data_center
696
+ end
697
+
698
+ result = client.post("/gdc/md/#{obj_id}/maintenance/export", export)
699
+ status_url = result['exportArtifact']['status']['uri']
700
+ polling_result = client.poll_on_response(status_url) do |body|
701
+ body['taskState']['status'] == 'RUNNING'
702
+ end
703
+
704
+ ensure_clone_task_ok(polling_result, GoodData::ExportCloneError)
705
+ result['exportArtifact']['token']
706
+ end
707
+
708
+ def folders(id = :all)
709
+ GoodData::Folder[id, project: self, client: client]
710
+ end
711
+
712
+ def user_groups(id = :all, options = {})
713
+ GoodData::UserGroup[id, options.merge(project: self)]
714
+ end
715
+
716
+ # Imports a clone into current project. The project has to be freshly
717
+ # created.
718
+ #
719
+ # @param export_token [String] Export token of the package to be imported
720
+ # @return [Project] current project
721
+ def import_clone(export_token, options = {})
722
+ import = {
723
+ :importProject => {
724
+ :token => export_token
725
+ }
726
+ }
727
+
728
+ result = client.post("/gdc/md/#{obj_id}/maintenance/import", import)
729
+ status_url = result['uri']
730
+ polling_result = client.poll_on_response(status_url, options) do |body|
731
+ body['taskState']['status'] == 'RUNNING'
732
+ end
733
+ ensure_clone_task_ok(polling_result, GoodData::ImportCloneError)
734
+ self
735
+ end
736
+
737
+ def compute_report(spec = {})
738
+ GoodData::ReportDefinition.execute(spec.merge(client: client, project: self))
739
+ end
740
+
741
+ def compute_metric(expression)
742
+ GoodData::Metric.xexecute(expression, client: client, project: self)
743
+ end
744
+
745
+ alias_method :compute_measure, :compute_metric
746
+
747
+ def create_schedule(process, date, executable, options = {})
748
+ s = GoodData::Schedule.create(process, date, executable, options.merge(client: client, project: self))
749
+ s.save
750
+ end
751
+
752
+ def create_variable(data)
753
+ GoodData::Variable.create(data, client: client, project: self)
754
+ end
755
+
756
+ # Helper for getting dashboards of a project
757
+ #
758
+ # @param id [String | Number | Object] Anything that you can pass to GoodData::Dashboard[id]
759
+ # @return [GoodData::Dashboard | Array<GoodData::Dashboard>] dashboard instance or list
760
+ def dashboards(id = :all)
761
+ GoodData::Dashboard[id, project: self, client: client]
762
+ end
763
+
764
+ def data_permissions(id = :all)
765
+ GoodData::MandatoryUserFilter[id, client: client, project: self]
766
+ end
767
+ alias_method :user_filters, :data_permissions
768
+
769
+ # Deletes project
770
+ def delete
771
+ fail "Project '#{title}' with id #{uri} is already deleted" if deleted?
772
+ client.delete(uri)
773
+ end
774
+
775
+ # Returns true if project is in deleted state
776
+ #
777
+ # @return [Boolean] Returns true if object deleted. False otherwise.
778
+ def deleted?
779
+ state == :deleted
780
+ end
781
+
782
+ # Helper for getting rid of all data in the project
783
+ #
784
+ # @option options [Boolean] :force has to be added otherwise the operation is not performed
785
+ # @return [Array] Result of executing MAQLs
786
+ def delete_all_data(options = {})
787
+ return false unless options[:force]
788
+ begin
789
+ datasets.reject(&:date_dimension?).pmap(&:delete_data)
790
+ rescue MaqlExecutionError => e
791
+ # This is here so that we do not throw out exceptions on synchornizing date dimensions
792
+ # Currently there is no reliable way how to tell it is a date dimension
793
+ fail e unless GoodData::Helpers.interpolate_error_messages(e.data['wTaskStatus']['messages']) == ["Internal error [handle_exception, hide_internal]."]
794
+ end
795
+ end
796
+
797
+ # Deletes dashboards for project
798
+ def delete_dashboards
799
+ Dashboard.all.map { |data| Dashboard[data['link']] }.each(&:delete)
800
+ end
801
+
802
+ def deploy_process(path, options = {})
803
+ GoodData::Process.deploy(path, options.merge(client: client, project: self))
804
+ end
805
+
806
+ # Executes DML expression. See (https://developer.gooddata.com/article/deleting-records-from-datasets)
807
+ # for some examples and explanations
808
+ #
809
+ # @param dml [String] DML expression
810
+ # @return [Hash] Result of executing DML
811
+ def execute_dml(dml, options = {})
812
+ uri = "/gdc/md/#{pid}/dml/manage"
813
+ result = client.post(uri, manage: { maql: dml })
814
+ polling_uri = result['uri']
815
+
816
+ client.poll_on_response(polling_uri, options) do |body|
817
+ body && body['taskState'] && body['taskState']['status'] == 'WAIT'
818
+ end
819
+ end
820
+
821
+ # Executes MAQL expression and waits for it to be finished.
822
+ #
823
+ # @param maql [String] MAQL expression
824
+ # @return [Hash] Result of executing MAQL
825
+ def execute_maql(maql, options = {})
826
+ ldm_links = client.get(md[GoodData::Model::LDM_CTG])
827
+ ldm_uri = Links.new(ldm_links)[GoodData::Model::LDM_MANAGE_CTG]
828
+ response = client.post(ldm_uri, manage: { maql: maql })
829
+ polling_uri = response['entries'].first['link']
830
+
831
+ result = client.poll_on_response(polling_uri, options) do |body|
832
+ body && body['wTaskStatus'] && body['wTaskStatus']['status'] == 'RUNNING'
833
+ end
834
+ if result['wTaskStatus']['status'] == 'ERROR'
835
+ fail MaqlExecutionError.new("Executionof MAQL '#{maql}' failed in project '#{pid}'", result)
836
+ end
837
+ result
838
+ end
839
+
840
+ # Helper for getting facts of a project
841
+ #
842
+ # @param [String | Number | Object] Anything that you can pass to GoodData::Fact[id]
843
+ # @return [GoodData::Fact | Array<GoodData::Fact>] fact instance or list
844
+ def facts(id = :all)
845
+ GoodData::Fact[id, project: self, client: client]
846
+ end
847
+
848
+ def fact_by_title(title)
849
+ GoodData::Fact.find_first_by_title(title, project: self, client: client)
850
+ end
851
+
852
+ def facts_by_title(title)
853
+ GoodData::Fact.find_by_title(title, project: self, client: client)
854
+ end
855
+
856
+ def find_attribute_element_value(uri)
857
+ GoodData::Attribute.find_element_value(uri, client: client, project: self)
858
+ end
859
+
860
+ # Get WebDav directory for project data
861
+ # @return [String]
862
+ def project_webdav_path
863
+ client.project_webdav_path(:project => self)
864
+ end
865
+
866
+ # Gets project role by its identifier
867
+ #
868
+ # @param [String] role_name Title of role to look for
869
+ # @return [GoodData::ProjectRole] Project role if found
870
+ def get_role_by_identifier(role_name, role_list = roles)
871
+ role_name = role_name.downcase.gsub(/role$/, '')
872
+ role_list.each do |role|
873
+ tmp_role_name = role.identifier.downcase.gsub(/role$/, '')
874
+ return role if tmp_role_name == role_name
875
+ end
876
+ nil
877
+ end
878
+
879
+ # Gets project role byt its summary
880
+ #
881
+ # @param [String] role_summary Summary of role to look for
882
+ # @return [GoodData::ProjectRole] Project role if found
883
+ def get_role_by_summary(role_summary, role_list = roles)
884
+ role_list.each do |role|
885
+ return role if role.summary.downcase == role_summary.downcase
886
+ end
887
+ nil
888
+ end
889
+
890
+ # Gets project role by its name
891
+ #
892
+ # @param [String] role_title Title of role to look for
893
+ # @return [GoodData::ProjectRole] Project role if found
894
+ def get_role_by_title(role_title, role_list = roles)
895
+ role_list.each do |role|
896
+ return role if role.title.downcase == role_title.downcase
897
+ end
898
+ nil
899
+ end
900
+
901
+ # Gets project role
902
+ #
903
+ # @param [String] role_title Title of role to look for
904
+ # @return [GoodData::ProjectRole] Project role if found
905
+ def get_role(role_name, role_list = roles)
906
+ return role_name if role_name.is_a? GoodData::ProjectRole
907
+
908
+ role_name.downcase!
909
+ role_list.each do |role|
910
+ return role if role.uri == role_name ||
911
+ role.identifier.downcase == role_name ||
912
+ role.identifier.downcase.gsub(/role$/, '') == role_name ||
913
+ role.title.downcase == role_name ||
914
+ role.summary.downcase == role_name
915
+ end
916
+ nil
917
+ end
918
+
919
+ # Gets user by its login or uri in various shapes
920
+ # It does not find by other information because that is not unique. If you want to search by name or email please
921
+ # use fuzzy_get_user.
922
+ #
923
+ # @param [String] name Name to look for
924
+ # @param [Array<GoodData::User>]user_list Optional cached list of users used for look-ups
925
+ # @return [GoodDta::Membership] User
926
+ def get_user(slug, user_list = users)
927
+ search_crit = if slug.respond_to?(:login)
928
+ slug.login || slug.uri
929
+ elsif slug.is_a?(Hash)
930
+ slug[:login] || slug[:uri]
931
+ else
932
+ slug
933
+ end
934
+ return nil unless search_crit
935
+ user_list.find do |user|
936
+ user.uri == search_crit.downcase ||
937
+ user.login.downcase == search_crit.downcase
938
+ end
939
+ end
940
+
941
+ def upload_file(file, options = {})
942
+ GoodData.upload_to_project_webdav(file, options.merge(project: self))
943
+ end
944
+
945
+ def download_file(file, where)
946
+ GoodData.download_from_project_webdav(file, where, project: self)
947
+ end
948
+
949
+ def driver
950
+ content['driver']
951
+ end
952
+
953
+ def environment
954
+ content['environment']
955
+ end
956
+
957
+ def public?
958
+ content['isPublic']
959
+ end
960
+
961
+ def token
962
+ content['authorizationToken']
963
+ end
964
+
965
+ # Gets user by its email, full_name, login or uri
966
+ alias_method :member, :get_user
967
+
968
+ def find_by_tag(tags)
969
+ tags = tags.split(',').map(&:strip) unless tags.is_a?(Array)
970
+
971
+ objects = tags.map do |tag|
972
+ url = "/gdc/md/#{pid}/tags/#{tag}"
973
+ res = client.get(url)
974
+
975
+ ((res || {})['entries'] || []).map do |entry|
976
+ entry['link']
977
+ end
978
+ end
979
+
980
+ objects.flatten!
981
+
982
+ objects.uniq!
983
+
984
+ objects
985
+ end
986
+
987
+ # Gets user by its email, full_name, login or uri.
988
+ #
989
+ # @param [String] name Name to look for
990
+ # @param [Array<GoodData::User>]user_list Optional cached list of users used for look-ups
991
+ # @return [GoodDta::Membership] User
992
+ def fuzzy_get_user(name, user_list = users)
993
+ return name if name.instance_of?(GoodData::Membership)
994
+ return member(name) if name.instance_of?(GoodData::Profile)
995
+ name = name.is_a?(Hash) ? name[:login] || name[:uri] : name
996
+ return nil unless name
997
+ name.downcase!
998
+ user_list.select do |user|
999
+ user.uri.downcase == name ||
1000
+ user.login.downcase == name ||
1001
+ user.email.downcase == name
1002
+ end
1003
+ nil
1004
+ end
1005
+
1006
+ # Checks whether user has particular role in given proejct
1007
+ #
1008
+ # @param user [GoodData::Profile | GoodData::Membership | String] User in question. Can be passed by login (String), profile or membershi objects
1009
+ # @param role_name [String || GoodData::ProjectRole] Project role cna be given by either string or GoodData::ProjectRole object
1010
+ # @return [Boolean] Tru if user has role_name
1011
+ def user_has_role?(user, role_name)
1012
+ member = get_user(user)
1013
+ role = get_role(role_name)
1014
+ member.roles.include?(role)
1015
+ rescue
1016
+ false
1017
+ end
1018
+
1019
+ # Initializes object instance from raw wire JSON
1020
+ #
1021
+ # @param json Json used for initialization
1022
+ def initialize(json)
1023
+ super
1024
+ @json = json
1025
+ @log_formatter = GoodData::ProjectLogFormatter.new(self)
1026
+ end
1027
+
1028
+ # Invites new user to project
1029
+ #
1030
+ # @param email [String] User to be invited
1031
+ # @param role [String] Role URL or Role ID to be used
1032
+ # @param msg [String] Optional invite message
1033
+ #
1034
+ # TODO: Return invite object
1035
+ def invite(email, role, msg = DEFAULT_INVITE_MESSAGE)
1036
+ puts "Inviting #{email}, role: #{role}"
1037
+
1038
+ role_url = nil
1039
+ if role.index('/gdc/').nil?
1040
+ tmp = get_role(role)
1041
+ role_url = tmp.uri if tmp
1042
+ else
1043
+ role_url = role if role_url.nil?
1044
+ end
1045
+
1046
+ data = {
1047
+ :invitations => [
1048
+ {
1049
+ :invitation => {
1050
+ :content => {
1051
+ :email => email,
1052
+ :role => role_url,
1053
+ :action => {
1054
+ :setMessage => msg
1055
+ }
1056
+ }
1057
+ }
1058
+ }
1059
+ ]
1060
+ }
1061
+
1062
+ url = "/gdc/projects/#{pid}/invitations"
1063
+ client.post(url, data)
1064
+ end
1065
+
1066
+ # Returns invitations to project
1067
+ #
1068
+ # @return [Array<GoodData::Invitation>] List of invitations
1069
+ def invitations
1070
+ invitations = client.get @json['project']['links']['invitations']
1071
+ invitations['invitations'].pmap do |invitation|
1072
+ client.create GoodData::Invitation, invitation
1073
+ end
1074
+ end
1075
+
1076
+ # Returns project related links
1077
+ #
1078
+ # @return [Hash] Project related links
1079
+ def links
1080
+ data['links']
1081
+ end
1082
+
1083
+ # Helper for getting labels of a project
1084
+ #
1085
+ # @param [String | Number | Object] Anything that you can pass to
1086
+ # GoodData::Label[id] + it supports :all as welll
1087
+ # @return [GoodData::Fact | Array<GoodData::Fact>] fact instance or list
1088
+ def labels(id = :all, opts = {})
1089
+ if id == :all
1090
+ attributes.pmapcat(&:labels).uniq
1091
+ else
1092
+ GoodData::Label[id, opts.merge(project: self, client: client)]
1093
+ end
1094
+ end
1095
+
1096
+ def md
1097
+ @md ||= client.create(Links, client.get(data['links']['metadata']))
1098
+ end
1099
+
1100
+ # Get data from project specific metadata storage
1101
+ #
1102
+ # @param [Symbol | String] :all or nothing for all keys or a string for value of specific key
1103
+ # @return [Hash] key Hash of stored data
1104
+ def metadata(key = :all)
1105
+ GoodData::ProjectMetadata[key, client: client, project: self]
1106
+ end
1107
+
1108
+ # Set data for specific key in project specific metadata storage
1109
+ #
1110
+ # @param [String] key key of the value to be stored
1111
+ # @return [String] val value to be stored
1112
+ def set_metadata(key, val)
1113
+ GoodData::ProjectMetadata.[]=(key, { client: client, project: self }, val)
1114
+ end
1115
+
1116
+ # Helper for getting metrics of a project
1117
+ #
1118
+ # @return [Array<GoodData::Metric>] matric instance or list
1119
+ def metrics(id = :all, opts = { :full => true })
1120
+ GoodData::Metric[id, opts.merge(project: self, client: client)]
1121
+ end
1122
+
1123
+ alias_method :measures, :metrics
1124
+
1125
+ def metric_by_title(title)
1126
+ GoodData::Metric.find_first_by_title(title, project: self, client: client)
1127
+ end
1128
+
1129
+ alias_method :measure_by_title, :metric_by_title
1130
+
1131
+ def metrics_by_title(title)
1132
+ GoodData::Metric.find_by_title(title, project: self, client: client)
1133
+ end
1134
+
1135
+ alias_method :measures_by_title, :metrics_by_title
1136
+
1137
+ # Checks if the profile is member of project
1138
+ #
1139
+ # @param [GoodData::Profile] profile - Profile to be checked
1140
+ # @param [Array<GoodData::Membership>] list Optional list of members to check against
1141
+ # @return [Boolean] true if is member else false
1142
+ def member?(profile, list = members)
1143
+ !member(profile, list).nil?
1144
+ end
1145
+
1146
+ def members?(profiles, list = members)
1147
+ profiles.map { |p| member?(p, list) }
1148
+ end
1149
+
1150
+ # Gets raw resource ID
1151
+ #
1152
+ # @return [String] Raw resource ID
1153
+ def obj_id
1154
+ uri.split('/').last
1155
+ end
1156
+
1157
+ alias_method :pid, :obj_id
1158
+
1159
+ # Helper for getting objects of a project
1160
+ #
1161
+ # @return [Array<GoodData::MdObject>] object instance or list
1162
+ def objects(id, opts = {})
1163
+ GoodData::MdObject[id, opts.merge(project: self, client: client)]
1164
+ end
1165
+
1166
+ # Transfer objects from one project to another
1167
+ #
1168
+ # @param [Array<GoodData::MdObject | String>, String, GoodData::MdObject] objs Any representation of the object or a list of those
1169
+ # @param [Hash] options The options to migration.
1170
+ # @option options [Number] :time_limit Time in seconds before the blocking call will fail. See GoodData::Rest::Client.poll_on_response for additional details
1171
+ # @option options [Number] :sleep_interval Interval between polls on the status of the migration.
1172
+ # @return [String] Returns token that you can use as input for object_import
1173
+ def objects_export(objs, options = {})
1174
+ fail 'Nothing to migrate. You have to pass list of objects, ids or uris that you would like to migrate' if objs.nil?
1175
+ objs = Array(objs)
1176
+ if objs.empty?
1177
+ GoodData.logger.warn 'Nothing to migrate.'
1178
+ return
1179
+ end
1180
+
1181
+ objs = objs.pmap { |obj| [obj, objects(obj)] }
1182
+ if objs.any? { |_, obj| obj.nil? }
1183
+ object = objs.select { |_, obj| obj.nil? }.map { |o, _| o }.join(', ')
1184
+ error_message = "Exporting objects failed with messages. " \
1185
+ "Object #{object} could not be found."
1186
+ fail ObjectsExportError, error_message
1187
+ end
1188
+ export_payload = {
1189
+ :partialMDExport => {
1190
+ :uris => objs.map { |_, obj| obj.uri },
1191
+ :exportAttributeProperties => '1',
1192
+ :crossDataCenterExport => '1'
1193
+ }
1194
+ }
1195
+ result = client.post("#{md['maintenance']}/partialmdexport", export_payload)
1196
+ polling_url = result['partialMDArtifact']['status']['uri']
1197
+ token = result['partialMDArtifact']['token']
1198
+
1199
+ polling_result = client.poll_on_response(polling_url, options) do |body|
1200
+ body['wTaskStatus'] && body['wTaskStatus']['status'] == 'RUNNING'
1201
+ end
1202
+ if polling_result['wTaskStatus'] && polling_result['wTaskStatus']['status'] == 'ERROR'
1203
+ messages = GoodData::Helpers.interpolate_error_messages(polling_result['wTaskStatus']['messages']).join(' ')
1204
+ fail ObjectsExportError, "Exporting objects failed with messages. #{messages}"
1205
+ end
1206
+ token
1207
+ end
1208
+
1209
+ # 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.
1210
+ #
1211
+ # @param [String] token Migration token ID
1212
+ # @param [Hash] options The options to migration.
1213
+ # @option options [Number] :time_limit Time in seconds before the blocking call will fail. See GoodData::Rest::Client.poll_on_response for additional details
1214
+ # @option options [Number] :sleep_interval Interval between polls on the status of the migration.
1215
+ # @return [Boolean] Returns true if it succeeds or throws exceoption
1216
+ def objects_import(token, options = {})
1217
+ fail 'You need to provide a token for object import' if token.blank?
1218
+
1219
+ import_payload = {
1220
+ :partialMDImport => {
1221
+ :token => token,
1222
+ :overwriteNewer => '1',
1223
+ :updateLDMObjects => '1',
1224
+ :importAttributeProperties => '1'
1225
+ }
1226
+ }
1227
+
1228
+ result = client.post("#{md['maintenance']}/partialmdimport", import_payload)
1229
+ polling_url = result['uri']
1230
+
1231
+ polling_result = client.poll_on_response(polling_url, options) do |body|
1232
+ body['wTaskStatus'] && body['wTaskStatus']['status'] == 'RUNNING'
1233
+ end
1234
+
1235
+ if polling_result['wTaskStatus']['status'] == 'ERROR'
1236
+ messages = GoodData::Helpers.interpolate_error_messages(polling_result['wTaskStatus']['messages']).join(' ')
1237
+ fail ObjectsImportError, "Importing objects failed with messages. #{messages}"
1238
+ end
1239
+ true
1240
+ end
1241
+
1242
+ # Transfer objects from one project to another
1243
+ #
1244
+ # @param [Array<GoodData::MdObject | String>, String, GoodData::MdObject] objects Any representation of the object or a list of those
1245
+ # @param [Hash] options The options to migration.
1246
+ # @option options [GoodData::Project | String | Array<String> | Array<GoodData::Project>] :project Project(s) to migrate to
1247
+ # @option options [Number] :batch_size Number of projects that are migrated at the same time. Default is 10
1248
+ #
1249
+ # @return [Boolean | Array<Hash>] Return either true or throws exception
1250
+ # if you passed only one project. If you provided an array returns list
1251
+ # of hashes signifying sucees or failure. Take note that in case of list
1252
+ # of projects it does not throw exception.
1253
+ def partial_md_export(objects, options = {})
1254
+ projects = options[:project]
1255
+ batch_size = options[:batch_size] || 10
1256
+ token = objects_export(objects)
1257
+ return if token.nil?
1258
+
1259
+ if projects.is_a?(Array)
1260
+ projects.each_slice(batch_size).flat_map do |batch|
1261
+ batch.pmap do |proj|
1262
+ begin
1263
+ target_project = client.projects(proj)
1264
+ target_project.objects_import(token, options)
1265
+ {
1266
+ project: target_project,
1267
+ result: true
1268
+ }
1269
+ rescue RestClient::Exception => e
1270
+ {
1271
+ project: proj,
1272
+ exception: e,
1273
+ result: false,
1274
+ reason: GoodData::Helpers.interpolate_error_message(MultiJson.load(e.response))
1275
+ }
1276
+ rescue GoodData::ObjectsImportError => e
1277
+ {
1278
+ project: target_project,
1279
+ result: false,
1280
+ reason: e.message
1281
+ }
1282
+ end
1283
+ end
1284
+ end
1285
+ else
1286
+ target_project = client.projects(projects)
1287
+ target_project.objects_import(token, options)
1288
+ end
1289
+ end
1290
+
1291
+ alias_method :transfer_objects, :partial_md_export
1292
+
1293
+ # Helper for getting processes of a project
1294
+ #
1295
+ # @param [String | Number | Object] Anything that you can pass to GoodData::Report[id]
1296
+ # @return [GoodData::Report | Array<GoodData::Report>] report instance or list
1297
+ def processes(id = :all)
1298
+ GoodData::Process[id, project: self, client: client]
1299
+ end
1300
+
1301
+ # Checks if this object instance is project
1302
+ #
1303
+ # @return [Boolean] Return true for all instances
1304
+ def project?
1305
+ true
1306
+ end
1307
+
1308
+ def info
1309
+ results = blueprint.datasets.pmap do |ds|
1310
+ [ds, ds.count(self)]
1311
+ end
1312
+ puts title
1313
+ puts GoodData::Helpers.underline(title)
1314
+ puts
1315
+ puts "Datasets - #{results.count}"
1316
+ puts
1317
+ results.each do |x|
1318
+ dataset, count = x
1319
+ dataset.title.tap do |t|
1320
+ puts t
1321
+ puts GoodData::Helpers.underline(t)
1322
+ puts "Size - #{count} rows"
1323
+ puts "#{dataset.attributes_and_anchors.count} attributes, #{dataset.facts.count} facts, #{dataset.references.count} references"
1324
+ puts
1325
+ end
1326
+ end
1327
+ nil
1328
+ end
1329
+
1330
+ # Forces project to reload
1331
+ def reload!
1332
+ if saved?
1333
+ response = client.get(uri)
1334
+ @json = response
1335
+ end
1336
+ self
1337
+ end
1338
+
1339
+ # Method used for walking through objects in project and trying to
1340
+ # replace all occurences of some object for another object. This is
1341
+ # typically used as a means for exchanging Date dimensions.
1342
+ #
1343
+ # @param mapping [Array<Array>] Mapping specifying what should be exchanged for what. As mapping should be used output of GoodData::Helpers.prepare_mapping.
1344
+ def replace_from_mapping(mapping, opts = {})
1345
+ default = {
1346
+ :purge => false,
1347
+ :dry_run => false
1348
+ }
1349
+ opts = default.merge(opts)
1350
+ dry_run = opts[:dry_run]
1351
+
1352
+ if opts[:purge]
1353
+ GoodData.logger.info 'Purging old project definitions'
1354
+ reports.peach(&:purge_report_of_unused_definitions!)
1355
+ end
1356
+
1357
+ fail ArgumentError, 'No mapping specified' if mapping.blank?
1358
+ rds = report_definitions
1359
+
1360
+ {
1361
+ # data_permissions: data_permissions,
1362
+ variables: variables,
1363
+ dashboards: dashboards,
1364
+ metrics: metrics,
1365
+ report_definitions: rds
1366
+ }.each do |key, collection|
1367
+ puts "Replacing #{key}"
1368
+ collection.peach do |item|
1369
+ new_item = item.replace(mapping)
1370
+ if new_item.json != item.json
1371
+ if dry_run
1372
+ GoodData.logger.info "Would save #{new_item.uri}. Running in dry run mode"
1373
+ else
1374
+ GoodData.logger.info "Saving #{new_item.uri}"
1375
+ new_item.save
1376
+ end
1377
+ else
1378
+ GoodData.logger.info "Ignore #{item.uri}"
1379
+ end
1380
+ end
1381
+ end
1382
+
1383
+ GoodData.logger.info 'Replacing hidden metrics'
1384
+ local_metrics = mapping.map { |a, _| a }.pmapcat { |a| a.usedby('metric') }.select { |m| m['deprecated'] == '1' }.map { |m| m['link'] }.uniq
1385
+ puts "Found #{local_metrics.count} metrics"
1386
+ local_metrics.pmap { |m| metrics(m) }.peach do |item|
1387
+ new_item = item.replace(mapping)
1388
+ if new_item.json != item.json
1389
+ if dry_run
1390
+ GoodData.logger.info "Would save #{new_item.uri}. Running in dry run mode"
1391
+ else
1392
+ GoodData.logger.info "Saving #{new_item.uri}"
1393
+ new_item.save
1394
+ end
1395
+ else
1396
+ GoodData.logger.info "Ignore #{item.uri}"
1397
+ end
1398
+ end
1399
+
1400
+ GoodData.logger.info 'Replacing dashboard saved views'
1401
+ contexts = mapping.map { |a, _| a }.pmapcat { |a| a.usedby('executionContext') }.map { |a| GoodData::MdObject[a['link'], client: client, project: self] }
1402
+ puts "Found #{contexts.count} dashboard saved views"
1403
+ contexts.peach do |item|
1404
+ new_item = GoodData::MdObject.replace_quoted(item, mapping)
1405
+ if new_item.json != item.json
1406
+ if dry_run
1407
+ GoodData.logger.info "Would save #{new_item.uri}. Running in dry run mode"
1408
+ else
1409
+ GoodData.logger.info "Saving #{new_item.uri}"
1410
+ new_item.save
1411
+ end
1412
+ else
1413
+ GoodData.logger.info "Ignore #{item.uri}"
1414
+ end
1415
+ end
1416
+
1417
+ GoodData.logger.info 'Replacing variable values'
1418
+ variables.each do |var|
1419
+ var.values.peach do |val|
1420
+ val.replace(mapping).save unless dry_run
1421
+ end
1422
+ end
1423
+
1424
+ {
1425
+ visualizations: MdObject.query('visualization', MdObject, client: client, project: self),
1426
+ visualization_widgets: MdObject.query('visualizationWidget', MdObject, client: client, project: self),
1427
+ kpis: MdObject.query('kpi', MdObject, client: client, project: self)
1428
+ }.each do |key, collection|
1429
+ GoodData.logger.info "Replacing #{key}"
1430
+ collection.each do |item|
1431
+ new_item = MdObject.replace_quoted(item, mapping)
1432
+ if new_item.json != item.json
1433
+ if dry_run
1434
+ GoodData.logger.info "Would save #{new_item.uri}. Running in dry run mode"
1435
+ else
1436
+ GoodData.logger.info "Saving #{new_item.uri}"
1437
+ new_item.save
1438
+ end
1439
+ else
1440
+ GoodData.logger.info "Ignore #{item.uri}"
1441
+ end
1442
+ end
1443
+ end
1444
+ nil
1445
+ end
1446
+
1447
+ # Helper for getting reports of a project
1448
+ #
1449
+ # @param [String | Number | Object] Anything that you can pass to GoodData::Report[id]
1450
+ # @return [GoodData::Report | Array<GoodData::Report>] report instance or list
1451
+ def reports(id = :all)
1452
+ GoodData::Report[id, project: self, client: client]
1453
+ end
1454
+
1455
+ # Helper for getting report definitions of a project
1456
+ #
1457
+ # @param [String | Number | Object] Anything that you can pass to GoodData::ReportDefinition[id]
1458
+ # @return [GoodData::ReportDefinition | Array<GoodData::ReportDefinition>] report definition instance or list
1459
+ def report_definitions(id = :all, options = {})
1460
+ GoodData::ReportDefinition[id, options.merge(project: self, client: client)]
1461
+ end
1462
+
1463
+ # Gets the list or project roles
1464
+ #
1465
+ # @return [Array<GoodData::ProjectRole>] List of roles
1466
+ def roles
1467
+ url = "/gdc/projects/#{pid}/roles"
1468
+
1469
+ tmp = client.get(url)
1470
+ tmp['projectRoles']['roles'].pmap do |role_url|
1471
+ json = client.get role_url
1472
+ client.create(GoodData::ProjectRole, json, project: self)
1473
+ end
1474
+ end
1475
+
1476
+ # Saves project
1477
+ def save
1478
+ data_to_send = GoodData::Helpers.deep_dup(raw_data)
1479
+ data_to_send['project']['content'].delete('cluster')
1480
+ data_to_send['project']['content'].delete('isPublic')
1481
+ data_to_send['project']['content'].delete('state')
1482
+ response = if uri
1483
+ client.post(PROJECT_PATH % pid, data_to_send)
1484
+ client.get uri
1485
+ else
1486
+ result = client.post(PROJECTS_PATH, data_to_send)
1487
+ client.get result['uri']
1488
+ end
1489
+ @json = response
1490
+ self
1491
+ end
1492
+
1493
+ # Schedules an email with dashboard or report content
1494
+ def schedule_mail(options = GoodData::ScheduledMail::DEFAULT_OPTS)
1495
+ GoodData::ScheduledMail.create(options.merge(client: client, project: self))
1496
+ end
1497
+
1498
+ def scheduled_mails(options = { :full => false })
1499
+ GoodData::ScheduledMail[:all, options.merge(project: self, client: client)]
1500
+ end
1501
+
1502
+ # @param [String | Number | Object] Anything that you can pass to GoodData::Schedule[id]
1503
+ # @return [GoodData::Schedule | Array<GoodData::Schedule>] schedule instance or list
1504
+ def schedules(id = :all)
1505
+ GoodData::Schedule[id, project: self, client: client]
1506
+ end
1507
+
1508
+ # Gets SLIs data
1509
+ #
1510
+ # @return [GoodData::Metadata] SLI Metadata
1511
+ def slis
1512
+ link = "#{data['links']['metadata']}#{SLIS_PATH}"
1513
+
1514
+ # FIXME: Review what to do with passed extra argument
1515
+ Metadata.new client.get(link)
1516
+ end
1517
+
1518
+ # Gets project state
1519
+ #
1520
+ # @return [String] Project state
1521
+ def state
1522
+ data['content']['state'].downcase.to_sym if data['content'] && data['content']['state']
1523
+ end
1524
+
1525
+ Project.metadata_property_reader :summary, :title
1526
+
1527
+ # Gets project title
1528
+ #
1529
+ # @return [String] Project title
1530
+ def title=(a_title)
1531
+ data['meta']['title'] = a_title if data['meta']
1532
+ end
1533
+
1534
+ # Uploads file to project
1535
+ #
1536
+ # @param file File to be uploaded
1537
+ # @param schema Schema to be used
1538
+ def upload(data, blueprint, dataset_name, options = {})
1539
+ GoodData::Model.upload_data(data, blueprint, dataset_name, options.merge(client: client, project: self))
1540
+ end
1541
+
1542
+ def upload_multiple(data, blueprint, options = {})
1543
+ GoodData::Model.upload_multiple_data(data, blueprint, options.merge(client: client, project: self))
1544
+ end
1545
+
1546
+ def uri
1547
+ data['links']['self'] if data && data['links'] && data['links']['self']
1548
+ end
1549
+
1550
+ # List of users in project
1551
+ #
1552
+ #
1553
+ # @return [Array<GoodData::User>] List of users
1554
+ def users(opts = {})
1555
+ client = client(opts)
1556
+ Enumerator.new do |y|
1557
+ offset = opts[:offset] || 0
1558
+ limit = opts[:limit] || 1_000
1559
+ loop do
1560
+ tmp = client.get("/gdc/projects/#{pid}/users", params: { offset: offset, limit: limit })
1561
+ tmp['users'].each do |user_data|
1562
+ user = client.create(GoodData::Membership, user_data, project: self)
1563
+
1564
+ if opts[:all]
1565
+ y << user
1566
+ elsif opts[:disabled]
1567
+ y << user if user && user.disabled?
1568
+ else
1569
+ y << user if user && user.enabled?
1570
+ end
1571
+ end
1572
+ break if tmp['users'].count < limit
1573
+ offset += limit
1574
+ end
1575
+ end
1576
+ end
1577
+
1578
+ alias_method :members, :users
1579
+
1580
+ def whitelist_users(new_users, users_list, whitelist, mode = :exclude)
1581
+ return [new_users, users_list] unless whitelist
1582
+
1583
+ new_whitelist_proc = proc do |user|
1584
+ whitelist.any? do |wl|
1585
+ if wl.is_a?(Regexp)
1586
+ user[:login] =~ wl
1587
+ else
1588
+ user[:login] && user[:login] == wl
1589
+ end
1590
+ end
1591
+ end
1592
+
1593
+ whitelist_proc = proc do |user|
1594
+ whitelist.any? do |wl|
1595
+ if wl.is_a?(Regexp)
1596
+ user.login =~ wl
1597
+ else
1598
+ user.login && user.login == wl
1599
+ end
1600
+ end
1601
+ end
1602
+
1603
+ if mode == :include
1604
+ [new_users.select(&new_whitelist_proc), users_list.select(&whitelist_proc)]
1605
+ elsif mode == :exclude
1606
+ [new_users.reject(&new_whitelist_proc), users_list.reject(&whitelist_proc)]
1607
+ end
1608
+ end
1609
+
1610
+ # Imports users
1611
+ def import_users(new_users, options = {})
1612
+ role_list = roles
1613
+ users_list = users
1614
+ new_users = new_users.map { |x| ((x.is_a?(Hash) && x[:user] && x[:user].to_hash.merge(role: x[:role])) || x.to_hash).tap { |u| u[:login].downcase! } }
1615
+
1616
+ GoodData.logger.warn("Importing users to project (#{pid})")
1617
+
1618
+ whitelisted_new_users, whitelisted_users = whitelist_users(new_users.map(&:to_hash), users_list, options[:whitelists])
1619
+
1620
+ # First check that if groups are provided we have them set up
1621
+ check_groups(new_users.map(&:to_hash).flat_map { |u| u[:user_group] || [] }.uniq, options)
1622
+
1623
+ # conform the role on list of new users so we can diff them with the users coming from the project
1624
+ diffable_new_with_default_role = whitelisted_new_users.map do |u|
1625
+ u[:role] = Array(u[:role] || u[:roles] || 'readOnlyUser')
1626
+ u
1627
+ end
1628
+
1629
+ intermediate_new = diffable_new_with_default_role.map do |u|
1630
+ u[:role] = u[:role].map do |r|
1631
+ role = get_role(r, role_list)
1632
+ role ? role.uri : r
1633
+ end
1634
+
1635
+ u[:role_title] = u[:role].map do |r|
1636
+ role = get_role(r, role_list)
1637
+ role ? role.title : r
1638
+ end
1639
+
1640
+ if u[:role].all?(&:nil?)
1641
+ u[:type] = :error
1642
+ u[:reason] = 'Invalid role(s) specified'
1643
+ else
1644
+ u[:type] = :ok
1645
+ end
1646
+
1647
+ u[:status] = 'ENABLED'
1648
+ u
1649
+ end
1650
+
1651
+ intermediate_new_by_type = intermediate_new.group_by { |i| i[:type] }
1652
+ diffable_new = intermediate_new_by_type[:ok] || []
1653
+
1654
+ # Diff users. Only login and role is important for the diff
1655
+ diff = GoodData::Helpers.diff(whitelisted_users, diffable_new, key: :login, fields: [:login, :role, :status])
1656
+ diff_results = diff.flat_map do |operation, users|
1657
+ if operation == :changed
1658
+ users.map { |u| u[:new_obj].merge(operation: operation) }
1659
+ else
1660
+ users.map { |u| u.merge(operation: operation) }
1661
+ end
1662
+ end
1663
+ diff_results = diff_results.map do |u|
1664
+ u[:login_uri] = "/gdc/account/profile/" + u[:login]
1665
+ u
1666
+ end
1667
+ return diff_results if options[:dry_run]
1668
+
1669
+ # Create new users
1670
+ results = []
1671
+ GoodData.logger.warn("Creating #{diff[:added].count} users in project (#{pid})")
1672
+ to_create = diff[:added].map { |x| { user: x, role: x[:role] } }
1673
+ created_users_result = create_users(to_create, roles: role_list, project_users: whitelisted_users)
1674
+ @log_formatter.log_created_users(created_users_result, diff[:added])
1675
+ results.concat(created_users_result)
1676
+ send_mail_to_new_users(diff[:added], options[:email_options]) if options[:email_options] && !options[:email_options].empty? && !diff[:added].empty?
1677
+
1678
+ # # Update existing users
1679
+ GoodData.logger.warn("Updating #{diff[:changed].count} users in project (#{pid})")
1680
+ to_update = diff[:changed].map { |x| { user: x[:new_obj], role: x[:new_obj][:role] || x[:new_obj][:roles] } }
1681
+ updated_users_result = set_users_roles(to_update, roles: role_list, project_users: whitelisted_users)
1682
+ @log_formatter.log_updated_users(updated_users_result, diff[:changed], role_list)
1683
+ results.concat(updated_users_result)
1684
+
1685
+ unless options[:do_not_touch_users_that_are_not_mentioned]
1686
+ # Remove old users
1687
+ to_disable = diff[:removed].reject { |user| user[:status] == 'DISABLED' || user[:status] == :disabled }
1688
+ GoodData.logger.warn("Disabling #{to_disable.count} users from project (#{pid})")
1689
+ disabled_users_result = disable_users(to_disable, roles: role_list, project_users: whitelisted_users)
1690
+ @log_formatter.log_disabled_users(disabled_users_result)
1691
+ results.concat(disabled_users_result)
1692
+
1693
+ # Remove old users completely
1694
+ if options[:remove_users_from_project]
1695
+ to_remove = (to_disable + users(disabled: true).to_a).map(&:to_hash).uniq do |user|
1696
+ user[:uri]
1697
+ end
1698
+ GoodData.logger.warn("Removing #{to_remove.count} users from project (#{pid})")
1699
+ removed_users_result = remove_users(to_remove)
1700
+ @log_formatter.log_removed_users(removed_users_result)
1701
+ results.concat(removed_users_result)
1702
+ end
1703
+ end
1704
+
1705
+ # reassign to groups
1706
+ mappings = new_users.map(&:to_hash).flat_map do |user|
1707
+ groups = user[:user_group] || []
1708
+ groups.map { |g| [user[:login], g] }
1709
+ end
1710
+ unless mappings.empty?
1711
+ users_lookup = users.reduce({}) do |a, e|
1712
+ a[e.login] = e
1713
+ a
1714
+ end
1715
+ mappings.group_by { |_, g| g }.each do |g, mapping|
1716
+ remote_users = mapping.map { |user, _| user }.map { |login| users_lookup[login] && users_lookup[login].uri }.reject(&:nil?)
1717
+ next if remote_users.empty?
1718
+ user_groups(g).set_members(remote_users)
1719
+ end
1720
+ mentioned_groups = mappings.map(&:last).uniq
1721
+ groups_to_cleanup = user_groups.reject { |g| mentioned_groups.include?(g.name) }
1722
+ # clean all groups not mentioned with exception of whitelisted users
1723
+ groups_to_cleanup.each do |g|
1724
+ g.set_members(whitelist_users(g.members.map(&:to_hash), [], options[:whitelists], :include).first.map { |x| x[:uri] })
1725
+ end
1726
+ end
1727
+ GoodData::Helpers.join(results, diff_results, [:user], [:login_uri])
1728
+ end
1729
+
1730
+ def disable_users(list, options = {})
1731
+ list = list.map(&:to_hash)
1732
+ url = "#{uri}/users"
1733
+ payloads = list.map do |u|
1734
+ uri, = resolve_roles(u, [], options)
1735
+ generate_user_payload(uri, 'DISABLED')
1736
+ end
1737
+
1738
+ payloads.each_slice(100).mapcat do |payload|
1739
+ result = client.post(url, 'users' => payload)
1740
+ result['projectUsersUpdateResult'].mapcat { |k, v| v.map { |x| { type: k.to_sym, user: x } } }
1741
+ end
1742
+ end
1743
+
1744
+ def remove_users(list)
1745
+ list = list.map(&:to_hash)
1746
+
1747
+ list.pmapcat do |u|
1748
+ u_id = GoodData::Helpers.last_uri_part(u[:uri])
1749
+ url = "#{uri}/users/#{u_id}"
1750
+ begin
1751
+ client.delete(url)
1752
+ [{ type: :successful, operation: :user_deleted_from_project, user: u }]
1753
+ rescue => e
1754
+ [{ type: :failed, message: e.message, user: u }]
1755
+ end
1756
+ end
1757
+ end
1758
+
1759
+ def check_groups(specified_groups, options = {})
1760
+ groups = user_groups.map(&:name)
1761
+ missing_groups = specified_groups - groups
1762
+ if options[:create_non_existing_user_groups]
1763
+ missing_groups.each do |g|
1764
+ create_group(name: g, description: g)
1765
+ end
1766
+ else
1767
+ unless missing_groups.empty?
1768
+ fail 'All groups have to be specified before you try to import ' \
1769
+ 'users. Groups that are currently in project are ' \
1770
+ "#{groups.join(',')} and you asked for #{missing_groups.join(',')}"
1771
+ end
1772
+ end
1773
+ end
1774
+
1775
+ # Update user
1776
+ #
1777
+ # @param user User to be updated
1778
+ # @param desired_roles Roles to be assigned to user
1779
+ # @param role_list Optional cached list of roles used for lookups
1780
+ def set_user_roles(login, desired_roles, options = {})
1781
+ user_uri, roles = resolve_roles(login, desired_roles, options)
1782
+ url = "#{uri}/users"
1783
+ payload = generate_user_payload(user_uri, 'ENABLED', roles)
1784
+ res = client.post(url, payload)
1785
+ failure = GoodData::Helpers.get_path(res, %w(projectUsersUpdateResult failed))
1786
+ fail ArgumentError, "User #{user_uri} could not be aded. #{failure.first['message']}" unless failure.blank?
1787
+ res
1788
+ end
1789
+ alias_method :add_user, :set_user_roles
1790
+
1791
+ # Update list of users
1792
+ #
1793
+ # @param list List of users to be updated
1794
+ # @param role_list Optional list of cached roles to prevent unnecessary server round-trips
1795
+ def set_users_roles(list, options = {})
1796
+ return [] if list.empty?
1797
+ role_list = options[:roles] || roles
1798
+ project_users = options[:project_users] || users
1799
+
1800
+ intermediate_users = list.flat_map do |user_hash|
1801
+ user = user_hash[:user] || user_hash[:login]
1802
+ desired_roles = user_hash[:role] || user_hash[:roles] || 'readOnlyUser'
1803
+ begin
1804
+ login, roles = resolve_roles(user, desired_roles, options.merge(project_users: project_users, roles: role_list))
1805
+ [{ :type => :successful, user: login, roles: roles }]
1806
+ rescue => e
1807
+ [{ :type => :failed, :reason => e.message, user: user, roles: desired_roles }]
1808
+ end
1809
+ end
1810
+
1811
+ # User can fail pre sending to API during resolving roles. We add only users that passed that step.
1812
+ users_by_type = intermediate_users.group_by { |u| u[:type] }
1813
+ users_to_add = users_by_type[:successful] || []
1814
+
1815
+ payloads = users_to_add.map { |u| generate_user_payload(u[:user], 'ENABLED', u[:roles]) }
1816
+ results = payloads.each_slice(100).map do |payload|
1817
+ client.post("#{uri}/users", 'users' => payload)
1818
+ end
1819
+ # this ugly line turns the hash of errors into list of errors with types so we can process them easily
1820
+ typed_results = results.flat_map do |x|
1821
+ x['projectUsersUpdateResult'].flat_map do |k, v|
1822
+ v.map { |v2| v2.is_a?(String) ? { type: k.to_sym, user: v2 } : GoodData::Helpers.symbolize_keys(v2).merge(type: k.to_sym) }
1823
+ end
1824
+ end
1825
+ # we have to concat errors from role resolution and API result
1826
+ typed_results + (users_by_type[:failed] || [])
1827
+ end
1828
+
1829
+ alias_method :add_users, :set_users_roles
1830
+ alias_method :create_users, :set_users_roles
1831
+
1832
+ def add_data_permissions(filters, options = {})
1833
+ GoodData::UserFilterBuilder.execute_mufs(filters, { client: client, project: self }.merge(options))
1834
+ end
1835
+
1836
+ def add_variable_permissions(filters, var, options = {})
1837
+ GoodData::UserFilterBuilder.execute_variables(filters, var, { client: client, project: self }.merge(options))
1838
+ end
1839
+
1840
+ # Run validation on project
1841
+ # Valid settins for validation are (default all):
1842
+ # ldm - Checks the consistency of LDM objects.
1843
+ # pdm Checks LDM to PDM mapping consistency, also checks PDM reference integrity.
1844
+ # metric_filter - Checks metadata for inconsistent metric filters.
1845
+ # invalid_objects - Checks metadata for invalid/corrupted objects.
1846
+ # asyncTask response
1847
+ def validate(filters = %w(ldm pdm metric_filter invalid_objects), options = {})
1848
+ response = client.post "#{md['validate-project']}", 'validateProject' => filters
1849
+ polling_link = response['asyncTask']['link']['poll']
1850
+ client.poll_on_response(polling_link, options) do |body|
1851
+ body['wTaskStatus'] && body['wTaskStatus']['status'] == 'RUNNING'
1852
+ end
1853
+ end
1854
+
1855
+ def variables(id = :all, options = { client: client, project: self })
1856
+ GoodData::Variable[id, options]
1857
+ end
1858
+
1859
+ def update_from_blueprint(blueprint, options = {})
1860
+ GoodData::Model::ProjectCreator.migrate(options.merge(spec: blueprint, token: options[:auth_token], client: client, project: self))
1861
+ end
1862
+
1863
+ def resolve_roles(login, desired_roles, options = {})
1864
+ user = if login.is_a?(String) && login.include?('@')
1865
+ '/gdc/account/profile/' + login
1866
+ elsif login.is_a?(String)
1867
+ login
1868
+ elsif login.is_a?(Hash) && login[:login]
1869
+ '/gdc/account/profile/' + login[:login]
1870
+ elsif login.is_a?(Hash) && login[:uri]
1871
+ login[:uri]
1872
+ elsif login.respond_to?(:uri) && login.uri
1873
+ login.uri
1874
+ elsif login.respond_to?(:login) && login.login
1875
+ '/gdc/account/profile/' + login.login
1876
+ else
1877
+ fail "Unsupported user specification #{login}"
1878
+ end
1879
+
1880
+ role_list = options[:roles] || roles
1881
+ desired_roles = Array(desired_roles)
1882
+ roles = desired_roles.map do |role_name|
1883
+ role = get_role(role_name, role_list)
1884
+ fail ArgumentError, "Invalid role '#{role_name}' specified for user '#{GoodData::Helpers.last_uri_part(user)}'" if role.nil?
1885
+ role.uri
1886
+ end
1887
+ [user, roles]
1888
+ end
1889
+
1890
+ def add
1891
+ @add ||= GoodData::AutomatedDataDistribution.new(self)
1892
+ @add
1893
+ end
1894
+
1895
+ def transfer_etl(target)
1896
+ GoodData::Project.transfer_etl(client, self, target)
1897
+ end
1898
+
1899
+ def transfer_processes(target)
1900
+ GoodData::Project.transfer_processes(self, target)
1901
+ end
1902
+
1903
+ def transfer_schedules(target)
1904
+ GoodData::Project.transfer_schedules(self, target)
1905
+ end
1906
+
1907
+ def transfer_tagged_stuff(target, tag)
1908
+ GoodData::Project.transfer_tagged_stuff(self, target, tag)
1909
+ end
1910
+
1911
+ def create_output_stage(ads, opts = {})
1912
+ add.create_output_stage(ads, opts)
1913
+ end
1914
+
1915
+ def transfer_color_palette(target)
1916
+ GoodData::Project.transfer_color_palette(self, target)
1917
+ end
1918
+
1919
+ def current_color_palette
1920
+ GoodData::StyleSetting.current(client: client, project: self)
1921
+ end
1922
+
1923
+ def create_custom_color_palette(colors)
1924
+ GoodData::StyleSetting.create(colors, client: client, project: self)
1925
+ end
1926
+
1927
+ def reset_color_palette
1928
+ GoodData::StyleSetting.reset(client: client, project: self)
1929
+ end
1930
+
1931
+ # get maql diff from another project or blueprint to current project
1932
+ #
1933
+ # @param options [Hash] options
1934
+ # @option options [GoodData::Project] :project source project
1935
+ # @option options [GoodData::Model::ProjectBlueprint] :blueprint blueprint of source project
1936
+ # @option options [Array] :params additional parameters for diff api
1937
+ # @return [Hash] project model diff
1938
+ def maql_diff(options = {})
1939
+ fail "No :project or :blueprint specified" unless options[:blueprint] || options[:project]
1940
+ bp = options[:blueprint] || options[:project].blueprint
1941
+ uri = "/gdc/projects/#{pid}/model/diff"
1942
+ params = Hash[(options[:params] || []).map { |i| [i, true] }]
1943
+ result = client.post(uri, bp.to_wire, params: params)
1944
+ client.poll_on_code(result['asyncTask']['link']['poll'])
1945
+ end
1946
+
1947
+ private
1948
+
1949
+ def send_mail_to_new_users(users, email_options)
1950
+ password = email_options[:email_password]
1951
+ from = email_options[:email_from]
1952
+ raise 'Missing sender email, please specify parameter "email_from"' unless from
1953
+ raise 'Missing authentication password, please specify parameter "email_password"' unless password
1954
+ template = get_email_template(email_options)
1955
+ smtp = Net::SMTP.new('relay1.na.intgdc.com', 25)
1956
+ smtp.enable_starttls OpenSSL::SSL::SSLContext.new("TLSv1_2_client")
1957
+ smtp.start('notifications.gooddata.com', 'gdc', password, :plain)
1958
+ users.each do |user|
1959
+ smtp.send_mail(get_email_body(template, user), from, user[:login])
1960
+ end
1961
+ end
1962
+
1963
+ def get_email_template(options)
1964
+ bucket = options[:email_template_bucket]
1965
+ path = options[:email_template_path]
1966
+ access_key = options[:email_template_access_key]
1967
+ secret_key = options[:email_template_secret_key]
1968
+ raise "Unable to connect to AWS. Parameter \"email_template_bucket\" seems to be empty" unless bucket
1969
+ raise "Unable to connect to AWS. Parameter \"email_template_path\" is missing" unless path
1970
+ raise "Unable to connect to AWS. Parameter \"email_template_access_key\" is missing" unless access_key
1971
+ raise "Unable to connect to AWS. Parameter \"email_template_secret_key\" is missing" unless secret_key
1972
+ args = {
1973
+ access_key_id: access_key,
1974
+ secret_access_key: secret_key,
1975
+ max_retries: 15,
1976
+ http_read_timeout: 120,
1977
+ http_open_timeout: 120
1978
+ }
1979
+
1980
+ server_side_encryption = options['email_server_side_encryption'] || false
1981
+ args['s3_server_side_encryption'] = :aes256 if server_side_encryption
1982
+
1983
+ s3 = Aws::S3::Resource.new(args)
1984
+ bucket = s3.bucket(bucket)
1985
+ process_email_template(bucket, path)
1986
+ end
1987
+
1988
+ def process_email_template(bucket, path)
1989
+ type = path.split('/').last.include?('.html') ? 'html' : 'txt'
1990
+ body = bucket.object(path).read
1991
+ body.prepend("MIME-Version: 1.0\nContent-type: text/html\n") if type == 'html'
1992
+ body
1993
+ end
1994
+
1995
+ def get_email_body(template, user)
1996
+ template.gsub('${name}', "#{user[:first_name]} #{user[:last_name]}")
1997
+ .gsub('${role}', user[:role_title].count == 1 ? user[:role_title].first : user[:role_title].to_s)
1998
+ .gsub('${user_group}', user[:user_group].count == 1 ? user[:user_group].first : user[:user_group].to_s)
1999
+ .gsub('${project}', Project[user[:pid]].title)
2000
+ end
2001
+
2002
+ def generate_user_payload(user_uri, status = 'ENABLED', roles_uri = nil)
2003
+ payload = {
2004
+ 'user' => {
2005
+ 'content' => {
2006
+ 'status' => status
2007
+ },
2008
+ 'links' => {
2009
+ 'self' => user_uri
2010
+ }
2011
+ }
2012
+ }
2013
+ payload['user']['content']['userRoles'] = roles_uri if roles_uri
2014
+ payload
2015
+ end
2016
+
2017
+ # Checks state of an export/import task.
2018
+ # @param response [Hash] Response from API
2019
+ # @param clone_task_error [Error] Error to raise when state is not OK
2020
+ def ensure_clone_task_ok(response, clone_task_error)
2021
+ if response['taskState'].nil?
2022
+ fail clone_task_error, "Clone task failed with unknown response: #{response}"
2023
+ elsif response['taskState']['status'] != 'OK'
2024
+ messages = response['taskState']['messages'] || []
2025
+ interpolated_messages = GoodData::Helpers.interpolate_error_messages(messages).join(' ')
2026
+ fail clone_task_error, "Clone task failed. #{interpolated_messages}"
2027
+ end
2028
+ end
2029
+ end
2030
+ end