gooddata 0.6.24 → 0.6.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +54 -0
  3. data/CHANGELOG.md +3 -0
  4. data/DEPENDENCIES.md +155 -154
  5. data/README.md +15 -6
  6. data/Rakefile +5 -3
  7. data/dependency_decisions.yml +2 -0
  8. data/gooddata.gemspec +2 -3
  9. data/lib/gooddata/cli/cli.rb +1 -3
  10. data/lib/gooddata/cli/commands/auth_cmd.rb +16 -7
  11. data/lib/gooddata/cli/commands/project_cmd.rb +16 -178
  12. data/lib/gooddata/cli/shared.rb +46 -44
  13. data/lib/gooddata/commands/auth.rb +4 -0
  14. data/lib/gooddata/commands/project.rb +7 -24
  15. data/lib/gooddata/exceptions/object_migration.rb +4 -0
  16. data/lib/gooddata/exceptions/segment_not_empty.rb +18 -0
  17. data/lib/gooddata/extensions/object.rb +12 -0
  18. data/lib/gooddata/goodzilla/goodzilla.rb +56 -9
  19. data/lib/gooddata/helpers/global_helpers.rb +92 -0
  20. data/lib/gooddata/mixins/md_finders.rb +2 -8
  21. data/lib/gooddata/mixins/md_grantees.rb +42 -0
  22. data/lib/gooddata/mixins/md_id_to_uri.rb +1 -8
  23. data/lib/gooddata/mixins/md_object_id.rb +1 -1
  24. data/lib/gooddata/mixins/md_object_indexer.rb +5 -8
  25. data/lib/gooddata/mixins/md_object_query.rb +2 -2
  26. data/lib/gooddata/mixins/not_group.rb +17 -0
  27. data/lib/gooddata/mixins/rest_getters.rb +2 -2
  28. data/lib/gooddata/mixins/rest_resource.rb +1 -0
  29. data/lib/gooddata/mixins/to_json.rb +11 -0
  30. data/lib/gooddata/mixins/uri_getter.rb +9 -0
  31. data/lib/gooddata/models/blueprint/anchor_field.rb +14 -0
  32. data/lib/gooddata/models/blueprint/project_blueprint.rb +15 -1
  33. data/lib/gooddata/models/blueprint/to_wire.rb +10 -0
  34. data/lib/gooddata/models/client.rb +178 -0
  35. data/lib/gooddata/models/client_synchronization_result.rb +31 -0
  36. data/lib/gooddata/models/client_synchronization_result_details.rb +41 -0
  37. data/lib/gooddata/models/datawarehouse.rb +1 -5
  38. data/lib/gooddata/models/domain.rb +85 -1
  39. data/lib/gooddata/models/execution.rb +0 -2
  40. data/lib/gooddata/models/execution_detail.rb +0 -2
  41. data/lib/gooddata/models/from_wire.rb +10 -0
  42. data/lib/gooddata/models/invitation.rb +1 -1
  43. data/lib/gooddata/models/links.rb +1 -1
  44. data/lib/gooddata/models/membership.rb +10 -6
  45. data/lib/gooddata/models/metadata.rb +98 -11
  46. data/lib/gooddata/models/metadata/attribute.rb +6 -7
  47. data/lib/gooddata/models/metadata/dashboard.rb +41 -75
  48. data/lib/gooddata/models/metadata/dashboard/dashboard_item.rb +20 -4
  49. data/lib/gooddata/models/metadata/dashboard/filter_apply_item.rb +37 -0
  50. data/lib/gooddata/models/metadata/dashboard/filter_item.rb +49 -0
  51. data/lib/gooddata/models/metadata/dashboard/geo_chart_item.rb +56 -0
  52. data/lib/gooddata/models/metadata/dashboard/headline_item.rb +56 -0
  53. data/lib/gooddata/models/metadata/dashboard/iframe_item.rb +46 -0
  54. data/lib/gooddata/models/metadata/dashboard/report_item.rb +49 -8
  55. data/lib/gooddata/models/metadata/dashboard/text_item.rb +55 -0
  56. data/lib/gooddata/models/metadata/dashboard_tab.rb +83 -30
  57. data/lib/gooddata/models/metadata/dataset.rb +0 -2
  58. data/lib/gooddata/models/metadata/dimension.rb +1 -3
  59. data/lib/gooddata/models/metadata/fact.rb +1 -3
  60. data/lib/gooddata/models/metadata/label.rb +1 -3
  61. data/lib/gooddata/models/metadata/metric.rb +11 -42
  62. data/lib/gooddata/models/metadata/report.rb +7 -18
  63. data/lib/gooddata/models/metadata/report_definition.rb +21 -113
  64. data/lib/gooddata/models/metadata/scheduled_mail.rb +274 -0
  65. data/lib/gooddata/models/metadata/scheduled_mail/dashboard_attachment.rb +62 -0
  66. data/lib/gooddata/models/metadata/scheduled_mail/report_attachment.rb +64 -0
  67. data/lib/gooddata/models/metadata/variable.rb +8 -2
  68. data/lib/gooddata/models/model.rb +2 -9
  69. data/lib/gooddata/models/process.rb +7 -29
  70. data/lib/gooddata/models/profile.rb +1 -1
  71. data/lib/gooddata/models/project.rb +131 -167
  72. data/lib/gooddata/models/project_creator.rb +2 -7
  73. data/lib/gooddata/models/project_metadata.rb +2 -10
  74. data/lib/gooddata/models/project_role.rb +4 -10
  75. data/lib/gooddata/models/report_data_result.rb +3 -5
  76. data/lib/gooddata/models/schedule.rb +4 -31
  77. data/lib/gooddata/models/segment.rb +192 -0
  78. data/lib/gooddata/models/user_filters/mandatory_user_filter.rb +2 -2
  79. data/lib/gooddata/models/user_filters/user_filter_builder.rb +1 -1
  80. data/lib/gooddata/models/user_filters/variable_user_filter.rb +11 -0
  81. data/lib/gooddata/models/user_group.rb +241 -0
  82. data/lib/gooddata/rest/connection.rb +81 -16
  83. data/lib/gooddata/rest/object.rb +29 -0
  84. data/lib/gooddata/rest/object_factory.rb +6 -1
  85. data/lib/gooddata/rest/resource.rb +7 -1
  86. data/lib/gooddata/version.rb +1 -1
  87. data/spec/environment/default.rb +19 -16
  88. data/spec/environment/develop.rb +10 -10
  89. data/spec/environment/hotfix.rb +6 -6
  90. data/spec/environment/production.rb +14 -14
  91. data/spec/environment/release.rb +6 -6
  92. data/spec/environment/staging.rb +9 -9
  93. data/spec/environment/staging_3.rb +14 -15
  94. data/spec/integration/blueprint_with_grain_spec.rb +72 -0
  95. data/spec/integration/clients_spec.rb +135 -0
  96. data/spec/integration/date_dim_switch_spec.rb +142 -0
  97. data/spec/integration/full_project_spec.rb +3 -3
  98. data/spec/integration/project_spec.rb +20 -0
  99. data/spec/integration/segments_spec.rb +141 -0
  100. data/spec/integration/user_group_spec.rb +127 -0
  101. data/spec/spec_helper.rb +4 -0
  102. data/spec/unit/models/domain_spec.rb +7 -1
  103. data/spec/unit/models/metric_spec.rb +0 -8
  104. data/spec/unit/models/profile_spec.rb +1 -1
  105. data/spec/unit/models/report_result_data_spec.rb +6 -0
  106. metadata +38 -38
  107. data/lib/gooddata/cli/commands/api_cmd.rb +0 -34
  108. data/lib/gooddata/cli/commands/console_cmd.rb +0 -40
  109. data/lib/gooddata/cli/commands/domain_cmd.rb +0 -46
  110. data/lib/gooddata/cli/commands/process_cmd.rb +0 -145
  111. data/lib/gooddata/cli/commands/projects_cmd.rb +0 -23
  112. data/lib/gooddata/cli/commands/run_ruby_cmd.rb +0 -77
  113. data/lib/gooddata/cli/commands/scaffold_cmd.rb +0 -35
  114. data/lib/gooddata/cli/commands/user_cmd.rb +0 -24
