gooddata 0.6.11 → 0.6.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/.travis.yml +5 -0
  4. data/CHANGELOG.md +34 -1
  5. data/CLI.md +1 -1
  6. data/authors.sh +4 -0
  7. data/lib/gooddata.rb +1 -1
  8. data/lib/gooddata/cli/commands/api_cmd.rb +0 -2
  9. data/lib/gooddata/cli/commands/auth_cmd.rb +0 -3
  10. data/lib/gooddata/cli/commands/console_cmd.rb +1 -2
  11. data/lib/gooddata/cli/commands/domain_cmd.rb +0 -2
  12. data/lib/gooddata/cli/commands/process_cmd.rb +0 -2
  13. data/lib/gooddata/cli/commands/project_cmd.rb +0 -2
  14. data/lib/gooddata/cli/commands/projects_cmd.rb +0 -2
  15. data/lib/gooddata/cli/commands/run_ruby_cmd.rb +2 -3
  16. data/lib/gooddata/cli/commands/scaffold_cmd.rb +0 -3
  17. data/lib/gooddata/cli/commands/user_cmd.rb +0 -2
  18. data/lib/gooddata/cli/shared.rb +1 -2
  19. data/lib/gooddata/commands/datawarehouse.rb +24 -0
  20. data/lib/gooddata/commands/process.rb +0 -1
  21. data/lib/gooddata/commands/project.rb +1 -1
  22. data/lib/gooddata/commands/scaffold.rb +0 -1
  23. data/lib/gooddata/core/connection.rb +376 -0
  24. data/lib/gooddata/core/logging.rb +13 -0
  25. data/lib/gooddata/core/rest.rb +40 -16
  26. data/lib/gooddata/exceptions/user_in_different_domain.rb +11 -0
  27. data/lib/gooddata/extensions/enumerable.rb +8 -0
  28. data/lib/gooddata/goodzilla/goodzilla.rb +24 -0
  29. data/lib/gooddata/helpers/global_helpers.rb +126 -12
  30. data/lib/gooddata/mixins/author.rb +11 -5
  31. data/lib/gooddata/mixins/is_dimension.rb +13 -0
  32. data/lib/gooddata/mixins/md_object_indexer.rb +17 -1
  33. data/lib/gooddata/mixins/md_object_query.rb +10 -2
  34. data/lib/gooddata/mixins/md_relations.rb +2 -2
  35. data/lib/gooddata/mixins/rest_resource.rb +1 -0
  36. data/lib/gooddata/models/data_result.rb +0 -1
  37. data/lib/gooddata/models/datawarehouse.rb +90 -0
  38. data/lib/gooddata/models/domain.rb +202 -76
  39. data/lib/gooddata/models/execution.rb +11 -0
  40. data/lib/gooddata/models/from_wire.rb +4 -4
  41. data/lib/gooddata/models/invitation.rb +0 -5
  42. data/lib/gooddata/models/membership.rb +121 -91
  43. data/lib/gooddata/models/metadata.rb +1 -2
  44. data/lib/gooddata/models/metadata/attribute.rb +7 -0
  45. data/lib/gooddata/models/metadata/dashboard.rb +1 -1
  46. data/lib/gooddata/models/metadata/dimension.rb +52 -0
  47. data/lib/gooddata/models/metadata/fact.rb +1 -1
  48. data/lib/gooddata/models/metadata/label.rb +21 -7
  49. data/lib/gooddata/models/metadata/metric.rb +1 -23
  50. data/lib/gooddata/models/metadata/report.rb +2 -2
  51. data/lib/gooddata/models/metadata/report_definition.rb +22 -2
  52. data/lib/gooddata/models/metadata/variable.rb +81 -0
  53. data/lib/gooddata/models/model.rb +2 -1
  54. data/lib/gooddata/models/process.rb +3 -4
  55. data/lib/gooddata/models/profile.rb +50 -82
  56. data/lib/gooddata/models/project.rb +170 -213
  57. data/lib/gooddata/models/project_blueprint.rb +14 -5
  58. data/lib/gooddata/models/project_creator.rb +2 -2
  59. data/lib/gooddata/models/schedule.rb +10 -8
  60. data/lib/gooddata/models/to_wire.rb +2 -2
  61. data/lib/gooddata/models/user_filters/mandatory_user_filter.rb +67 -0
  62. data/lib/gooddata/models/user_filters/user_filter.rb +96 -0
  63. data/lib/gooddata/models/user_filters/user_filter_builder.rb +409 -0
  64. data/lib/gooddata/{rest/connections/connections.rb → models/user_filters/user_filters.rb} +1 -0
  65. data/lib/gooddata/models/user_filters/variable_user_filter.rb +14 -0
  66. data/lib/gooddata/rest/client.rb +32 -21
  67. data/lib/gooddata/rest/connection.rb +283 -11
  68. data/lib/gooddata/rest/connections/rest_client_connection.rb +47 -109
  69. data/lib/gooddata/version.rb +1 -1
  70. data/spec/data/column_based_permissions.csv +7 -0
  71. data/spec/data/column_based_permissions2.csv +6 -0
  72. data/spec/data/hello_world_process/hello_world.rb +3 -1
  73. data/spec/data/line_based_permissions.csv +3 -0
  74. data/spec/data/m_n_model/blueprint.json +76 -0
  75. data/spec/data/{model_view.json → wire_models/model_view.json} +0 -0
  76. data/spec/data/wire_models/nu_model.json +3046 -0
  77. data/spec/helpers/process_helper.rb +2 -2
  78. data/spec/helpers/project_helper.rb +29 -0
  79. data/spec/helpers/schedule_helper.rb +1 -1
  80. data/spec/integration/command_datawarehouse_spec.rb +32 -0
  81. data/spec/integration/create_project_spec.rb +0 -1
  82. data/spec/integration/full_process_schedule_spec.rb +13 -5
  83. data/spec/integration/full_project_spec.rb +2 -1
  84. data/spec/integration/over_to_user_filters_spec.rb +92 -0
  85. data/spec/integration/project_spec.rb +233 -0
  86. data/spec/integration/rest_spec.rb +209 -0
  87. data/spec/integration/user_filters_spec.rb +193 -0
  88. data/spec/integration/variables_spec.rb +196 -0
  89. data/spec/unit/commands/command_auth_spec.rb +0 -7
  90. data/spec/unit/commands/command_process_spec.rb +10 -13
  91. data/spec/unit/core/connection_spec.rb +0 -19
  92. data/spec/unit/helpers/global_helpers_spec.rb +57 -0
  93. data/spec/unit/models/domain_spec.rb +80 -40
  94. data/spec/unit/models/from_wire_spec.rb +8 -1
  95. data/spec/unit/models/params_spec.rb +6 -6
  96. data/spec/unit/models/profile_spec.rb +23 -22
  97. data/spec/unit/models/project_blueprint_spec.rb +1 -6
  98. data/spec/unit/models/project_spec.rb +331 -286
  99. data/spec/unit/models/schedule_spec.rb +39 -14
  100. data/spec/unit/models/user_filters_spec.rb +89 -0
  101. data/spec/unit/models/variable_spec.rb +259 -0
  102. metadata +31 -7
  103. data/lib/gooddata/rest/connections/dummy_connection.rb +0 -52
  104. data/spec/unit/core/rest_spec.rb +0 -106
