gooddata 1.0.0-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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