@@ -38,17 +38,12 @@ module GoodData
38
38
 
39
39
  def migrate_datasets(spec, opts = {})
40
40
  opts = { client: GoodData.connection }.merge(opts)
41
- client = opts[:client]
42
41
  dry_run = opts[:dry_run]
43
- fail ArgumentError, 'No :client specified' if client.nil?
44
42
 
45
- p = opts[:project]
46
- fail ArgumentError, 'No :project specified' if p.nil?
43
+ client, project = GoodData.get_client_and_project(opts)
47
44
 
48
- project = client.projects(p)
49
- fail ArgumentError, 'Wrong :project specified' if project.nil?
50
45
  bp = ProjectBlueprint.new(spec)
51
- uri = "/gdc/projects/#{project.pid}/model/diff"
46
+ uri = "/gdc/projects/#{project.pid}/model/diff?includeGrain=true"
52
47
  result = client.post(uri, bp.to_wire)
53
48
 
54
49
  link = result['asyncTask']['link']['poll']
@@ -12,11 +12,7 @@ module GoodData
12
12
  end
13
13
 
14
14
  def [](key, opts = { :client => GoodData.connection, :project => GoodData.project })
15
- client = opts[:client]
16
- fail ArgumentError, 'No :client specified' if client.nil?
17
-
18
- project = opts[:project]
19
- fail ArgumentError, 'No :project specified' if project.nil?
15
+ client, project = GoodData.get_client_and_project(opts)
20
16
 