@@ -88,17 +88,18 @@ module GoodData
88
88
  # Returns dataset specified. It can check even for a date dimension
89
89
  #
90
90
  # @param project [GoodData::Model::ProjectBlueprint | Hash] Project blueprint
91
- # @param name [GoodData::Model::DatasetBlueprint | String | Hash] Dataset
91
+ # @param obj [GoodData::Model::DatasetBlueprint | String | Hash] Dataset
92
92
  # @param options [Hash] options
93
93
  # @return [GoodData::Model::DatasetBlueprint]
94
- def self.find_dataset(project, name, options = {})
94
+ def self.find_dataset(project, obj, options = {})
95
95
  include_date_dimensions = options[:include_date_dimensions] || options[:dd]
96
- return name.to_hash if DatasetBlueprint.dataset_blueprint?(name)
96
+ return obj.to_hash if DatasetBlueprint.dataset_blueprint?(obj)
97
97
  all_datasets = if include_date_dimensions
98
98
  datasets(project) + date_dimensions(project)
99
99
  else
100
100
  datasets(project)
101
101
  end
102
+ name = obj.respond_to?(:key?) ? obj[:name] : obj
102
103
  ds = all_datasets.find { |d| d[:name] == name }
103
104
  fail "Dataset #{name} could not be found" if ds.nil?
104
105
  ds
@@ -290,7 +291,7 @@ module GoodData
290
291
  # @param project [GoodData::Model::DatasetBlueprint | Hash | String] Dataset blueprint
291
292
  # @return [Array<Hash>]
292
293
  def referenced_by(dataset)
