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.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.editorconfig +12 -0
- data/.flayignore +6 -0
- data/.gitignore +38 -0
- data/.pronto.yml +3 -0
- data/.rspec +5 -0
- data/.rubocop.yml +101 -0
- data/.travis.yml +9 -0
- data/.yardopts +22 -0
- data/CHANGELOG.md +272 -0
- data/CLI.md +435 -0
- data/CONTRIBUTING.md +38 -0
- data/DEPENDENCIES.md +880 -0
- data/Dockerfile.jruby +17 -0
- data/Dockerfile.ruby +19 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/LICENSE +22 -0
- data/LICENSE.rb +5 -0
- data/README.md +78 -0
- data/Rakefile +204 -0
- data/TODO.md +32 -0
- data/authors.sh +4 -0
- data/bin/gooddata +7 -0
- data/ci.rake +47 -0
- data/dependency_decisions.yml +104 -0
- data/docker-compose.yml +34 -0
- data/gooddata +9 -0
- data/gooddata.gemspec +72 -0
- data/lib/gooddata.rb +34 -0
- data/lib/gooddata/app/app.rb +16 -0
- data/lib/gooddata/bricks/base_downloader.rb +86 -0
- data/lib/gooddata/bricks/brick.rb +37 -0
- data/lib/gooddata/bricks/bricks.rb +17 -0
- data/lib/gooddata/bricks/middleware/aws_middleware.rb +41 -0
- data/lib/gooddata/bricks/middleware/base_middleware.rb +57 -0
- data/lib/gooddata/bricks/middleware/bench_middleware.rb +25 -0
- data/lib/gooddata/bricks/middleware/bulk_salesforce_middleware.rb +37 -0
- data/lib/gooddata/bricks/middleware/decode_params_middleware.rb +21 -0
- data/lib/gooddata/bricks/middleware/dwh_middleware.rb +41 -0
- data/lib/gooddata/bricks/middleware/fs_download_middleware.rb +48 -0
- data/lib/gooddata/bricks/middleware/fs_upload_middleware.rb +36 -0
- data/lib/gooddata/bricks/middleware/gooddata_middleware.rb +112 -0
- data/lib/gooddata/bricks/middleware/logger_middleware.rb +33 -0
- data/lib/gooddata/bricks/middleware/middleware.rb +12 -0
- data/lib/gooddata/bricks/middleware/restforce_middleware.rb +58 -0
- data/lib/gooddata/bricks/middleware/stdout_middleware.rb +23 -0
- data/lib/gooddata/bricks/middleware/twitter_middleware.rb +29 -0
- data/lib/gooddata/bricks/middleware/undot_params_middleware.rb +37 -0
- data/lib/gooddata/bricks/pipeline.rb +25 -0
- data/lib/gooddata/bricks/utils.rb +18 -0
- data/lib/gooddata/cli/cli.rb +27 -0
- data/lib/gooddata/cli/commands/auth_cmd.rb +29 -0
- data/lib/gooddata/cli/commands/domain_cmd.rb +28 -0
- data/lib/gooddata/cli/commands/project_cmd.rb +28 -0
- data/lib/gooddata/cli/hooks.rb +56 -0
- data/lib/gooddata/cli/shared.rb +66 -0
- data/lib/gooddata/cli/terminal.rb +20 -0
- data/lib/gooddata/client.rb +67 -0
- data/lib/gooddata/commands/api.rb +64 -0
- data/lib/gooddata/commands/auth.rb +107 -0
- data/lib/gooddata/commands/base.rb +12 -0
- data/lib/gooddata/commands/commands.rb +12 -0
- data/lib/gooddata/commands/datasets.rb +148 -0
- data/lib/gooddata/commands/datawarehouse.rb +20 -0
- data/lib/gooddata/commands/domain.rb +40 -0
- data/lib/gooddata/commands/process.rb +67 -0
- data/lib/gooddata/commands/project.rb +144 -0
- data/lib/gooddata/commands/projects.rb +20 -0
- data/lib/gooddata/commands/role.rb +36 -0
- data/lib/gooddata/commands/runners.rb +47 -0
- data/lib/gooddata/commands/scaffold.rb +69 -0
- data/lib/gooddata/commands/user.rb +39 -0
- data/lib/gooddata/connection.rb +127 -0
- data/lib/gooddata/core/core.rb +12 -0
- data/lib/gooddata/core/logging.rb +105 -0
- data/lib/gooddata/core/nil_logger.rb +32 -0
- data/lib/gooddata/core/project.rb +74 -0
- data/lib/gooddata/core/rest.rb +149 -0
- data/lib/gooddata/core/user.rb +20 -0
- data/lib/gooddata/data/data.rb +12 -0
- data/lib/gooddata/data/guesser.rb +122 -0
- data/lib/gooddata/exceptions/attr_element_not_found.rb +16 -0
- data/lib/gooddata/exceptions/command_failed.rb +11 -0
- data/lib/gooddata/exceptions/exceptions.rb +12 -0
- data/lib/gooddata/exceptions/execution_limit_exceeded.rb +13 -0
- data/lib/gooddata/exceptions/export_clone.rb +4 -0
- data/lib/gooddata/exceptions/filter_maqlization.rb +16 -0
- data/lib/gooddata/exceptions/import_clone.rb +4 -0
- data/lib/gooddata/exceptions/malformed_user.rb +15 -0
- data/lib/gooddata/exceptions/maql_execution.rb +16 -0
- data/lib/gooddata/exceptions/no_project_error.rb +19 -0
- data/lib/gooddata/exceptions/object_migration.rb +32 -0
- data/lib/gooddata/exceptions/project_not_found.rb +13 -0
- data/lib/gooddata/exceptions/segment_not_empty.rb +18 -0
- data/lib/gooddata/exceptions/uncomputable_report.rb +13 -0
- data/lib/gooddata/exceptions/user_in_different_domain.rb +15 -0
- data/lib/gooddata/exceptions/validation_error.rb +16 -0
- data/lib/gooddata/extensions/big_decimal.rb +17 -0
- data/lib/gooddata/extensions/class.rb +11 -0
- data/lib/gooddata/extensions/enumerable.rb +39 -0
- data/lib/gooddata/extensions/extensions.rb +10 -0
- data/lib/gooddata/extensions/false.rb +23 -0
- data/lib/gooddata/extensions/hash.rb +49 -0
- data/lib/gooddata/extensions/integer.rb +5 -0
- data/lib/gooddata/extensions/nil.rb +19 -0
- data/lib/gooddata/extensions/numeric.rb +15 -0
- data/lib/gooddata/extensions/object.rb +31 -0
- data/lib/gooddata/extensions/string.rb +7 -0
- data/lib/gooddata/extensions/symbol.rb +15 -0
- data/lib/gooddata/extensions/true.rb +23 -0
- data/lib/gooddata/extract.rb +21 -0
- data/lib/gooddata/goodzilla/goodzilla.rb +160 -0
- data/lib/gooddata/helpers/auth_helpers.rb +75 -0
- data/lib/gooddata/helpers/csv_helper.rb +61 -0
- data/lib/gooddata/helpers/data_helper.rb +129 -0
- data/lib/gooddata/helpers/erb_helper.rb +23 -0
- data/lib/gooddata/helpers/global_helpers.rb +266 -0
- data/lib/gooddata/helpers/global_helpers_params.rb +292 -0
- data/lib/gooddata/helpers/helpers.rb +10 -0
- data/lib/gooddata/lcm/actions/actions.rb +12 -0
- data/lib/gooddata/lcm/actions/apply_custom_maql.rb +80 -0
- data/lib/gooddata/lcm/actions/associate_clients.rb +87 -0
- data/lib/gooddata/lcm/actions/base_action.rb +23 -0
- data/lib/gooddata/lcm/actions/collect_ca_metrics.rb +53 -0
- data/lib/gooddata/lcm/actions/collect_client_projects.rb +78 -0
- data/lib/gooddata/lcm/actions/collect_clients.rb +128 -0
- data/lib/gooddata/lcm/actions/collect_data_product.rb +57 -0
- data/lib/gooddata/lcm/actions/collect_dynamic_schedule_params.rb +62 -0
- data/lib/gooddata/lcm/actions/collect_ldm_objects.rb +56 -0
- data/lib/gooddata/lcm/actions/collect_meta.rb +88 -0
- data/lib/gooddata/lcm/actions/collect_segment_clients.rb +113 -0
- data/lib/gooddata/lcm/actions/collect_segments.rb +72 -0
- data/lib/gooddata/lcm/actions/collect_tagged_objects.rb +80 -0
- data/lib/gooddata/lcm/actions/collect_users_brick_users.rb +46 -0
- data/lib/gooddata/lcm/actions/create_segment_masters.rb +160 -0
- data/lib/gooddata/lcm/actions/ensure_data_product.rb +53 -0
- data/lib/gooddata/lcm/actions/ensure_release_table.rb +53 -0
- data/lib/gooddata/lcm/actions/ensure_segments.rb +32 -0
- data/lib/gooddata/lcm/actions/ensure_technical_users_domain.rb +70 -0
- data/lib/gooddata/lcm/actions/ensure_technical_users_project.rb +83 -0
- data/lib/gooddata/lcm/actions/execute_schedules.rb +128 -0
- data/lib/gooddata/lcm/actions/hello_world.rb +41 -0
- data/lib/gooddata/lcm/actions/import_object_collections.rb +60 -0
- data/lib/gooddata/lcm/actions/print_actions.rb +58 -0
- data/lib/gooddata/lcm/actions/print_modes.rb +69 -0
- data/lib/gooddata/lcm/actions/print_types.rb +52 -0
- data/lib/gooddata/lcm/actions/provision_clients.rb +89 -0
- data/lib/gooddata/lcm/actions/purge_clients.rb +58 -0
- data/lib/gooddata/lcm/actions/rename_existing_client_projects.rb +70 -0
- data/lib/gooddata/lcm/actions/segments_filter.rb +50 -0
- data/lib/gooddata/lcm/actions/synchronize_attribute_drillpaths.rb +64 -0
- data/lib/gooddata/lcm/actions/synchronize_cas.rb +72 -0
- data/lib/gooddata/lcm/actions/synchronize_clients.rb +94 -0
- data/lib/gooddata/lcm/actions/synchronize_color_palette.rb +67 -0
- data/lib/gooddata/lcm/actions/synchronize_etls_in_segment.rb +155 -0
- data/lib/gooddata/lcm/actions/synchronize_label_types.rb +64 -0
- data/lib/gooddata/lcm/actions/synchronize_ldm.rb +82 -0
- data/lib/gooddata/lcm/actions/synchronize_meta.rb +52 -0
- data/lib/gooddata/lcm/actions/synchronize_new_segments.rb +61 -0
- data/lib/gooddata/lcm/actions/synchronize_processes.rb +66 -0
- data/lib/gooddata/lcm/actions/synchronize_schedules.rb +91 -0
- data/lib/gooddata/lcm/actions/synchronize_tag_objects.rb +64 -0
- data/lib/gooddata/lcm/actions/synchronize_user_filters.rb +210 -0
- data/lib/gooddata/lcm/actions/synchronize_user_groups.rb +53 -0
- data/lib/gooddata/lcm/actions/synchronize_users.rb +336 -0
- data/lib/gooddata/lcm/actions/update_release_table.rb +77 -0
- data/lib/gooddata/lcm/data/create_lcm_release.sql.erb +6 -0
- data/lib/gooddata/lcm/data/insert_into_lcm_release.sql.erb +11 -0
- data/lib/gooddata/lcm/data/select_from_lcm_release.sql.erb +8 -0
- data/lib/gooddata/lcm/data/update_lcm_release.sql.erb +7 -0
- data/lib/gooddata/lcm/dsl/dsl.rb +50 -0
- data/lib/gooddata/lcm/dsl/params_dsl.rb +61 -0
- data/lib/gooddata/lcm/dsl/type_dsl.rb +62 -0
- data/lib/gooddata/lcm/helpers/check_helper.rb +41 -0
- data/lib/gooddata/lcm/helpers/helpers.rb +12 -0
- data/lib/gooddata/lcm/helpers/tags_helper.rb +36 -0
- data/lib/gooddata/lcm/lcm.rb +325 -0
- data/lib/gooddata/lcm/lcm2.rb +400 -0
- data/lib/gooddata/lcm/types/base_type.rb +29 -0
- data/lib/gooddata/lcm/types/class/ads_client.rb +35 -0
- data/lib/gooddata/lcm/types/class/class.rb +21 -0
- data/lib/gooddata/lcm/types/class/gd_client.rb +35 -0
- data/lib/gooddata/lcm/types/class/types.rb +12 -0
- data/lib/gooddata/lcm/types/complex/complex.rb +33 -0
- data/lib/gooddata/lcm/types/complex/release_query.rb +35 -0
- data/lib/gooddata/lcm/types/complex/segment.rb +40 -0
- data/lib/gooddata/lcm/types/complex/synchronization_info.rb +35 -0
- data/lib/gooddata/lcm/types/complex/tokens.rb +31 -0
- data/lib/gooddata/lcm/types/complex/types.rb +12 -0
- data/lib/gooddata/lcm/types/complex/update_preference.rb +38 -0
- data/lib/gooddata/lcm/types/complex/users_brick_config.rb +32 -0
- data/lib/gooddata/lcm/types/param.rb +16 -0
- data/lib/gooddata/lcm/types/scalar/bool.rb +22 -0
- data/lib/gooddata/lcm/types/scalar/hash.rb +22 -0
- data/lib/gooddata/lcm/types/scalar/integer.rb +22 -0
- data/lib/gooddata/lcm/types/scalar/object.rb +22 -0
- data/lib/gooddata/lcm/types/scalar/string.rb +22 -0
- data/lib/gooddata/lcm/types/scalar/types.rb +12 -0
- data/lib/gooddata/lcm/types/special/array.rb +33 -0
- data/lib/gooddata/lcm/types/special/enum.rb +15 -0
- data/lib/gooddata/lcm/types/special/types.rb +12 -0
- data/lib/gooddata/lcm/types/types.rb +12 -0
- data/lib/gooddata/lcm/user_bricks_helper.rb +32 -0
- data/lib/gooddata/mixins/author.rb +26 -0
- data/lib/gooddata/mixins/content_getter.rb +15 -0
- data/lib/gooddata/mixins/content_property_reader.rb +17 -0
- data/lib/gooddata/mixins/content_property_writer.rb +17 -0
- data/lib/gooddata/mixins/contributor.rb +20 -0
- data/lib/gooddata/mixins/data_getter.rb +15 -0
- data/lib/gooddata/mixins/data_property_reader.rb +19 -0
- data/lib/gooddata/mixins/data_property_writer.rb +19 -0
- data/lib/gooddata/mixins/inspector.rb +53 -0
- data/lib/gooddata/mixins/is_attribute.rb +17 -0
- data/lib/gooddata/mixins/is_dimension.rb +17 -0
- data/lib/gooddata/mixins/is_fact.rb +17 -0
- data/lib/gooddata/mixins/is_folder.rb +11 -0
- data/lib/gooddata/mixins/is_label.rb +19 -0
- data/lib/gooddata/mixins/links.rb +15 -0
- data/lib/gooddata/mixins/md_finders.rb +79 -0
- data/lib/gooddata/mixins/md_grantees.rb +42 -0
- data/lib/gooddata/mixins/md_id_to_uri.rb +39 -0
- data/lib/gooddata/mixins/md_json.rb +15 -0
- data/lib/gooddata/mixins/md_lock.rb +87 -0
- data/lib/gooddata/mixins/md_object_id.rb +15 -0
- data/lib/gooddata/mixins/md_object_indexer.rb +74 -0
- data/lib/gooddata/mixins/md_object_query.rb +134 -0
- data/lib/gooddata/mixins/md_relations.rb +43 -0
- data/lib/gooddata/mixins/meta_getter.rb +17 -0
- data/lib/gooddata/mixins/meta_property_reader.rb +19 -0
- data/lib/gooddata/mixins/meta_property_writer.rb +19 -0
- data/lib/gooddata/mixins/mixins.rb +19 -0
- data/lib/gooddata/mixins/not_attribute.rb +17 -0
- data/lib/gooddata/mixins/not_exportable.rb +15 -0
- data/lib/gooddata/mixins/not_fact.rb +17 -0
- data/lib/gooddata/mixins/not_group.rb +17 -0
- data/lib/gooddata/mixins/not_label.rb +19 -0
- data/lib/gooddata/mixins/not_metric.rb +19 -0
- data/lib/gooddata/mixins/obj_id.rb +15 -0
- data/lib/gooddata/mixins/rest_getters.rb +17 -0
- data/lib/gooddata/mixins/rest_resource.rb +47 -0
- data/lib/gooddata/mixins/root_key_getter.rb +15 -0
- data/lib/gooddata/mixins/root_key_setter.rb +15 -0
- data/lib/gooddata/mixins/timestamps.rb +19 -0
- data/lib/gooddata/mixins/to_json.rb +11 -0
- data/lib/gooddata/mixins/uri_getter.rb +9 -0
- data/lib/gooddata/models/ads_output_stage.rb +85 -0
- data/lib/gooddata/models/automated_data_distribution.rb +36 -0
- data/lib/gooddata/models/blueprint/anchor_field.rb +65 -0
- data/lib/gooddata/models/blueprint/attribute_field.rb +45 -0
- data/lib/gooddata/models/blueprint/blueprint.rb +11 -0
- data/lib/gooddata/models/blueprint/blueprint_field.rb +70 -0
- data/lib/gooddata/models/blueprint/dashboard_builder.rb +30 -0
- data/lib/gooddata/models/blueprint/dataset_blueprint.rb +455 -0
- data/lib/gooddata/models/blueprint/date_dimension.rb +20 -0
- data/lib/gooddata/models/blueprint/fact_field.rb +20 -0
- data/lib/gooddata/models/blueprint/label_field.rb +47 -0
- data/lib/gooddata/models/blueprint/project_blueprint.rb +791 -0
- data/lib/gooddata/models/blueprint/project_builder.rb +103 -0
- data/lib/gooddata/models/blueprint/reference_field.rb +43 -0
- data/lib/gooddata/models/blueprint/schema_blueprint.rb +160 -0
- data/lib/gooddata/models/blueprint/schema_builder.rb +89 -0
- data/lib/gooddata/models/blueprint/to_manifest.rb +185 -0
- data/lib/gooddata/models/blueprint/to_wire.rb +173 -0
- data/lib/gooddata/models/channel_configuration.rb +112 -0
- data/lib/gooddata/models/client.rb +236 -0
- data/lib/gooddata/models/client_synchronization_result.rb +31 -0
- data/lib/gooddata/models/client_synchronization_result_details.rb +41 -0
- data/lib/gooddata/models/data_product.rb +149 -0
- data/lib/gooddata/models/datawarehouse.rb +114 -0
- data/lib/gooddata/models/domain.rb +505 -0
- data/lib/gooddata/models/execution.rb +115 -0
- data/lib/gooddata/models/execution_detail.rb +81 -0
- data/lib/gooddata/models/from_wire.rb +173 -0
- data/lib/gooddata/models/invitation.rb +75 -0
- data/lib/gooddata/models/links.rb +50 -0
- data/lib/gooddata/models/membership.rb +441 -0
- data/lib/gooddata/models/metadata.rb +324 -0
- data/lib/gooddata/models/metadata/attribute.rb +155 -0
- data/lib/gooddata/models/metadata/dashboard.rb +120 -0
- data/lib/gooddata/models/metadata/dashboard/dashboard_item.rb +76 -0
- data/lib/gooddata/models/metadata/dashboard/filter_apply_item.rb +37 -0
- data/lib/gooddata/models/metadata/dashboard/filter_item.rb +64 -0
- data/lib/gooddata/models/metadata/dashboard/geo_chart_item.rb +56 -0
- data/lib/gooddata/models/metadata/dashboard/headline_item.rb +56 -0
- data/lib/gooddata/models/metadata/dashboard/iframe_item.rb +46 -0
- data/lib/gooddata/models/metadata/dashboard/report_item.rb +92 -0
- data/lib/gooddata/models/metadata/dashboard/text_item.rb +55 -0
- data/lib/gooddata/models/metadata/dashboard_tab.rb +141 -0
- data/lib/gooddata/models/metadata/dataset.rb +67 -0
- data/lib/gooddata/models/metadata/dimension.rb +57 -0
- data/lib/gooddata/models/metadata/fact.rb +51 -0
- data/lib/gooddata/models/metadata/folder.rb +49 -0
- data/lib/gooddata/models/metadata/label.rb +128 -0
- data/lib/gooddata/models/metadata/metadata.rb +12 -0
- data/lib/gooddata/models/metadata/metric.rb +206 -0
- data/lib/gooddata/models/metadata/report.rb +268 -0
- data/lib/gooddata/models/metadata/report_definition.rb +272 -0
- data/lib/gooddata/models/metadata/scheduled_mail.rb +277 -0
- data/lib/gooddata/models/metadata/scheduled_mail/dashboard_attachment.rb +62 -0
- data/lib/gooddata/models/metadata/scheduled_mail/report_attachment.rb +64 -0
- data/lib/gooddata/models/metadata/variable.rb +96 -0
- data/lib/gooddata/models/model.rb +293 -0
- data/lib/gooddata/models/models.rb +12 -0
- data/lib/gooddata/models/module_constants.rb +31 -0
- data/lib/gooddata/models/notification_rule.rb +113 -0
- data/lib/gooddata/models/process.rb +371 -0
- data/lib/gooddata/models/profile.rb +451 -0
- data/lib/gooddata/models/project.rb +2030 -0
- data/lib/gooddata/models/project_creator.rb +209 -0
- data/lib/gooddata/models/project_log_formatter.rb +204 -0
- data/lib/gooddata/models/project_metadata.rb +67 -0
- data/lib/gooddata/models/project_role.rb +79 -0
- data/lib/gooddata/models/report_data_result.rb +270 -0
- data/lib/gooddata/models/schedule.rb +538 -0
- data/lib/gooddata/models/segment.rb +274 -0
- data/lib/gooddata/models/style_setting.rb +62 -0
- data/lib/gooddata/models/subscription.rb +188 -0
- data/lib/gooddata/models/tab_builder.rb +27 -0
- data/lib/gooddata/models/user_filters/mandatory_user_filter.rb +76 -0
- data/lib/gooddata/models/user_filters/user_filter.rb +101 -0
- data/lib/gooddata/models/user_filters/user_filter_builder.rb +553 -0
- data/lib/gooddata/models/user_filters/user_filters.rb +13 -0
- data/lib/gooddata/models/user_filters/variable_user_filter.rb +33 -0
- data/lib/gooddata/models/user_group.rb +250 -0
- data/lib/gooddata/rest/README.md +37 -0
- data/lib/gooddata/rest/client.rb +396 -0
- data/lib/gooddata/rest/connection.rb +776 -0
- data/lib/gooddata/rest/object.rb +69 -0
- data/lib/gooddata/rest/object_factory.rb +51 -0
- data/lib/gooddata/rest/resource.rb +27 -0
- data/lib/gooddata/rest/rest.rb +24 -0
- data/lib/gooddata/version.rb +23 -0
- data/lib/templates/bricks/brick.rb.erb +7 -0
- data/lib/templates/bricks/main.rb.erb +5 -0
- data/lib/templates/project/Goodfile.erb +4 -0
- data/lib/templates/project/data/commits.csv +4 -0
- data/lib/templates/project/data/devs.csv +4 -0
- data/lib/templates/project/data/repos.csv +3 -0
- data/lib/templates/project/model/model.rb.erb +20 -0
- data/spec/.rubocop.yml +16 -0
- data/spec/bricks/bricks_spec.rb +110 -0
- data/spec/bricks/default-config.json +8 -0
- data/spec/data/.gooddata +4 -0
- data/spec/data/blueprints/additional_dataset_module.json +32 -0
- data/spec/data/blueprints/attribute_sort_order_blueprint.json +72 -0
- data/spec/data/blueprints/big_blueprint_not_pruned.json +2079 -0
- data/spec/data/blueprints/invalid_blueprint.json +103 -0
- data/spec/data/blueprints/m_n_model.json +104 -0
- data/spec/data/blueprints/model_module.json +25 -0
- data/spec/data/blueprints/test_blueprint.json +39 -0
- data/spec/data/blueprints/test_project_model_spec.json +106 -0
- data/spec/data/cc/data/source/commits.csv +4 -0
- data/spec/data/cc/data/source/devs.csv +4 -0
- data/spec/data/cc/data/source/repos.csv +3 -0
- data/spec/data/cc/devel.prm +0 -0
- data/spec/data/cc/graph/graph.grf +11 -0
- data/spec/data/cc/workspace.prm +19 -0
- data/spec/data/column_based_permissions.csv +7 -0
- data/spec/data/column_based_permissions2.csv +6 -0
- data/spec/data/dynamic_schedule_params_table.csv +7 -0
- data/spec/data/gd_gse_data_blueprint.json +1371 -0
- data/spec/data/gd_gse_data_manifest.json +1424 -0
- data/spec/data/gd_gse_data_model.json +1772 -0
- data/spec/data/gooddata_version_process/gooddata_version.rb +9 -0
- data/spec/data/gooddata_version_process/gooddata_version.zip +0 -0
- data/spec/data/hello_world_process/hello_world.rb +9 -0
- data/spec/data/hello_world_process/hello_world.zip +0 -0
- data/spec/data/line_based_permissions.csv +3 -0
- data/spec/data/manifests/test_blueprint.json +32 -0
- data/spec/data/manifests/test_project.json +107 -0
- data/spec/data/reports/left_attr_report.json +108 -0
- data/spec/data/reports/metric_only_one_line.json +83 -0
- data/spec/data/reports/report_1.json +197 -0
- data/spec/data/reports/top_attr_report.json +108 -0
- data/spec/data/ruby_params_process/ruby_params.rb +9 -0
- data/spec/data/ruby_process/deep_files/deep_stuff.txt +1 -0
- data/spec/data/ruby_process/process.rb +8 -0
- data/spec/data/ruby_process/stuff.txt +1 -0
- data/spec/data/superfluous_titles_view.json +81 -0
- data/spec/data/test-ci-data.csv +2 -0
- data/spec/data/users.csv +12 -0
- data/spec/data/wire_models/attribute_sort_by_model.json +73 -0
- data/spec/data/wire_models/model_view.json +1775 -0
- data/spec/data/wire_models/nu_model.json +3046 -0
- data/spec/data/wire_models/test_blueprint.json +66 -0
- data/spec/data/wire_test_project.json +150 -0
- data/spec/data/workspace_table.csv +3 -0
- data/spec/environment/default.rb +43 -0
- data/spec/environment/development.rb +32 -0
- data/spec/environment/environment.rb +38 -0
- data/spec/environment/production.rb +27 -0
- data/spec/environment/staging.rb +33 -0
- data/spec/environment/testing.rb +32 -0
- data/spec/helpers/blueprint_helper.rb +27 -0
- data/spec/helpers/cli_helper.rb +38 -0
- data/spec/helpers/connection_helper.rb +43 -0
- data/spec/helpers/crypto_helper.rb +19 -0
- data/spec/helpers/csv_helper.rb +20 -0
- data/spec/helpers/process_helper.rb +35 -0
- data/spec/helpers/project_helper.rb +74 -0
- data/spec/helpers/schedule_helper.rb +33 -0
- data/spec/helpers/spec_helper.rb +17 -0
- data/spec/integration/ads_output_stage_spec.rb +45 -0
- data/spec/integration/blueprint_updates_spec.rb +107 -0
- data/spec/integration/blueprint_with_ca_spec.rb +56 -0
- data/spec/integration/blueprint_with_grain_spec.rb +74 -0
- data/spec/integration/channel_configuration_spec.rb +67 -0
- data/spec/integration/clients_spec.rb +164 -0
- data/spec/integration/command_datawarehouse_spec.rb +45 -0
- data/spec/integration/command_projects_spec.rb +32 -0
- data/spec/integration/commands/command_projects_spec.rb +22 -0
- data/spec/integration/core/connection_spec.rb +56 -0
- data/spec/integration/core/logging_spec.rb +130 -0
- data/spec/integration/core/project_spec.rb +54 -0
- data/spec/integration/create_from_template_spec.rb +29 -0
- data/spec/integration/create_project_spec.rb +27 -0
- data/spec/integration/date_dim_switch_spec.rb +150 -0
- data/spec/integration/deprecated_load_spec.rb +60 -0
- data/spec/integration/full_process_schedule_spec.rb +367 -0
- data/spec/integration/full_project_spec.rb +592 -0
- data/spec/integration/helpers_spec.rb +16 -0
- data/spec/integration/lcm_spec.rb +54 -0
- data/spec/integration/mixins/id_to_uri_spec.rb +44 -0
- data/spec/integration/models/data_product_spec.rb +71 -0
- data/spec/integration/models/domain_spec.rb +162 -0
- data/spec/integration/models/invitation_spec.rb +17 -0
- data/spec/integration/models/membership_spec.rb +127 -0
- data/spec/integration/models/metadata/report_spec.rb +54 -0
- data/spec/integration/models/params_spec.rb +118 -0
- data/spec/integration/models/profile_spec.rb +210 -0
- data/spec/integration/models/project_role_spec.rb +94 -0
- data/spec/integration/models/project_spec.rb +225 -0
- data/spec/integration/models/schedule_spec.rb +485 -0
- data/spec/integration/models/unit_project_spec.rb +130 -0
- data/spec/integration/over_to_user_filters_spec.rb +98 -0
- data/spec/integration/partial_md_export_import_spec.rb +41 -0
- data/spec/integration/project_spec.rb +381 -0
- data/spec/integration/rest_spec.rb +214 -0
- data/spec/integration/schedule_spec.rb +613 -0
- data/spec/integration/segments_spec.rb +100 -0
- data/spec/integration/subscription_spec.rb +88 -0
- data/spec/integration/urn_date_dim_spec.rb +53 -0
- data/spec/integration/user_filters_spec.rb +306 -0
- data/spec/integration/user_group_spec.rb +147 -0
- data/spec/integration/variables_spec.rb +189 -0
- data/spec/logging_in_logging_out_spec.rb +91 -0
- data/spec/spec_helper.rb +66 -0
- data/spec/unit/actions/associate_clients_spec.rb +47 -0
- data/spec/unit/actions/collect_client_projects_spec.rb +47 -0
- data/spec/unit/actions/collect_clients_spec.rb +65 -0
- data/spec/unit/actions/collect_data_product_spec.rb +56 -0
- data/spec/unit/actions/collect_dynamic_schedule_params_spec.rb +56 -0
- data/spec/unit/actions/collect_meta_spec.rb +88 -0
- data/spec/unit/actions/collect_segment_clients_spec.rb +81 -0
- data/spec/unit/actions/collect_tagged_objects_spec.rb +126 -0
- data/spec/unit/actions/collect_users_brick_users_spec.rb +36 -0
- data/spec/unit/actions/create_segment_masters_spec.rb +64 -0
- data/spec/unit/actions/ensure_data_product_spec.rb +38 -0
- data/spec/unit/actions/ensure_technical_users_domain_spec.rb +51 -0
- data/spec/unit/actions/ensure_technical_users_project_spec.rb +72 -0
- data/spec/unit/actions/execute_schedules_spec.rb +94 -0
- data/spec/unit/actions/provision_clients_spec.rb +45 -0
- data/spec/unit/actions/purge_clients_spec.rb +47 -0
- data/spec/unit/actions/rename_existing_client_projects_spec.rb +54 -0
- data/spec/unit/actions/segments_filter_spec.rb +46 -0
- data/spec/unit/actions/shared_examples_for_user_actions.rb +26 -0
- data/spec/unit/actions/synchronize_cas_spec.rb +58 -0
- data/spec/unit/actions/synchronize_etls_in_segment_spec.rb +212 -0
- data/spec/unit/actions/synchronize_ldm_spec.rb +57 -0
- data/spec/unit/actions/synchronize_user_filters_spec.rb +146 -0
- data/spec/unit/actions/synchronize_user_groups_spec.rb +49 -0
- data/spec/unit/actions/synchronize_users_spec.rb +134 -0
- data/spec/unit/bricks/bricks_spec.rb +34 -0
- data/spec/unit/bricks/middleware/aws_middelware_spec.rb +98 -0
- data/spec/unit/bricks/middleware/bench_middleware_spec.rb +15 -0
- data/spec/unit/bricks/middleware/bulk_salesforce_middleware_spec.rb +15 -0
- data/spec/unit/bricks/middleware/gooddata_middleware_spec.rb +15 -0
- data/spec/unit/bricks/middleware/logger_middleware_spec.rb +30 -0
- data/spec/unit/bricks/middleware/restforce_middleware_spec.rb +15 -0
- data/spec/unit/bricks/middleware/stdout_middleware_spec.rb +15 -0
- data/spec/unit/bricks/middleware/twitter_middleware_spec.rb +15 -0
- data/spec/unit/cli/cli_spec.rb +17 -0
- data/spec/unit/cli/commands/cmd_auth_spec.rb +17 -0
- data/spec/unit/core/nil_logger_spec.rb +13 -0
- data/spec/unit/extensions/hash_spec.rb +22 -0
- data/spec/unit/godzilla/goodzilla_spec.rb +74 -0
- data/spec/unit/helpers/csv_helper_spec.rb +22 -0
- data/spec/unit/helpers/data_helper_spec.rb +67 -0
- data/spec/unit/helpers/global_helpers_spec.rb +264 -0
- data/spec/unit/helpers_spec.rb +254 -0
- data/spec/unit/lcm/user_bricks_helper_spec.rb +58 -0
- data/spec/unit/models/blueprint/attribute_sort_by_spec.rb +20 -0
- data/spec/unit/models/blueprint/attributes_spec.rb +28 -0
- data/spec/unit/models/blueprint/dataset_spec.rb +120 -0
- data/spec/unit/models/blueprint/labels_spec.rb +43 -0
- data/spec/unit/models/blueprint/project_blueprint_spec.rb +642 -0
- data/spec/unit/models/blueprint/reference_spec.rb +28 -0
- data/spec/unit/models/blueprint/schema_builder_spec.rb +36 -0
- data/spec/unit/models/blueprint/to_wire_spec.rb +195 -0
- data/spec/unit/models/execution_spec.rb +109 -0
- data/spec/unit/models/from_wire_spec.rb +301 -0
- data/spec/unit/models/metadata_spec.rb +140 -0
- data/spec/unit/models/metric_spec.rb +129 -0
- data/spec/unit/models/model_spec.rb +77 -0
- data/spec/unit/models/project_creator_spec.rb +90 -0
- data/spec/unit/models/project_spec.rb +94 -0
- data/spec/unit/models/report_result_data_spec.rb +194 -0
- data/spec/unit/models/to_manifest_spec.rb +136 -0
- data/spec/unit/models/user_filters/user_filter_builder_spec.rb +110 -0
- data/spec/unit/models/user_filters_spec.rb +95 -0
- data/spec/unit/models/variable_spec.rb +280 -0
- data/spec/unit/rest/polling_spec.rb +101 -0
- data/spec/unit/rest/resource_spec.rb +10 -0
- data/yard-server.sh +3 -0
- 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
|