gooddata 0.6.11 → 0.6.12

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 (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