293
- find_dataset(dataset).references.map do |ref|
294
+ find_dataset(dataset, include_date_dimensions: true).references.map do |ref|
294
295
  find_dataset(ref[:dataset], include_date_dimensions: true)
295
296
  end
296
297
  end
@@ -360,7 +361,6 @@ module GoodData
360
361
  anchor_name: dataset.anchor[:name]
361
362
  }
362
363
  end
363
-
364
364
  date_facts = datasets.mapcat(&:date_facts)
365
365
  date_facts.each do |date_fact|
366
366
  errors << {
@@ -463,6 +463,7 @@ module GoodData
463
463
  # @return [GoodData::Model::ProjectBlueprint]
464
464
  def merge(a_blueprint)
465
465
  temp_blueprint = dup
466
+ return temp_blueprint unless a_blueprint
466
467
  a_blueprint.datasets.each do |dataset|
467
468
  if temp_blueprint.dataset?(dataset.name)
468
469
  local_dataset = temp_blueprint.find_dataset(dataset.name)
@@ -525,6 +526,14 @@ module GoodData
525
526
  def to_hash
526
527
  @data
527
528
  end
529
+
530
+ def ==(other)
531
+ to_hash == other.to_hash
532
+ end
533
+
534
+ def eql?(other)
535
+ to_hash == other.to_hash
536
+ end
528
537
  end
529
538
  end
530
539
  end
@@ -9,7 +9,7 @@ module GoodData
9
9
  module Model
10
10
  class ProjectCreator
11
11
  class << self
12
- def migrate(opts = { :client => GoodData.connection, :project => GoodData.project })
12
+ def migrate(opts = { client: GoodData.connection, project: GoodData.project })
13
13
  client = opts[:client]
14
14
  fail ArgumentError, 'No :client specified' if client.nil?
15
15
 
@@ -66,7 +66,7 @@ module GoodData
66
66
  # pp response
67
67
  while response.code != 200
68
68
  sleep 1
69
- GoodData::Rest::Client.retryable(:tries => 3, :on => RestClient::InternalServerError) do
69
+ GoodData::Rest::Client.retryable(:tries => 3) do
70
70
  sleep 1
71
71
  response = client.get(link, :process => false)
72
72
  # pp response
@@ -59,7 +59,7 @@ module GoodData
59
59
  fail ArgumentError, 'Wrong :project specified' if project.nil?
60
60
 
61
61
  tmp = c.get "/gdc/projects/#{project.pid}/schedules"
62
- tmp['schedules']['items'].map { |schedule| c.create(GoodData::Schedule, schedule) }
62
+ tmp['schedules']['items'].map { |schedule| c.create(GoodData::Schedule, schedule, project: project) }
63
63
  end
64
64
 
65
65
  # Creates new schedules from parameters passed
@@ -190,16 +190,18 @@ module GoodData
190
190
 
191
191
  # Executes schedule
192
192
  #
193
+ # @param [Hash] opts execution options.
194
+ # @option opts [Boolean] :wait Wait for execution result
193
195
  # @return [Object] Raw Response
194
- def execute
196
+ def execute(opts = { :wait => true })
195
197
  data = {
196
198
  :execution => {}
197
199
  }
198
- execution = client.post(execution_url, data)
199
- res = client.poll_on_response(execution['execution']['links']['self']) do |body|
200
- body['execution'] && (body['execution']['status'] == 'RUNNING' || body['execution']['status'] == 'SCHEDULED')
201
- end
202
- client.create(GoodData::Execution, res, client: client, project: project)
200
+ res = client.post(execution_url, data)
201
+ execution = client.create(GoodData::Execution, res, client: client, project: project)
202
+
203
+ return execution unless opts[:wait]
204
+ execution.wait_for_result
203
205
  end
204
206
 
205
207
  # Returns execution URL
@@ -360,7 +362,7 @@ module GoodData
360
362
  'reschedule' => @json['schedule']['reschedule'] || 0
361
363
  }
362
364
  }
363
- res = GoodData.put uri, update_json
365
+ res = client.put(uri, update_json)
364
366
  @json = res
365
367
  @dirty = false
366
368
  return true
@@ -138,8 +138,8 @@ module GoodData
138
138
  diffRequest: {
139
139
  targetModel: {
140
140
  projectModel: {
141
- datasets: what[:datasets].map { |d| dataset_to_wire(what, d) },
142
- dateDimensions: what[:date_dimensions].map { |d| date_dimensions_to_wire(what, d) }
141
+ datasets: (what[:datasets] || []).map { |d| dataset_to_wire(what, d) },
142
+ dateDimensions: (what[:date_dimensions] || []).map { |d| date_dimensions_to_wire(what, d) }
143
143
  }
144
144
  }
145
145
  }