21
17
  if key == :all
22
18
  uri = "/gdc/projects/#{project.pid}/dataload/metadata"
@@ -43,11 +39,7 @@ module GoodData
43
39
  end
44
40
 
45
41
  def []=(key, opts = { :client => GoodData.connection, :project => GoodData.project }, val = nil)
46
- client = opts[:client]
47
- fail ArgumentError, 'No :client specified' if client.nil?
48
-
49
- project = opts[:project]
50
- fail ArgumentError, 'No :project specified' if project.nil?
42
+ client, project = GoodData.get_client_and_project(opts)
51
43
 
52
44
  data = {
53
45
  :metadataItem => {
@@ -13,16 +13,10 @@ require_relative '../rest/rest'
13
13
  require_relative '../mixins/rest_resource'
14
14
 
15
15
  module GoodData
16
- class ProjectRole < GoodData::Rest::Object
17
- attr_accessor :json
18
-
19
- include GoodData::Mixin::RestResource
20
-
21
- root_key :projectRole
22
-
23
- include GoodData::Mixin::Author
24
- include GoodData::Mixin::Contributor
25
- include GoodData::Mixin::Timestamps
16
+ class ProjectRole < Rest::Resource
17
+ include Mixin::Author
18
+ include Mixin::Contributor
19
+ include Mixin::Timestamps
26
20
 
27
21
  EMPTY_OBJECT = {
28
22
  'projectRole' => {
@@ -5,15 +5,13 @@
5
5
  # LICENSE file in the root directory of this source tree.
6
6
 
7
7
  module GoodData
8
- class ReportDataResult < Rest::Object
8
+ class ReportDataResult < Rest::Resource
9
9
  class << self
10
10
  # Does all the needed parsing on the apyload coming from the API and returns an instance of ReportDataResult
11
11
  #
12
12
  # @param [Hash] data Data coming from the API
13
13
  # @return [GoodData::ReportDataResult] Returns new report data result
14
- def from_xtab(data, options = {})
15
- client = options[:client]
16
- project = options[:project]
14
+ def from_xtab(data)
17
15
  top = top_headers(data)
18
16
  left = left_headers(data)
19
17
  jank = GoodData::Helpers.zeroes(rows(top), cols(left), nil)
@@ -23,7 +21,7 @@ module GoodData
23
21
  a = jank.zip(top).map { |x, y| x + y }
24
22
  b = left.zip(stuff).map { |x, y| x + y }
25
23
  result = a + b
26
- client ? client.create(ReportDataResult, data: result, top: rows(top), left: cols(left), project: project) : ReportDataResult.new(data: result, top: rows(top), left: cols(left))
24
+ ReportDataResult.new(data: result, top: rows(top), left: cols(left))
27
25
  end
28
26
 
29
27
  private
@@ -15,12 +15,6 @@ module GoodData
15
15
  class Schedule < Rest::Resource
16
16
  attr_reader :dirty, :json
17
17
 
18
- alias_method :data, :json
19
- alias_method :raw_data, :json
20
-
21
- include GoodData::Mixin::RestResource
22
- root_key :schedule
23
-
24
18
  SCHEDULE_TEMPLATE = {
25
19
  :schedule => {
26
20
  :type => nil,
@@ -36,14 +30,7 @@ module GoodData
36
30
  # @param id [String] URL, ID of schedule or :all
37
31
  # @return [GoodData::Schedule|Array<GoodData::Schedule>] List of schedules
38
32
  def [](id, opts = { :client => GoodData.connection, :project => GoodData.project })
39
- c = client(opts)
40
- fail ArgumentError, 'No :client specified' if c.nil?
41
-
42
- p = opts[:project]
43
- fail ArgumentError, 'No :project specified' if p.nil?
44
-
45
- project = GoodData::Project[p, opts]
46
- fail ArgumentError, 'Wrong :project specified' if project.nil?
33
+ c, project = GoodData.get_client_and_project(opts)
47
34
 
48
35
  if id == :all
49
36
  GoodData::Schedule.all(opts)
@@ -62,14 +49,7 @@ module GoodData
62
49
  # Returns list of all schedules for active project
63
50
  # @return [Array<GoodData::Schedule>] List of schedules
64
51
  def all(opts = { :client => GoodData.connection, :project => GoodData.project })
65
- c = client(opts)
66
- fail ArgumentError, 'No :client specified' if c.nil?
67
-
68
- p = opts[:project]
69
- fail ArgumentError, 'No :project specified' if p.nil?
70
-
71
- project = GoodData::Project[p, opts]
72
- fail ArgumentError, 'Wrong :project specified' if project.nil?
52
+ c, project = GoodData.get_client_and_project(opts)
73
53
 
74
54
  tmp = c.get "/gdc/projects/#{project.pid}/schedules"
75
55
  tmp['schedules']['items'].map { |schedule| c.create(GoodData::Schedule, schedule, project: project) }
@@ -83,20 +63,13 @@ module GoodData
83
63
  # @param options [Hash] Optional options
84
64
  # @return [GoodData::Schedule] New GoodData::Schedule instance
85
65
  def create(process_id, trigger, executable, options = {})
86
- c = client(options)
87
- fail ArgumentError, 'No :client specified' if c.nil?
88
-
89
- p = options[:project]
90
- fail ArgumentError, 'No :project specified' if p.nil?
91
-
92
- project = GoodData::Project[p, options]
93
- fail ArgumentError, 'Wrong :project specified' if project.nil?
66
+ c, project = GoodData.get_client_and_project(options)
94
67
 
95
68
  fail 'Process ID has to be provided' if process_id.blank?
96
69
  fail 'Executable has to be provided' if executable.blank?
97
70
  fail 'Trigger schedule has to be provided' if trigger.blank?
98
71
 
99
- schedule = c.create(GoodData::Schedule, GoodData::Helpers.deep_stringify_keys(GoodData::Helpers.deep_dup(SCHEDULE_TEMPLATE)), client: c, project: p)
72
+ schedule = c.create(GoodData::Schedule, GoodData::Helpers.deep_stringify_keys(GoodData::Helpers.deep_dup(SCHEDULE_TEMPLATE)), client: c, project: project)
100
73
 
101
74
  default_opts = {
102
75
  :type => 'MSETL',
@@ -0,0 +1,192 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright (c) 2010-2015 GoodData Corporation. All rights reserved.
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ require_relative './client'
8
+ require_relative './domain'
9
+ require_relative '../models/client_synchronization_result'
10
+
11
+ require_relative '../mixins/data_property_reader'
12
+ require_relative '../mixins/links'
13
+ require_relative '../mixins/rest_resource'
14
+ require_relative '../mixins/uri_getter'
15
+
16
+ module GoodData
17
+ class Segment < Rest::Resource
18
+ SYNCHRONIZE_URI = '/gdc/domains/%s/segments/%s/synchronizeClients'
19
+
20
+ attr_accessor :domain
21
+
22
+ data_property_reader 'id'
23
+
24
+ include Mixin::Links
25
+ include Mixin::UriGetter
26
+
27
+ SEGMENT_TEMPLATE = {
28
+ :segment => {
29
+ :id => nil,
30
+ :masterProject => nil
31
+ }
32
+ }
33
+
34
+ class << self
35
+ # Returns list of all segments or a particular segment
36
+ #
37
+ # @param id [String|Symbol] Uri of the segment required or :all for all segments.
38
+ # @return [Array<GoodData::Segment>] List of segments for a particular domain
39
+ def [](id, opts = {})
40
+ domain = opts[:domain]
41
+ fail ArgumentError, 'No :domain specified' if domain.nil?
42
+
43
+ client = domain.client
44
+ fail ArgumentError, 'No client specified' if client.nil?
45
+
46
+ if id == :all
47
+ GoodData::Segment.all(opts)
48
+ else
49
+ result = client.get(domain.segments_uri + "/segments/#{CGI.escape(id)}")
50
+ client.create(GoodData::Segment, result.merge('domain' => domain))
51
+ end
52
+ end
53
+
54
+ # Returns list of all segments for domain
55
+ #
56
+ # @param opts [Hash] Options. Should contain :domain for which you want to get the segments.
57
+ # @return [Array<GoodData::Segment>] List of segments for a particular domain
58
+ def all(opts = {})
59
+ domain = opts[:domain]
60
+ fail 'Domain has to be passed in options' unless domain
61
+ client = domain.client
62
+
63
+ results = client.get(domain.segments_uri + '/segments')
64
+ GoodData::Helpers.get_path(results, %w(segments items)).map { |i| client.create(GoodData::Segment, i.merge('domain' => domain)) }
65
+ end
66
+
67
+ # Creates new segment from parameters passed
68
+ #
69
+ # @param data [Hash] Data for segment namely :segment_id and :master_project is accepted. Master_project can be given as either a PID or a Project instance
70
+ # @param options [Hash] Trigger of schedule. Can be cron string or reference to another schedule.
71
+ # @return [GoodData::Segment] New Segment instance
72
+ def create(data = {}, options = {})
73
+ segment_id = data[:segment_id]
74
+ fail 'Custom ID has to be provided' if segment_id.blank?
75
+ client = options[:client]
76
+ segment = client.create(GoodData::Segment, GoodData::Helpers.deep_stringify_keys(SEGMENT_TEMPLATE).merge('domain' => options[:domain]))
77
+ segment.tap do |s|
78
+ s.segment_id = segment_id
79
+ s.master_project = data[:master_project]
80
+ end
81
+ end
82
+ end
83
+
84
+ def initialize(data)
85
+ super
86
+ @domain = data.delete('domain')
87
+ @json = data
88
+ end
89
+
90
+ # Segment id getter for the Segment. Called segment_id since id is a reserved word in ruby world
91
+ #
92
+ # @return [String] Segment id
93
+ def segment_id
94
+ data['id']
95
+ end
96
+
97
+ # Segment id setter for the Segment. Called segment_id since id is a reserved word in ruby world
98
+ #
99
+ # @param an_id [String] Id of the segment.
100
+ # @return [String] Segment id
101
+ def segment_id=(an_id)
102
+ data['id'] = an_id
103
+ self
104
+ end
105
+
106
+ # Master project id getter for the Segment.
107
+ #
108
+ # @return [String] Segment id
109
+ def master_project=(a_project)
110
+ data['masterProject'] = a_project.respond_to?(:uri) ? a_project.uri : a_project
111
+ self
112
+ end
113
+
114
+ alias_method :master=, :master_project=
115
+
116
+ # Master project id getter for the Segment.
117
+ #
118
+ # @return [String] Project uri
119
+ def master_project_uri
120
+ data['masterProject']
121
+ end
122
+
123
+ alias_method :master_uri, :master_project_uri
124
+
125
+ # Master project getter for the Segment. It returns the instance not just the URI
126
+ #
127
+ # @return [GoodData::Project] Project associated with the segment
128
+ def master_project
129
+ client.projects(master_project_uri)
130
+ end
131
+
132
+ alias_method :master, :master_project
133
+
134
+ def create_client(data)
135
+ client = GoodData::Client.create(data, segment: self)
136
+ client.save
137
+ end
138
+
139
+ # Returns all the clients associated with the segment. Since this is potentially paging operation it returns an Enumerable.
140
+ #
141
+ # @return [Enumerable] Clients associated with the segment
142
+ def clients(tenant_id = :all)
143
+ GoodData::Client[tenant_id, domain: domain, segment: self]
144
+ end
145
+
146
+ # Creates or updates a segment instance on the API.
147
+ #
148
+ # @return [GoodData::Segment] Segment instance
149
+ def save
150
+ if uri
151
+ client.put(uri, json)
152
+ else
153
+ res = client.post(domain.segments_uri + '/segments', json)
154
+ @json = res
155
+ end
156
+ self
157
+ end
158
+
159
+ # Runs async process that walks thorugh segments and provisions projects if necessary.
160
+ #
161
+ # @return [Array] Returns array of results
162
+ def synchronize_clients
163
+ sync_uri = SYNCHRONIZE_URI % [domain.obj_id, id]
164
+ res = client.post sync_uri, nil
165
+
166
+ # wait until the instance is created
167
+ res = client.poll_on_response(res['asyncTask']['links']['poll'], :sleep_interval => 1) do |r|
168
+ r['synchronizationResult'].nil?
169
+ end
170
+
171
+ client.create(ClientSynchronizationResult, res)
172
+ end
173
+
174
+ # Deletes a segment instance on the API.
175
+ #
176
+ # @return [GoodData::Segment] Segment instance
177
+ def delete(options = {})
178
+ force = options[:force] == true ? true : false
179
+ clients.peach(&:delete) if force
180
+ client.delete(uri) if uri
181
+ self
182
+ rescue RestClient::BadRequest => e
183
+ payload = GoodData::Helpers.parse_http_exception(e)
184
+ case GoodData::Helpers.get_path(payload)
185
+ when 'gdc.c4.conflict.domain.segment.contains_clients'
186
+ throw SegmentNotEmpty
187
+ else
188
+ raise e
189
+ end
190
+ end
191
+ end
192
+ end
@@ -21,11 +21,11 @@ module GoodData
21
21
  c = client(options)
22
22
  project = options[:project]
23
23
  filters = query('userFilter', nil, options)
24
- count = 10_000
24
+ count = 1_000
25
25
  offset = 0
26
26
  user_lookup = {}
27
27
  loop do
28
- result = c.get("/gdc/md/#{project.pid}/userfilters?count=1000&offset=#{offset}")
28
+ result = c.get("/gdc/md/#{project.pid}/userfilters?count=#{count}&offset=#{offset}")
29
29
  result['userFilters']['items'].each do |item|
30
30
  item['userFilters'].each do |f|
31
31
  user_lookup[f] = item['user']
@@ -186,7 +186,7 @@ module GoodData
186
186
  # so it precaches the values and still be able to function for larger ones even
187
187
  # though that would mean tons of requests
188
188
  def self.get_small_labels(labels_cache)
189
- labels_cache.values.select { |label| label.values_count < 100_000 }
189
+ labels_cache.values.select { |label| label && label.values_count && label.values_count < 100_000 }
190
190
  end
191
191
 
192
192
  # Creates a MAQL expression(s) based on the filter defintion.
@@ -16,5 +16,16 @@ module GoodData
16
16
  @json[:uri] = res['uri']
17
17
  self
18
18
  end
19
+
20
+ # Method used for replacing values in their state according to mapping. Can be used to replace any values but it is typically used to replace the URIs. Returns a new object of the same type.
21
+ #
22
+ # @param [Array<Array>]Mapping specifying what should be exchanged for what. As mapping should be used output of GoodData::Helpers.prepare_mapping.
23
+ # @return [GoodData::VariableUserFilter]
24
+ def replace(mapping)
25
+ x = GoodData::MdObject.replace_quoted(self, mapping)
26
+ x = GoodData::MdObject.replace_bracketed(x, mapping)
27
+ vals = GoodData::MdObject.find_replaceable_values(x, mapping)
28
+ GoodData::MdObject.replace_bracketed(x, vals)
29
+ end
19
30
  end
20
31
  end
@@ -0,0 +1,241 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright (c) 2010-2015 GoodData Corporation. All rights reserved.
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ require_relative '../rest/rest'
8
+ require_relative '../rest/resource'
9
+ require_relative '../mixins/author'
10
+ require_relative '../mixins/contributor'
11
+ require_relative '../mixins/links'
12
+ require_relative '../mixins/rest_resource'
13
+ require_relative '../mixins/uri_getter'
14
+
15
+ module GoodData
16
+ # Representation of User Group
17
+ #
18
+ # Use user groups to manage user access to dashboards on the GoodData Portal.
19
+ # Create groups to more quickly manage permissions for users with
20
+ # the the same role or who need similar access to dashboards.
21
+ # Groups can be part of groups.
22
+ class UserGroup < Rest::Resource
23
+ include Mixin::Author
24
+ include Mixin::Contributor
25
+ include Mixin::Links
26
+ include Mixin::UriGetter
27
+
28
+ EMPTY_OBJECT = {
29
+ 'userGroup' => {
30
+ 'content' => {
31
+ 'name' => nil,
32
+ 'description' => nil,
33
+ 'project' => nil
34
+ }
35
+ }
36
+ }
37
+
38
+ class << self
39
+ # Returns list of all segments or a particular segment
40
+ #
41
+ # @param id [String|Symbol] Uri of the segment required or :all for all segments.
42
+ # @return [Array<GoodData::Segment>] List of segments for a particular domain
43
+ def [](id, opts = {})
44
+ # TODO: Replace with GoodData.get_client_and_project(opts)
45
+ project = opts[:project]
46
+ fail 'Project has to be passed in options' unless project
47
+ fail 'Project has to be of type GoodData::Project' unless project.is_a?(GoodData::Project)
48
+ client = project.client
49
+
50
+ results = client.get('/gdc/userGroups', params: { :project => project.pid, :user => opts[:user] }.compact)
51
+ groups = GoodData::Helpers.get_path(results, %w(userGroups items)).map { |i| client.create(GoodData::UserGroup, i, :project => project) }
52
+ id == :all ? groups : groups.find { |g| g.obj_id == id || g.name == id }
53
+ end
54
+
55
+ # Create new user group
56
+ #
57
+ # @param data [Hash] Initial data
58
+ # @return [UserGroup] Newly created user group
59
+ def create(data)
60
+ new_data = GoodData::Helpers.deep_dup(EMPTY_OBJECT).tap do |d|
61
+ d['userGroup']['content']['name'] = data[:name]
62
+ d['userGroup']['content']['description'] = data[:description]
63
+ d['userGroup']['content']['project'] = data[:project].respond_to?(:uri) ? data[:project].uri : data[:project]
64
+ end
65
+
66
+ client.create(GoodData::UserGroup, GoodData::Helpers.deep_stringify_keys(new_data))
67
+ end
68
+
69
+ # Constructs payload for user management/manipulation
70
+ #
71
+ # @return [Hash] Created payload
72
+ def construct_payload(users, operation)
73
+ users = users.is_a?(Array) ? users : [users]
74
+
75
+ {
76
+ modifyMembers: {
77
+ operation: operation,
78
+ items: users.map do |user|
79
+ uri = user.respond_to?(:uri) ? user.uri : user
80
+ fail 'You cannot add group as member of another group as of now.' if uri =~ %r{^\/gdc\/userGroups\/}
81
+ uri
82
+ end
83
+ }
84
+ }
85
+ end
86
+
87
+ # URI used for membership manipulation/managementv
88
+ #
89
+ # @param client [Client] Client used for communication with platform
90
+ # @param users [User | String | Array<User> | Array<String>] User(s) to be modified
91
+ # @param operation [String] Operation to be performed - ADD, SET, REMOVE
92
+ # @param uri [String] URI to be used for operation
93
+ # @return [String] URI used for membership manipulation/management
94
+ def modify_users(client, users, operation, uri)
95
+ payload = construct_payload(users, operation)
96
+ client.post(uri, payload)
97
+ end
98
+ end
99
+
100
+ # Initialize object with json
101
+ #
102
+ # @return [UserGroup] User Group object initialized with json
103
+ def initialize(json)
104
+ @json = json
105
+ self
106
+ end
107
+
108
+ # Add member(s) to user group
109
+ #
110
+ # @param [String | User | Array<User>] Users to add to user group
111
+ # @return [nil] Nothing is returned
112
+ def add_members(user)
113
+ UserGroup.modify_users(client, user, 'ADD', uri_modify_members)
114
+ end
115
+
116
+ alias_method :add_member, :add_members
117
+
118
+ # Gets user group name
119
+ #
120
+ # @return [String] User group name
121
+ def name
122
+ content['name']
123
+ end
124
+
125
+ # Sets user group name
126
+ #
127
+ # @param name [String] New user group name
128
+ # @return [String] New user group name
129
+ def name=(name)
130
+ content['name'] = name
131
+ name
132
+ end
133
+
134
+ # Gets user group description
135
+ #
136
+ # @return [String] User group description
137
+ def description
138
+ content['description']
139
+ end
140
+
141
+ # Sets user group description
142
+ #
143
+ # @param name [String] New user group description
144
+ # @return [String] New user group description
145
+ def description=(name)
146
+ content['description'] = name
147
+ end
148
+
149
+ # Gets Users with this Role
150
+ #
151
+ # @return [Array<GoodData::Profile>] List of users
152
+ def members
153
+ url = GoodData::Helpers.get_path(data, %w(links members))
154
+ return [] unless url
155
+ Enumerator.new do |y|
156
+ loop do
157
+ res = client.get url
158
+ res['userGroupMembers']['paging']['next']
159
+ res['userGroupMembers']['items'].each do |member|
160
+ case member.keys.first
161
+ when 'user'
162
+ y << client.create(GoodData::Profile, client.get(GoodData::Helpers.get_path(member, %w(user links self))), :project => project)
163
+ when 'userGroup'
164
+ y << client.create(UserGroup, client.get(GoodData::Helpers.get_path(member, %w(userGroup links self))), :project => project)
165
+ end
166
+ end
167
+ url = res['userGroupMembers']['paging']['next']
168
+ break unless url
169
+ end
170
+ end
171
+ end
172
+
173
+ # Verifies if user is in a group or any nested group and returns true if it does
174
+ #
175
+ # @return [Boolean] Retruns true if member is member of the group or any of its members
176
+ def member?(a_member)
177
+ # could be better on API directly?
178
+ uri = a_member.respond_to?(:uri) ? a_member.uri : a_member
179
+ members.map(&:uri).include?(uri)
180
+ end
181
+
182
+ # Save user group
183
+ # New group is created if needed else existing one is updated
184
+ #
185
+ # @return [UserGroup] Created or updated user group
186
+ def save
187
+ res = if uri
188
+ # get rid of unsupprted keys
189
+ data = json['userGroup']
190
+ client.put(uri, 'userGroup' => data.except('meta', 'links'))
191
+ else
192
+ client.post('/gdc/userGroups', @json)
193
+ end
194
+ @json = client.get(res['uri'])
195
+ self
196
+ end
197
+
198
+ # Remove member(s) from user group
199
+ #
200
+ # @param [String | User | Array<User>] Users to remove from user group
201
+ # @return [nil] Nothing is returned
202
+ def remove_members(user)
203
+ UserGroup.modify_users(client, user, 'REMOVE', uri_modify_members)
204
+ end
205
+
206
+ alias_method :remove_member, :remove_members
207
+
208
+ # Set member(s) to user group.
209
+ # Only users passed to this call will be new members of user group.
210
+ # Old members not passed to this method will be removed!
211
+ #
212
+ # @param [String | User | Array<User>] Users to set as members of user group
213
+ # @return [nil] Nothing is returned
214
+ def set_members(user) # rubocop:disable Style/AccessorMethodName
215
+ UserGroup.modify_users(client, user, 'SET', uri_modify_members)
216
+ end
217
+
218
+ alias_method :set_member, :set_members
219
+
220
+ # URI used for membership manipulation/management
221
+ #
222
+ # @return [String] URI used for membership manipulation/management
223
+ def uri_modify_members
224
+ links['modifyMembers']
225
+ end
226
+
227
+ # Is it a user group?
228
+ #
229
+ # @return [Boolean] Return true if it is a user group
230
+ def user_group?
231
+ true
232
+ end
233
+
234
+ # Checks if two user groups are same
235
+ #
236
+ # @return [Boolean] Return true if the two groups are same
237
+ def ==(other)
238
+ uri == other.uri
239
+ end
240
+ end
241
+ end