@@ -0,0 +1,67 @@
1
+ # encoding: UTF-8
2
+
3
+ require_relative 'user_filter'
4
+
5
+ module GoodData
6
+ class MandatoryUserFilter < UserFilter
7
+ class << self
8
+ def [](id, options = { client: GoodData.connection, project: GoodData.project })
9
+ if id == :all
10
+ all(options)
11
+ else
12
+ super
13
+ end
14
+ end
15
+
16
+ def all(options = { client: GoodData.connection, project: GoodData.project })
17
+ c = client(options)
18
+ project = options[:project]
19
+ vars = c.get(project.md['query'] + '/userfilters/')['query']['entries']
20
+ count = 10_000
21
+ offset = 0
22
+ user_lookup = {}
23
+ loop do
24
+ result = c.get("/gdc/md/#{project.pid}/userfilters?count=1000&offset=#{offset}")
25
+ result['userFilters']['items'].each do |item|
26
+ item['userFilters'].each do |f|
27
+ user_lookup[f] = item['user']
28
+ end
29
+ end
30
+ break if result['userFilters']['length'] < offset
31
+ offset += count
32
+ end
33
+ vars.map do |a|
34
+ uri = a['link']
35
+ data = c.get(uri)
36
+ payload = {
37
+ 'expression' => data['userFilter']['content']['expression'],
38
+ 'related' => user_lookup[a['link']],
39
+ 'level' => :user,
40
+ 'type' => :filter,
41
+ 'uri' => a['link']
42
+ }
43
+ c.create(GoodData::MandatoryUserFilter, payload, project: project)
44
+ end
45
+ end
46
+ end
47
+
48
+ # Creates or updates the mandatory user filter on the server
49
+ #
50
+ # @return [GoodData::MandatoryUserFilter]
51
+ def save
52
+ data = {
53
+ 'userFilter' => {
54
+ 'content' => {
55
+ 'expression' => expression
56
+ },
57
+ 'meta' => {
58
+ 'category' => 'userFilter',
59
+ 'title' => related_uri
60
+ }
61
+ }
62
+ }
63
+ res = client.post(project.md['obj'], data)
64
+ @json['uri'] = res['uri']
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,96 @@
1
+ # encoding: UTF-8
2
+
3
+ module GoodData
4
+ class UserFilter < GoodData::MdObject
5
+ def initialize(data)
6
+ @dirty = false
7
+ @json = data
8
+ end
9
+
10
+ def ==(other)
11
+ other.class == self.class && other.related_uri == related_uri && other.expression == expression
12
+ end
13
+ alias_method :eql?, :==
14
+
15
+ def hash
16
+ [related_uri, expression].hash
17
+ end
18
+
19
+ # Returns the uri of the object this filter is related to. It can be either project or a user
20
+ #
21
+ # @return [String] Uri of related object
22
+ def related_uri
23
+ @json['related']
24
+ end
25
+
26
+ # Returns the the object of this filter is related to. It can be either project or a user
27
+ #
28
+ # @return [GoodData::Project | GoodData::Profile] Related object
29
+ def related
30
+ uri = related_uri
31
+ level == :project ? client.projects(uri) : client.create(GoodData::Profile, client.get(uri))
32
+ end
33
+
34
+ # Returns the the object of this filter is related to. It can be either project or a user
35
+ #
36
+ # @return [GoodData::Project | GoodData::Profile] Related object
37
+ def variable
38
+ uri = @json['prompt']
39
+ GoodData::Variable[uri, client: client, project: project]
40
+ end
41
+
42
+ # Returns the level this filter is applied on. Either project or user. This is useful for
43
+ # variables where you can have both types. Project level is the default that is applied when
44
+ # user does not have assigned a value. When both user and project value and user value is missing
45
+ # value, you will get 'uncomputable report' errors.
46
+ #
47
+ # @return [Symbol] level on which this filter will be applied
48
+ def level
49
+ @json['level'].to_sym
50
+ end
51
+
52
+ # Returns the MAQL expression of the filter
53
+ #
54
+ # @return [String] MAQL expression
55
+ def expression
56
+ @json['expression']
57
+ end
58
+
59
+ # Allows to set the MAQL expression of the filter
60
+ #
61
+ # @param expression [String] MAQL expression
62
+ # @return [String] MAQL expression
63
+ def expression=(expression)
64
+ @dirty = true
65
+ @json['expression'] = expression
66
+ end
67
+
68
+ # Gives you URI of the filter
69
+ #
70
+ # @return [String]
71
+ def uri
72
+ @json['uri']
73
+ end
74
+
75
+ # Allows to set URI of the filter
76
+ #
77
+ # @return [String]
78
+ def uri=(uri)
79
+ @json['uri'] = uri
80
+ end
81
+
82
+ # Returns pretty version of the expression
83
+ #
84
+ # @return [String]
85
+ def pretty_expression
86
+ SmallGoodZilla.pretty_print(expression, client: client, project: project)
87
+ end
88
+
89
+ # Deletes the filter from the server
90
+ #
91
+ # @return [String]
92
+ def delete
93
+ client.delete(uri)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,409 @@
1
+ # encoding: UTF-8
2
+
3
+ module GoodData
4
+ module UserFilterBuilder
5
+ # Main Entry function. Gets values and processes them to get filters
6
+ # that are suitable for other function to process.
7
+ # Values can be read from file or provided inline as an array.
8
+ # The results are then preprocessed. It is possible to provide
9
+ # multiple values for an attribute tries to deduplicate the values if
10
+ # they are not unique. Allows for setting over/to filters and allows for
11
+ # setting up filters from multiple columns. It is specially designed so many
12
+ # aspects of configuration are modifiable so you do have to preprocess the
13
+ # data as little as possible ideally you should be able to use data that
14
+ # came directly from the source system and that are intended for use in
15
+ # other parts of ETL.
16
+ #
17
+ # @param options [Hash]
18
+ # @return [Boolean]
19
+ def self.get_filters(file, options = {})
20
+ values = get_values(file, options)
21
+ reduce_results(values)
22
+ end
23
+
24
+ # Function that tells you if the file should be read line_wise. This happens
25
+ # if you have only one label defined and you do not have columns specified
26
+ #
27
+ # @param options [Hash]
28
+ # @return [Boolean]
29
+ def self.row_based?(options = {})
30
+ options[:labels].count == 1 && !options[:labels].first.key?(:column)
31
+ end
32
+
33
+ def self.read_file(file, options = {})
34
+ memo = {}
35
+ params = row_based?(options) ? { headers: false } : { headers: true }
36
+ CSV.foreach(file, params.merge(return_headers: false)) do |e|
37
+ key, data = process_line(e, options)
38
+ memo[key] = [] unless memo.key?(key)
39
+ memo[key].concat(data)
40
+ end
41
+ memo
42
+ end
43
+
44
+ # Processes a line from source file. It is processed in
45
+ # 2 formats. First mode is column_based.
46
+ # It means getting all specific columns.
47
+ # These are specified either by index or name. Multiple
48
+ # values are provided by several rows for the same user
49
+ #
50
+ # Second mode is row based which means there are no headers
51
+ # and number of columns can be variable. Each row specifies multiple
52
+ # values for one user. It is implied that the file provides values
53
+ # for just one label
54
+ #
55
+ # @param options [Hash]
56
+ # @return
57
+ def self.process_line(line, options = {})
58
+ index = options[:user_column] || 0
59
+ login = line[index]
60
+
61
+ results = options[:labels].mapcat do |label|
62
+ column = label[:column] || Range.new(1, -1)
63
+ values = column.is_a?(Range) ? line.slice(column) : [line[column]]
64
+ [create_filter(label, values.compact)]
65
+ end
66
+ [login, results]
67
+ end
68
+
69
+ def self.create_filter(label, values)
70
+ {
71
+ :label => label[:label],
72
+ :values => values,
73
+ :over => label[:over],
74
+ :to => label[:to]
75
+ }
76
+ end
77
+
78
+ # Processes values in a map reduce way so the result is as readable as possible and
79
+ # poses minimal impact on the API
80
+ #
81
+ # @param options [Hash]
82
+ # @return [Array]
83
+ def self.reduce_results(data)
84
+ data.map { |k, v| { login: k, filters: UserFilterBuilder.collect_labels(v) } }
85
+ end
86
+
87
+ # Groups the values by particular label. And passes each group to deduplication
88
+ # @param options [Hash]
89
+ # @return
90
+ def self.collect_labels(data)
91
+ data.group_by { |x| [x[:label], x[:over], x[:to]] }.map { |l, v| { label: l[0], over: l[1], to: l[2], values: UserFilterBuilder.collect_values(v) } }
92
+ end
93
+
94
+ # Collects specific values and deduplicates if necessary
95
+ def self.collect_values(data)
96
+ data.mapcat do |e|
97
+ e[:values]
98
+ end.uniq
99
+ end
100
+
101
+ def self.create_cache(data, key)
102
+ data.reduce({}) do |a, e|
103
+ a[e.send(key)] = e
104
+ a
105
+ end
106
+ end
107
+
108
+ def self.verify_existing_users(filters, options = { project: GoodData.project, client: GoodData.connection })
109
+ project = options[:project]
110
+
111
+ users_must_exist = options[:users_must_exist] == false ? false : true
112
+ users_cache = options[:users_cache] || create_cache(project.users, :login)
113
+
114
+ if users_must_exist
115
+ list = users_cache.values
116
+ missing_users = filters.map { |x| x[:login] }.reject { |u| project.member?(u, list) }
117
+ fail "#{missing_users.count} users are not part of the project and variable cannot be resolved since :users_must_exist is set to true (#{missing_users.join(', ')})" unless missing_users.empty?
118
+ end
119
+ end
120
+
121
+ def self.create_label_cache(result, options = { project: GoodData.project, client: GoodData.connection })
122
+ project = options[:project]
123
+
124
+ result.reduce({}) do |a, e|
125
+ e[:filters].map do |filter|
126
+ a[filter[:label]] = project.labels(filter[:label]) unless a.key?(filter[:label])
127
+ end
128
+ a
129
+ end
130
+ end
131
+
132
+ def self.create_lookups_cache(small_labels)
133
+ small_labels.reduce({}) do |a, e|
134
+ lookup = e.values(:limit => 1_000_000).reduce({}) do |a1, e1|
135
+ a1[e1[:value]] = e1[:uri]
136
+ a1
137
+ end
138
+ a[e.uri] = lookup
139
+ a
140
+ end
141
+ end
142
+
143
+ # Walks over provided labels and picks those that have fewer than certain amount of values
144
+ # This tries to balance for speed when working with small datasets (like users)
145
+ # so it precaches the values and still be able to function for larger ones even
146
+ # though that would mean tons of requests
147
+ def self.get_small_labels(labels_cache)
148
+ labels_cache.values.select { |label| label.values_count < 100_000 }
149
+ end
150
+
151
+ # Creates a MAQL expression(s) based on the filter defintion.
152
+ # Takes the filter definition looks up any necessary values and provides API executable MAQL
153
+ def self.create_expression(filter, labels_cache, lookups_cache, options = {})
154
+ errors = []
155
+ values = filter[:values]
156
+ label = labels_cache[filter[:label]]
157
+ element_uris = values.map do |v|
158
+ begin
159
+ if lookups_cache.key?(label.uri)
160
+ if lookups_cache[label.uri].key?(v)
161
+ lookups_cache[label.uri][v]
162
+ else
163
+ fail
164
+ end
165
+ else
166
+ label.find_value_uri(v)
167
+ end
168
+ rescue
169
+ errors << [label, v]
170
+ nil
171
+ end
172
+ end
173
+ expression = if element_uris.compact.empty? && options[:restrict_if_missing_all_values] && options[:type] == :muf
174
+ '1 <> 1'
175
+ elsif element_uris.compact.empty? && options[:restrict_if_missing_all_values] && options[:type] == :variable
176
+ nil
177
+ elsif element_uris.compact.empty?
178
+ 'TRUE'
179
+ elsif filter[:over] && filter[:to]
180
+ "([#{label.attribute_uri}] IN (#{ element_uris.compact.sort.map { |e| '[' + e + ']' }.join(', ') })) OVER [#{filter[:over]}] TO [#{filter[:to]}]"
181
+ else
182
+ "[#{label.attribute_uri}] IN (#{ element_uris.compact.sort.map { |e| '[' + e + ']' }.join(', ') })"
183
+ end
184
+ [expression, errors]
185
+ end
186
+
187
+ # Encapuslates the creation of filter
188
+ def self.create_user_filter(expression, related)
189
+ {
190
+ 'related' => related,
191
+ 'level' => :user,
192
+ 'expression' => expression,
193
+ 'type' => :filter
194
+ }
195
+ end
196
+
197
+ # Resolves and creates maql statements from filter definitions.
198
+ # This method does not perform any modifications on API but
199
+ # collects all the information that is needed to do so.
200
+ # Method collects all info from the user and current state in project and compares.
201
+ # Returns suggestion of what should be deleted and what should be created
202
+ # If there is some discrepancies in the data (missing values, nonexistent users) it
203
+ # finishes and collects all the errors at once
204
+ #
205
+ # @param filters [Array<Hash>] Filters definition
206
+ # @return [Array] first is list of MAQL statements
207
+ def self.maqlify_filters(filters, options = { project: GoodData.project, client: GoodData.connection })
208
+ project = options[:project]
209
+ users_cache = options[:users_cache] || create_cache(project.users, :login)
210
+ labels_cache = create_label_cache(filters, options)
211
+ small_labels = get_small_labels(labels_cache)
212
+ lookups_cache = create_lookups_cache(small_labels)
213
+
214
+ errors = []
215
+ results = filters.mapcat do |filter|
216
+ login = filter[:login]
217
+ filter[:filters].mapcat do |f|
218
+ expression, error = create_expression(f, labels_cache, lookups_cache, options)
219
+ errors << error unless error.empty?
220
+ profiles_uri = (users_cache[login] && users_cache[login].uri)
221
+ if profiles_uri && expression
222
+ [create_user_filter(expression, profiles_uri)]
223
+ else
224
+ []
225
+ end
226
+ end
227
+ end
228
+ [results, errors]
229
+ end
230
+
231
+ def self.resolve_user_filter(user = [], project = [])
232
+ user ||= []
233
+ project ||= []
234
+ to_create = user - project
235
+ to_delete = project - user
236
+ { :create => to_create, :delete => to_delete }
237
+ end
238
+
239
+ # Gets user defined filters and values from project regardless if they
240
+ # come from Mandatory Filters or Variable filters and tries to
241
+ # resolve what needs to be removed an what needs to be updated
242
+ def self.resolve_user_filters(user_filters, vals)
243
+ project_vals_lookup = vals.group_by(&:related_uri)
244
+ user_vals_lookup = user_filters.group_by(&:related_uri)
245
+
246
+ a = vals.map { |x| [x.related_uri, x] }
247
+ b = user_filters.map { |x| [x.related_uri, x] }
248
+
249
+ users_to_try = a.map(&:first).concat(b.map(&:first)).uniq
250
+ results = users_to_try.map do |user|
251
+ resolve_user_filter(user_vals_lookup[user], project_vals_lookup[user])
252
+ end
253
+
254
+ to_create = results.map { |x| x[:create] }.flatten.group_by(&:related_uri)
255
+ to_delete = results.map { |x| x[:delete] }.flatten.group_by(&:related_uri)
256
+ [to_create, to_delete]
257
+ end
258
+
259
+ # Executes the update for variables. It resolves what is new and needed to update.
260
+ # @param filters [Array<Hash>] Filter Definitions
261
+ # @param filters [Variable] Variable instance to be updated
262
+ # @param options [Hash]
263
+ # @option options [Boolean] :dry_run If dry run is true. No changes to he proejct are made but list of changes is provided
264
+ # @return [Array] list of filters that needs to be created and deleted
265
+ def self.execute_variables(filters, var, options = { client: GoodData.connection, project: GoodData.project })
266
+ client = options[:client]
267
+ project = options[:project]
268
+ dry_run = options[:dry_run]
269
+ to_create, to_delete = execute(filters, var.user_values, VariableUserFilter, options.merge(type: :variable))
270
+ return [to_create, to_delete] if dry_run
271
+
272
+ # TODO: get values that are about to be deleted and created and update them.
273
+ # This will make sure there is no downitme in filter existence
274
+ unless options[:do_not_touch_filters_that_are_not_mentioned]
275
+ to_delete.each { |_, group| group.each(&:delete) }
276
+ end
277
+ data = to_create.values.flatten.map(&:to_hash).map { |var_val| var_val.merge(prompt: var.uri) }
278
+ data.each_slice(200) do |slice|
279
+ client.post("/gdc/md/#{project.obj_id}/variables/user", :variables => slice)
280
+ end
281
+ [to_create, to_delete]
282
+ end
283
+
284
+ def self.execute_mufs(filters, options = { client: GoodData.connection, project: GoodData.project })
285
+ client = options[:client]
286
+ project = options[:project]
287
+
288
+ dry_run = options[:dry_run]
289
+ to_create, to_delete = execute(filters, project.data_permissions, MandatoryUserFilter, options.merge(type: :muf))
290
+ return [to_create, to_delete] if dry_run
291
+
292
+ to_create.each_pair do |related_uri, group|
293
+ group.each(&:save)
294
+
295
+ res = client.get("/gdc/md/#{project.pid}/userfilters?users=#{related_uri}")
296
+ items = res['userFilters']['items'].empty? ? [] : res['userFilters']['items'].first['userFilters']
297
+
298
+ payload = {
299
+ 'userFilters' => {
300
+ 'items' => [{
301
+ 'user' => related_uri,
302
+ 'userFilters' => items.concat(group.map(&:uri))
303
+ }]
304
+ }
305
+ }
306
+ client.post("/gdc/md/#{project.pid}/userfilters", payload)
307
+ end
308
+ unless options[:do_not_touch_filters_that_are_not_mentioned]
309
+ to_delete.each do |related_uri, group|
310
+ if related_uri
311
+ res = client.get("/gdc/md/#{project.pid}/userfilters?users=#{related_uri}")
312
+ items = res['userFilters']['items'].empty? ? [] : res['userFilters']['items'].first['userFilters']
313
+ payload = {
314
+ 'userFilters' => {
315
+ 'items' => [
316
+ {
317
+ 'user' => related_uri,
318
+ 'userFilters' => items - group.map(&:uri)
319
+ }
320
+ ]
321
+ }
322
+ }
323
+ client.post("/gdc/md/#{project.pid}/userfilters", payload)
324
+ end
325
+ group.each(&:delete)
326
+ end
327
+ end
328
+ [to_create, to_delete]
329
+ end
330
+
331
+ private
332
+
333
+ # Reads values from File/Array. Abstracts away the fact if it is column based,
334
+ # row based or in file or provided inline as an array
335
+ # @param file [String | Array] File or array of values to be parsed for filters
336
+ # @param options [Hash] Filter definitions
337
+ # @return [Array<Hash>]
338
+ def self.get_values(file, options)
339
+ file.is_a?(Array) ? read_array(file, options) : read_file(file, options)
340
+ end
341
+
342
+ # Reads array of values which are expected to be in a line wise manner
343
+ # [
344
+ # ['john.doe@example.com', 'Engineering', 'Marketing']
345
+ # ]
346
+ # @param data [Array<Array>]
347
+ def self.read_array(data, options = {})
348
+ memo = {}
349
+ data.each do |e|
350
+ key, data = process_line(e, options)
351
+ memo[key] = [] unless memo.key?(key)
352
+ memo[key].concat(data)
353
+ end
354
+ memo
355
+ end
356
+
357
+ # Executes the procedure necessary for loading user filters. This method has what
358
+ # is common for both implementations. Funcion
359
+ # * makes sure that filters are in normalized form.
360
+ # * verifies that users are in the project (and domain)
361
+ # * creates maql expressions of the filters provided
362
+ # * resolves the filters against current values in the project
363
+ # @param user_filters [Array] Filters that user is trying to set up
364
+ # @param project_filters [Array] List of filters currently in the project
365
+ # @param klass [Class] Class can be aither UserFilter or VariableFilter
366
+ # @param options [Hash] Filter definitions
367
+ # @return [Array<Hash>]
368
+ def self.execute(user_filters, project_filters, klass, options = { client: GoodData.connection, project: GoodData.project })
369
+ client = options[:client]
370
+ project = options[:project]
371
+
372
+ ignore_missing_values = options[:ignore_missing_values]
373
+ users_must_exist = options[:users_must_exist] == false ? false : true
374
+ filters = normalize_filters(user_filters)
375
+ domain = options[:domain]
376
+ users = domain ? project.users + domain.users : project.users
377
+ users_cache = create_cache(users, :login)
378
+ verify_existing_users(filters, options.merge(users_must_exist: users_must_exist, users_cache: users_cache))
379
+ user_filters, errors = maqlify_filters(filters, options.merge(users_cache: users_cache, users_must_exist: users_must_exist))
380
+ fail "Validation failed #{errors}" if !ignore_missing_values && !errors.empty?
381
+
382
+ filters = user_filters.map { |data| client.create(klass, data, project: project) }
383
+ resolve_user_filters(filters, project_filters)
384
+ end
385
+
386
+ # Gets definition of filters from user. They might either come in the full definition
387
+ # as hash or a simplified version. The simplified version do not cover all the possible
388
+ # features but it is much simpler to remember and suitable for quick hacking around
389
+ # @param filters [Array<Array | Hash>]
390
+ # @return [Array<Hash>]
391
+ def self.normalize_filters(filters)
392
+ filters.map do |filter|
393
+ if filter.is_a?(Hash)
394
+ filter
395
+ else
396
+ {
397
+ :login => filter.first,
398
+ :filters => [
399
+ {
400
+ :label => filter[1],
401
+ :values => filter[2..-1]
402
+ }
403
+ ]
404
+ }
405
+ end
406
+ end
407
+ end
408
+ end
409
+ end