gooddata 0.6.17 → 0.6.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/gooddata/models/domain.rb +32 -20
- data/lib/gooddata/models/process.rb +1 -0
- data/lib/gooddata/models/project.rb +28 -11
- data/lib/gooddata/models/user_filters/mandatory_user_filter.rb +19 -11
- data/lib/gooddata/models/user_filters/user_filter_builder.rb +296 -8
- data/lib/gooddata/rest/client.rb +4 -0
- data/lib/gooddata/version.rb +1 -1
- data/spec/unit/models/domain_spec.rb +1 -1
- metadata +1 -3
- data/lib/gooddata/models/user_filters/user_filter_builder_create.rb +0 -115
- data/lib/gooddata/models/user_filters/user_filter_builder_execute.rb +0 -133
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f683852cdcca1c3ac58923b66d2875199f4941ca
|
4
|
+
data.tar.gz: c439059752871aa8edaaae9578ebb5d8070d055a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7645abe267313b36bcc9f5d9ae076be9a7b608860db6a7bd27a91919dab47af0e31d3a48a921521d546f28fe822312ff69007826886447ea0dd165b170f7e5bb
|
7
|
+
data.tar.gz: 319d48cffc079ce239e78ff4b8ab4d2c3911a5fb381a13d47fb8d91f58e7947dab7d7bbed9fbfa44a97d87689f6b1f3340612246a59c8ffcec581109e69c1765
|
@@ -168,7 +168,8 @@ module GoodData
|
|
168
168
|
c = client(opts)
|
169
169
|
escaped_login = CGI.escape(login)
|
170
170
|
domain = c.domain(domain)
|
171
|
-
|
171
|
+
GoodData.logger.warn("Retrieving particular user \"#{login.inspect}\" from domain #{domain.name}")
|
172
|
+
url = "#{domain.uri}/users?login=#{escaped_login}"
|
172
173
|
tmp = c.get url
|
173
174
|
items = tmp['accountSettings']['items'] if tmp['accountSettings']
|
174
175
|
items && items.length > 0 ? c.factory.create(GoodData::Profile, items.first) : nil
|
@@ -180,23 +181,30 @@ module GoodData
|
|
180
181
|
# @option opts [Number] :offset The subject
|
181
182
|
# @option opts [Number] :limit From address
|
182
183
|
# TODO: Review opts[:limit] functionality
|
183
|
-
def users(domain, opts = {})
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
184
|
+
def users(domain, id = :all, opts = {})
|
185
|
+
c = client(opts)
|
186
|
+
domain = c.domain(domain)
|
187
|
+
if id == :all
|
188
|
+
GoodData.logger.warn("Retrieving all users from domain #{domain.name}")
|
189
|
+
result = []
|
190
|
+
page_limit = opts[:page_limit] || 1000
|
191
|
+
limit = opts[:limit] || Float::INFINITY
|
192
|
+
offset = opts[:offset]
|
193
|
+
uri = "#{domain.uri}/users?offset=#{offset}&limit=#{page_limit}"
|
194
|
+
loop do
|
195
|
+
tmp = client(opts).get(uri)
|
196
|
+
tmp['accountSettings']['items'].each do |account|
|
197
|
+
result << client(opts).create(GoodData::Profile, account)
|
198
|
+
end
|
199
|
+
break if result.length >= limit
|
195
200
|
|
196
|
-
|
197
|
-
|
201
|
+
uri = tmp['accountSettings']['paging']['next']
|
202
|
+
break unless uri
|
203
|
+
end
|
204
|
+
result
|
205
|
+
else
|
206
|
+
find_user_by_login(domain, id)
|
198
207
|
end
|
199
|
-
result
|
200
208
|
end
|
201
209
|
|
202
210
|
# Create users specified in list
|
@@ -264,8 +272,8 @@ module GoodData
|
|
264
272
|
# @param [Array<GoodData::User>]user_list Optional cached list of users used for look-ups
|
265
273
|
# @return [GoodDta::Membership] User
|
266
274
|
def get_user(name, user_list = users)
|
267
|
-
return member(name) if name.instance_of?(GoodData::Membership)
|
268
|
-
return member(name) if name.instance_of?(GoodData::Profile)
|
275
|
+
return member(name, user_list) if name.instance_of?(GoodData::Membership)
|
276
|
+
return member(name, user_list) if name.instance_of?(GoodData::Profile)
|
269
277
|
name = name.is_a?(Hash) ? name[:login] || name[:uri] : name
|
270
278
|
return nil unless name
|
271
279
|
name.downcase!
|
@@ -332,12 +340,16 @@ module GoodData
|
|
332
340
|
# domain = GoodData::Domain['gooddata-tomas-korcak']
|
333
341
|
# pp domain.users
|
334
342
|
#
|
335
|
-
def users(opts = {})
|
336
|
-
GoodData::Domain.users(name, opts.merge(client: client))
|
343
|
+
def users(id = :all, opts = {})
|
344
|
+
GoodData::Domain.users(name, id, opts.merge(client: client))
|
337
345
|
end
|
338
346
|
|
339
347
|
alias_method :members, :users
|
340
348
|
|
349
|
+
def uri
|
350
|
+
"/gdc/account/domains/#{name}"
|
351
|
+
end
|
352
|
+
|
341
353
|
private
|
342
354
|
|
343
355
|
# Private setter of domain name. Used by constructor not available for external users.
|
@@ -205,6 +205,7 @@ module GoodData
|
|
205
205
|
# @return [IO] The stream of data that represents a zipped deployed process.
|
206
206
|
def download
|
207
207
|
link = links['source']
|
208
|
+
client.connection.refresh_token
|
208
209
|
client.get(link, process: false) { |_, _, result| RestClient.get(result.to_hash['location'].first) }
|
209
210
|
end
|
210
211
|
|
@@ -350,8 +350,8 @@ module GoodData
|
|
350
350
|
GoodData::Dashboard[id, project: self, client: client]
|
351
351
|
end
|
352
352
|
|
353
|
-
def data_permissions
|
354
|
-
GoodData::MandatoryUserFilter
|
353
|
+
def data_permissions(id = :all)
|
354
|
+
GoodData::MandatoryUserFilter[id, client: client, project: self]
|
355
355
|
end
|
356
356
|
|
357
357
|
# Deletes project
|
@@ -890,7 +890,7 @@ module GoodData
|
|
890
890
|
# List of users in project
|
891
891
|
#
|
892
892
|
# @return [Array<GoodData::User>] List of users
|
893
|
-
def users(opts = { offset: 0, limit:
|
893
|
+
def users(opts = { offset: 0, limit: 1_000 })
|
894
894
|
result = []
|
895
895
|
|
896
896
|
# TODO: @korczis, review this after WA-3953 get fixed
|
@@ -933,9 +933,11 @@ module GoodData
|
|
933
933
|
def import_users(new_users, options = {})
|
934
934
|
domain = options[:domain]
|
935
935
|
role_list = roles
|
936
|
-
users_list = users
|
936
|
+
users_list = users(all: true, offset: 0, limit: 1_000)
|
937
937
|
new_users = new_users.map { |x| (x.is_a?(Hash) && x[:user] && x[:user].to_hash.merge(role: x[:role])) || x.to_hash }
|
938
938
|
|
939
|
+
GoodData.logger.warn("Importing users to project (#{pid})")
|
940
|
+
|
939
941
|
whitelisted_new_users, whitelisted_users = whitelist_users(new_users.map(&:to_hash), users_list, options[:whitelists])
|
940
942
|
|
941
943
|
# conform the role on list of new users so we can diff them with the users coming from the project
|
@@ -949,11 +951,12 @@ module GoodData
|
|
949
951
|
role = get_role(r, role_list)
|
950
952
|
role && role.uri
|
951
953
|
end
|
954
|
+
u[:status] = 'ENABLED'
|
952
955
|
u
|
953
956
|
end
|
954
957
|
|
955
958
|
# Diff users. Only login and role is important for the diff
|
956
|
-
diff = GoodData::Helpers.diff(whitelisted_users, diffable_new, key: :login, fields: [:login, :role])
|
959
|
+
diff = GoodData::Helpers.diff(whitelisted_users, diffable_new, key: :login, fields: [:login, :role, :status])
|
957
960
|
|
958
961
|
results = []
|
959
962
|
# Create new users
|
@@ -963,15 +966,19 @@ module GoodData
|
|
963
966
|
role: x[:role]
|
964
967
|
}
|
965
968
|
end
|
966
|
-
|
967
|
-
|
969
|
+
# This is only creating users that were not in the proejcts so far. This means this will reach into domain
|
970
|
+
GoodData.logger.warn("Creating #{diff[:added].count} users in project (#{pid})")
|
971
|
+
results.concat create_users(u, roles: role_list, domain: domain, project_users: whitelisted_users, only_domain: true)
|
968
972
|
|
969
973
|
# # Update existing users
|
974
|
+
GoodData.logger.warn("Updating #{diff[:changed].count} users in project (#{pid})")
|
970
975
|
list = diff[:changed].map { |x| { user: x[:new_obj], role: x[:new_obj][:role] || x[:new_obj][:roles] } }
|
971
976
|
results.concat(set_users_roles(list, roles: role_list, project_users: whitelisted_users))
|
972
977
|
|
973
978
|
# Remove old users
|
974
|
-
|
979
|
+
to_remove = diff[:removed].reject { |user| user[:status] == 'DISABLED' || user[:status] == :disabled }
|
980
|
+
GoodData.logger.warn("Removing #{to_remove.count} users in project (#{pid})")
|
981
|
+
results.concat(disable_users(to_remove))
|
975
982
|
results
|
976
983
|
end
|
977
984
|
|
@@ -982,7 +989,8 @@ module GoodData
|
|
982
989
|
generate_user_payload(u[:uri], 'DISABLED')
|
983
990
|
end
|
984
991
|
payloads.each_slice(100).mapcat do |payload|
|
985
|
-
client.post(url, 'users' => payload)
|
992
|
+
result = client.post(url, 'users' => payload)
|
993
|
+
result['projectUsersUpdateResult'].mapcat { |k, v| v.map { |x| { type: k.to_sym, uri: x } } }
|
986
994
|
end
|
987
995
|
end
|
988
996
|
|
@@ -1033,7 +1041,16 @@ module GoodData
|
|
1033
1041
|
role_list = options[:roles] || roles
|
1034
1042
|
project_users = options[:project_users] || users
|
1035
1043
|
domain = options[:domain] && client.domain(options[:domain])
|
1036
|
-
domain_users = domain.nil?
|
1044
|
+
domain_users = if domain.nil?
|
1045
|
+
options[:domain_users]
|
1046
|
+
else
|
1047
|
+
if options[:only_domain] && list.count < 100
|
1048
|
+
list.map { |l| domain.find_user_by_login(l[:user][:login]) }
|
1049
|
+
else
|
1050
|
+
domain.users
|
1051
|
+
end
|
1052
|
+
end
|
1053
|
+
|
1037
1054
|
users_to_add = list.flat_map do |user_hash|
|
1038
1055
|
user = user_hash[:user] || user_hash[:login]
|
1039
1056
|
desired_roles = user_hash[:role] || user_hash[:roles] || 'readOnlyUser'
|
@@ -1049,7 +1066,7 @@ module GoodData
|
|
1049
1066
|
results = payloads.each_slice(100).map do |payload|
|
1050
1067
|
client.post(url, 'users' => payload)
|
1051
1068
|
end
|
1052
|
-
results.flat_map { |x| x['projectUsersUpdateResult'].flat_map { |k, v| v.map { |v_2| {
|
1069
|
+
results.flat_map { |x| x['projectUsersUpdateResult'].flat_map { |k, v| v.map { |v_2| { type: k.to_sym, uri: v_2 } } } }
|
1053
1070
|
end
|
1054
1071
|
|
1055
1072
|
alias_method :add_users, :set_users_roles
|
@@ -30,19 +30,27 @@ module GoodData
|
|
30
30
|
break if result['userFilters']['length'] < offset
|
31
31
|
offset += count
|
32
32
|
end
|
33
|
-
vars.
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
33
|
+
vars.each_slice(100).mapcat do |batch|
|
34
|
+
batch.pmap do |a|
|
35
|
+
uri = a['link']
|
36
|
+
data = c.get(uri)
|
37
|
+
payload = {
|
38
|
+
'expression' => data['userFilter']['content']['expression'],
|
39
|
+
'related' => user_lookup[a['link']],
|
40
|
+
'level' => :user,
|
41
|
+
'type' => :filter,
|
42
|
+
'uri' => a['link']
|
43
|
+
}
|
44
|
+
c.create(GoodData::MandatoryUserFilter, payload, project: project)
|
45
|
+
end
|
44
46
|
end
|
45
47
|
end
|
48
|
+
|
49
|
+
def count(options = { client: GoodData.connection, project: GoodData.project })
|
50
|
+
c = client(options)
|
51
|
+
project = options[:project]
|
52
|
+
c.get(project.md['query'] + '/userfilters/')['query']['entries'].count
|
53
|
+
end
|
46
54
|
end
|
47
55
|
|
48
56
|
# Creates or updates the mandatory user filter on the server
|
@@ -1,8 +1,5 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
|
3
|
-
require_relative 'user_filter_builder_create'
|
4
|
-
require_relative 'user_filter_builder_execute'
|
5
|
-
|
6
3
|
module GoodData
|
7
4
|
module UserFilterBuilder
|
8
5
|
# Main Entry function. Gets values and processes them to get filters
|
@@ -109,19 +106,168 @@ module GoodData
|
|
109
106
|
end
|
110
107
|
end
|
111
108
|
|
112
|
-
def self.
|
113
|
-
|
109
|
+
def self.get_missing_users(filters, options = {})
|
110
|
+
users_cache = options[:users_cache]
|
111
|
+
filters.reject { |u| users_cache.key?(u[:login]) }
|
112
|
+
end
|
114
113
|
|
114
|
+
def self.verify_existing_users(filters, options = {})
|
115
115
|
users_must_exist = options[:users_must_exist] == false ? false : true
|
116
|
-
users_cache = options[:users_cache]
|
116
|
+
users_cache = options[:users_cache]
|
117
|
+
domain = options[:domain]
|
117
118
|
|
118
119
|
if users_must_exist
|
119
|
-
|
120
|
-
|
120
|
+
missing_users = filters.reject do |u|
|
121
|
+
next true if users_cache.key?(u[:login])
|
122
|
+
domain_user = (domain && domain.find_user_by_login(u[:login]))
|
123
|
+
users_cache[domain_user.login] = domain_user if domain_user
|
124
|
+
next true if domain_user
|
125
|
+
false
|
126
|
+
end
|
121
127
|
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?
|
122
128
|
end
|
123
129
|
end
|
124
130
|
|
131
|
+
def self.create_label_cache(result, options = {})
|
132
|
+
project = options[:project]
|
133
|
+
|
134
|
+
result.reduce({}) do |a, e|
|
135
|
+
e[:filters].map do |filter|
|
136
|
+
a[filter[:label]] = project.labels(filter[:label]) unless a.key?(filter[:label])
|
137
|
+
end
|
138
|
+
a
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def self.create_lookups_cache(small_labels)
|
143
|
+
small_labels.reduce({}) do |a, e|
|
144
|
+
lookup = e.values(:limit => 1_000_000).reduce({}) do |a1, e1|
|
145
|
+
a1[e1[:value]] = e1[:uri]
|
146
|
+
a1
|
147
|
+
end
|
148
|
+
a[e.uri] = lookup
|
149
|
+
a
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.create_attrs_cache(filters, options = {})
|
154
|
+
project = options[:project]
|
155
|
+
|
156
|
+
labels = filters.flat_map do |f|
|
157
|
+
f[:filters]
|
158
|
+
end
|
159
|
+
|
160
|
+
over_cache = labels.reduce({}) do |a, e|
|
161
|
+
a[e[:over]] = e[:over]
|
162
|
+
a
|
163
|
+
end
|
164
|
+
to_cache = labels.reduce({}) do |a, e|
|
165
|
+
a[e[:to]] = e[:to]
|
166
|
+
a
|
167
|
+
end
|
168
|
+
cache = over_cache.merge(to_cache)
|
169
|
+
attr_cache = {}
|
170
|
+
cache.each_pair do |k, v|
|
171
|
+
begin
|
172
|
+
attr_cache[k] = project.attributes(v)
|
173
|
+
rescue
|
174
|
+
nil
|
175
|
+
end
|
176
|
+
end
|
177
|
+
attr_cache
|
178
|
+
end
|
179
|
+
|
180
|
+
# Walks over provided labels and picks those that have fewer than certain amount of values
|
181
|
+
# This tries to balance for speed when working with small datasets (like users)
|
182
|
+
# so it precaches the values and still be able to function for larger ones even
|
183
|
+
# though that would mean tons of requests
|
184
|
+
def self.get_small_labels(labels_cache)
|
185
|
+
labels_cache.values.select { |label| label.values_count < 100_000 }
|
186
|
+
end
|
187
|
+
|
188
|
+
# Creates a MAQL expression(s) based on the filter defintion.
|
189
|
+
# Takes the filter definition looks up any necessary values and provides API executable MAQL
|
190
|
+
def self.create_expression(filter, labels_cache, lookups_cache, attr_cache, options = {})
|
191
|
+
errors = []
|
192
|
+
values = filter[:values]
|
193
|
+
label = labels_cache[filter[:label]]
|
194
|
+
element_uris = values.map do |v|
|
195
|
+
begin
|
196
|
+
if lookups_cache.key?(label.uri)
|
197
|
+
if lookups_cache[label.uri].key?(v)
|
198
|
+
lookups_cache[label.uri][v]
|
199
|
+
else
|
200
|
+
fail
|
201
|
+
end
|
202
|
+
else
|
203
|
+
label.find_value_uri(v)
|
204
|
+
end
|
205
|
+
rescue
|
206
|
+
errors << [label.title, v]
|
207
|
+
nil
|
208
|
+
end
|
209
|
+
end
|
210
|
+
expression = if element_uris.compact.empty? && options[:restrict_if_missing_all_values] && options[:type] == :muf
|
211
|
+
'1 <> 1'
|
212
|
+
elsif element_uris.compact.empty? && options[:restrict_if_missing_all_values] && options[:type] == :variable
|
213
|
+
nil
|
214
|
+
elsif element_uris.compact.empty?
|
215
|
+
'TRUE'
|
216
|
+
elsif filter[:over] && filter[:to]
|
217
|
+
over = attr_cache[filter[:over]]
|
218
|
+
to = attr_cache[filter[:to]]
|
219
|
+
"([#{label.attribute_uri}] IN (#{ element_uris.compact.sort.map { |e| '[' + e + ']' }.join(', ') })) OVER [#{over && over.uri}] TO [#{to && to.uri}]"
|
220
|
+
else
|
221
|
+
"[#{label.attribute_uri}] IN (#{ element_uris.compact.sort.map { |e| '[' + e + ']' }.join(', ') })"
|
222
|
+
end
|
223
|
+
[expression, errors]
|
224
|
+
end
|
225
|
+
|
226
|
+
# Encapuslates the creation of filter
|
227
|
+
def self.create_user_filter(expression, related)
|
228
|
+
{
|
229
|
+
'related' => related,
|
230
|
+
'level' => :user,
|
231
|
+
'expression' => expression,
|
232
|
+
'type' => :filter
|
233
|
+
}
|
234
|
+
end
|
235
|
+
|
236
|
+
# Resolves and creates maql statements from filter definitions.
|
237
|
+
# This method does not perform any modifications on API but
|
238
|
+
# collects all the information that is needed to do so.
|
239
|
+
# Method collects all info from the user and current state in project and compares.
|
240
|
+
# Returns suggestion of what should be deleted and what should be created
|
241
|
+
# If there is some discrepancies in the data (missing values, nonexistent users) it
|
242
|
+
# finishes and collects all the errors at once
|
243
|
+
#
|
244
|
+
# @param filters [Array<Hash>] Filters definition
|
245
|
+
# @return [Array] first is list of MAQL statements
|
246
|
+
def self.maqlify_filters(filters, options = {})
|
247
|
+
# project = options[:project]
|
248
|
+
users_cache = options[:users_cache] # || create_cache(project.users, :login)
|
249
|
+
labels_cache = create_label_cache(filters, options)
|
250
|
+
small_labels = get_small_labels(labels_cache)
|
251
|
+
lookups_cache = create_lookups_cache(small_labels)
|
252
|
+
attrs_cache = create_attrs_cache(filters, options)
|
253
|
+
|
254
|
+
errors = []
|
255
|
+
results = filters.pmapcat do |filter|
|
256
|
+
login = filter[:login]
|
257
|
+
filter[:filters].pmapcat do |f|
|
258
|
+
expression, error = create_expression(f, labels_cache, lookups_cache, attrs_cache, options)
|
259
|
+
errors << error unless error.empty?
|
260
|
+
profiles_uri = (users_cache[login] && users_cache[login].uri)
|
261
|
+
if profiles_uri && expression
|
262
|
+
[create_user_filter(expression, profiles_uri)]
|
263
|
+
else
|
264
|
+
[]
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
[results, errors]
|
269
|
+
end
|
270
|
+
|
125
271
|
def self.resolve_user_filter(user = [], project = [])
|
126
272
|
user ||= []
|
127
273
|
project ||= []
|
@@ -150,6 +296,83 @@ module GoodData
|
|
150
296
|
[to_create, to_delete]
|
151
297
|
end
|
152
298
|
|
299
|
+
# Executes the update for variables. It resolves what is new and needed to update.
|
300
|
+
# @param filters [Array<Hash>] Filter Definitions
|
301
|
+
# @param filters [Variable] Variable instance to be updated
|
302
|
+
# @param options [Hash]
|
303
|
+
# @option options [Boolean] :dry_run If dry run is true. No changes to he proejct are made but list of changes is provided
|
304
|
+
# @return [Array] list of filters that needs to be created and deleted
|
305
|
+
def self.execute_variables(filters, var, options = {})
|
306
|
+
client = options[:client]
|
307
|
+
project = options[:project]
|
308
|
+
dry_run = options[:dry_run]
|
309
|
+
to_create, to_delete = execute(filters, var.user_values, VariableUserFilter, options.merge(type: :variable))
|
310
|
+
return [to_create, to_delete] if dry_run
|
311
|
+
|
312
|
+
# TODO: get values that are about to be deleted and created and update them.
|
313
|
+
# This will make sure there is no downitme in filter existence
|
314
|
+
unless options[:do_not_touch_filters_that_are_not_mentioned]
|
315
|
+
to_delete.each { |_, group| group.each(&:delete) }
|
316
|
+
end
|
317
|
+
data = to_create.values.flatten.map(&:to_hash).map { |var_val| var_val.merge(prompt: var.uri) }
|
318
|
+
data.each_slice(200) do |slice|
|
319
|
+
client.post("/gdc/md/#{project.obj_id}/variables/user", :variables => slice)
|
320
|
+
end
|
321
|
+
[to_create, to_delete]
|
322
|
+
end
|
323
|
+
|
324
|
+
def self.execute_mufs(filters, options = {})
|
325
|
+
client = options[:client]
|
326
|
+
project = options[:project]
|
327
|
+
|
328
|
+
dry_run = options[:dry_run]
|
329
|
+
to_create, to_delete = execute(filters, project.data_permissions, MandatoryUserFilter, options.merge(type: :muf))
|
330
|
+
GoodData.logger.warn("Data permissions computed: #{to_create.count} to create and #{to_delete.count} to delete")
|
331
|
+
return [to_create, to_delete] if dry_run
|
332
|
+
|
333
|
+
to_create.each_slice(100).flat_map do |batch|
|
334
|
+
batch.peach do |related_uri, group|
|
335
|
+
group.each(&:save)
|
336
|
+
|
337
|
+
res = client.get("/gdc/md/#{project.pid}/userfilters?users=#{related_uri}")
|
338
|
+
items = res['userFilters']['items'].empty? ? [] : res['userFilters']['items'].first['userFilters']
|
339
|
+
|
340
|
+
payload = {
|
341
|
+
'userFilters' => {
|
342
|
+
'items' => [{
|
343
|
+
'user' => related_uri,
|
344
|
+
'userFilters' => items.concat(group.map(&:uri))
|
345
|
+
}]
|
346
|
+
}
|
347
|
+
}
|
348
|
+
client.post("/gdc/md/#{project.pid}/userfilters", payload)
|
349
|
+
end
|
350
|
+
end
|
351
|
+
unless options[:do_not_touch_filters_that_are_not_mentioned]
|
352
|
+
to_delete.each_slice(100).flat_map do |batch|
|
353
|
+
batch.peach do |related_uri, group|
|
354
|
+
if related_uri
|
355
|
+
res = client.get("/gdc/md/#{project.pid}/userfilters?users=#{related_uri}")
|
356
|
+
items = res['userFilters']['items'].empty? ? [] : res['userFilters']['items'].first['userFilters']
|
357
|
+
payload = {
|
358
|
+
'userFilters' => {
|
359
|
+
'items' => [
|
360
|
+
{
|
361
|
+
'user' => related_uri,
|
362
|
+
'userFilters' => items - group.map(&:uri)
|
363
|
+
}
|
364
|
+
]
|
365
|
+
}
|
366
|
+
}
|
367
|
+
client.post("/gdc/md/#{project.pid}/userfilters", payload)
|
368
|
+
end
|
369
|
+
group.peach(&:delete)
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
[to_create, to_delete]
|
374
|
+
end
|
375
|
+
|
153
376
|
private
|
154
377
|
|
155
378
|
# Reads values from File/Array. Abstracts away the fact if it is column based,
|
@@ -175,5 +398,70 @@ module GoodData
|
|
175
398
|
end
|
176
399
|
memo
|
177
400
|
end
|
401
|
+
|
402
|
+
# Executes the procedure necessary for loading user filters. This method has what
|
403
|
+
# is common for both implementations. Funcion
|
404
|
+
# * makes sure that filters are in normalized form.
|
405
|
+
# * verifies that users are in the project (and domain)
|
406
|
+
# * creates maql expressions of the filters provided
|
407
|
+
# * resolves the filters against current values in the project
|
408
|
+
# @param user_filters [Array] Filters that user is trying to set up
|
409
|
+
# @param project_filters [Array] List of filters currently in the project
|
410
|
+
# @param klass [Class] Class can be aither UserFilter or VariableFilter
|
411
|
+
# @param options [Hash] Filter definitions
|
412
|
+
# @return [Array<Hash>]
|
413
|
+
def self.execute(user_filters, project_filters, klass, options = {})
|
414
|
+
client = options[:client]
|
415
|
+
project = options[:project]
|
416
|
+
|
417
|
+
ignore_missing_values = options[:ignore_missing_values]
|
418
|
+
users_must_exist = options[:users_must_exist] == false ? false : true
|
419
|
+
filters = normalize_filters(user_filters)
|
420
|
+
domain = options[:domain]
|
421
|
+
users = project.users
|
422
|
+
# users = domain ? project.users : project.users
|
423
|
+
users_cache = create_cache(users, :login)
|
424
|
+
missing_users = get_missing_users(filters, options.merge(users_cache: users_cache))
|
425
|
+
user_filters, errors = if missing_users.empty?
|
426
|
+
verify_existing_users(filters, project: project, users_must_exist: users_must_exist, users_cache: users_cache)
|
427
|
+
maqlify_filters(filters, options.merge(users_cache: users_cache, users_must_exist: users_must_exist))
|
428
|
+
elsif missing_users.count < 100
|
429
|
+
verify_existing_users(filters, project: project, users_must_exist: users_must_exist, users_cache: users_cache, domain: domain)
|
430
|
+
maqlify_filters(filters, options.merge(users_cache: users_cache, users_must_exist: users_must_exist, domain: domain))
|
431
|
+
else
|
432
|
+
users += domain.users
|
433
|
+
users_cache = create_cache(users, :login)
|
434
|
+
verify_existing_users(filters, project: project, users_must_exist: users_must_exist, users_cache: users_cache, domain: domain)
|
435
|
+
maqlify_filters(filters, options.merge(users_cache: users_cache, users_must_exist: users_must_exist, domain: domain))
|
436
|
+
end
|
437
|
+
|
438
|
+
fail "Validation failed #{errors}" if !ignore_missing_values && !errors.empty?
|
439
|
+
|
440
|
+
filters = user_filters.map { |data| client.create(klass, data, project: project) }
|
441
|
+
resolve_user_filters(filters, project_filters)
|
442
|
+
end
|
443
|
+
|
444
|
+
# Gets definition of filters from user. They might either come in the full definition
|
445
|
+
# as hash or a simplified version. The simplified version do not cover all the possible
|
446
|
+
# features but it is much simpler to remember and suitable for quick hacking around
|
447
|
+
# @param filters [Array<Array | Hash>]
|
448
|
+
# @return [Array<Hash>]
|
449
|
+
def self.normalize_filters(filters)
|
450
|
+
filters.map do |filter|
|
451
|
+
if filter.is_a?(Hash)
|
452
|
+
filter
|
453
|
+
else
|
454
|
+
{
|
455
|
+
:login => filter.first,
|
456
|
+
:filters => [
|
457
|
+
{
|
458
|
+
:label => filter[1],
|
459
|
+
:values => filter[2..-1]
|
460
|
+
}
|
461
|
+
]
|
462
|
+
}
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|
178
466
|
end
|
179
467
|
end
|
data/lib/gooddata/rest/client.rb
CHANGED
data/lib/gooddata/version.rb
CHANGED
@@ -63,7 +63,7 @@ describe GoodData::Domain do
|
|
63
63
|
end
|
64
64
|
|
65
65
|
it 'Accepts pagination options - limit' do
|
66
|
-
users = @domain.users(limit: 10)
|
66
|
+
users = @domain.users(:all, limit: 10)
|
67
67
|
expect(users).to be_instance_of(Array)
|
68
68
|
users.each do |user|
|
69
69
|
expect(user).to be_an_instance_of(GoodData::Profile)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gooddata
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.18
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pavel Kolesnikov
|
@@ -759,8 +759,6 @@ files:
|
|
759
759
|
- lib/gooddata/models/user_filters/mandatory_user_filter.rb
|
760
760
|
- lib/gooddata/models/user_filters/user_filter.rb
|
761
761
|
- lib/gooddata/models/user_filters/user_filter_builder.rb
|
762
|
-
- lib/gooddata/models/user_filters/user_filter_builder_create.rb
|
763
|
-
- lib/gooddata/models/user_filters/user_filter_builder_execute.rb
|
764
762
|
- lib/gooddata/models/user_filters/user_filters.rb
|
765
763
|
- lib/gooddata/models/user_filters/variable_user_filter.rb
|
766
764
|
- lib/gooddata/rest/README.md
|
@@ -1,115 +0,0 @@
|
|
1
|
-
# encoding: UTF-8
|
2
|
-
|
3
|
-
module GoodData
|
4
|
-
module UserFilterBuilder
|
5
|
-
def self.create_label_cache(result, options = {})
|
6
|
-
project = options[:project]
|
7
|
-
|
8
|
-
result.reduce({}) do |a, e|
|
9
|
-
e[:filters].map do |filter|
|
10
|
-
a[filter[:label]] = project.labels(filter[:label]) unless a.key?(filter[:label])
|
11
|
-
end
|
12
|
-
a
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def self.create_lookups_cache(small_labels)
|
17
|
-
small_labels.reduce({}) do |a, e|
|
18
|
-
lookup = e.values(:limit => 1_000_000).reduce({}) do |a1, e1|
|
19
|
-
a1[e1[:value]] = e1[:uri]
|
20
|
-
a1
|
21
|
-
end
|
22
|
-
a[e.uri] = lookup
|
23
|
-
a
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
# Walks over provided labels and picks those that have fewer than certain amount of values
|
28
|
-
# This tries to balance for speed when working with small datasets (like users)
|
29
|
-
# so it precaches the values and still be able to function for larger ones even
|
30
|
-
# though that would mean tons of requests
|
31
|
-
def self.get_small_labels(labels_cache)
|
32
|
-
labels_cache.values.select { |label| label.values_count < 100_000 }
|
33
|
-
end
|
34
|
-
|
35
|
-
# Creates a MAQL expression(s) based on the filter defintion.
|
36
|
-
# Takes the filter definition looks up any necessary values and provides API executable MAQL
|
37
|
-
def self.create_expression(filter, labels_cache, lookups_cache, options = {})
|
38
|
-
errors = []
|
39
|
-
values = filter[:values]
|
40
|
-
label = labels_cache[filter[:label]]
|
41
|
-
element_uris = values.map do |v|
|
42
|
-
begin
|
43
|
-
if lookups_cache.key?(label.uri)
|
44
|
-
if lookups_cache[label.uri].key?(v)
|
45
|
-
lookups_cache[label.uri][v]
|
46
|
-
else
|
47
|
-
fail
|
48
|
-
end
|
49
|
-
else
|
50
|
-
label.find_value_uri(v)
|
51
|
-
end
|
52
|
-
rescue
|
53
|
-
errors << [label.title, v]
|
54
|
-
nil
|
55
|
-
end
|
56
|
-
end
|
57
|
-
expression = if element_uris.compact.empty? && options[:restrict_if_missing_all_values] && options[:type] == :muf
|
58
|
-
'1 <> 1'
|
59
|
-
elsif element_uris.compact.empty? && options[:restrict_if_missing_all_values] && options[:type] == :variable
|
60
|
-
nil
|
61
|
-
elsif element_uris.compact.empty?
|
62
|
-
'TRUE'
|
63
|
-
elsif filter[:over] && filter[:to]
|
64
|
-
"([#{label.attribute_uri}] IN (#{ element_uris.compact.sort.map { |e| '[' + e + ']' }.join(', ') })) OVER [#{filter[:over]}] TO [#{filter[:to]}]"
|
65
|
-
else
|
66
|
-
"[#{label.attribute_uri}] IN (#{ element_uris.compact.sort.map { |e| '[' + e + ']' }.join(', ') })"
|
67
|
-
end
|
68
|
-
[expression, errors]
|
69
|
-
end
|
70
|
-
|
71
|
-
# Encapuslates the creation of filter
|
72
|
-
def self.create_user_filter(expression, related)
|
73
|
-
{
|
74
|
-
'related' => related,
|
75
|
-
'level' => :user,
|
76
|
-
'expression' => expression,
|
77
|
-
'type' => :filter
|
78
|
-
}
|
79
|
-
end
|
80
|
-
|
81
|
-
# Resolves and creates maql statements from filter definitions.
|
82
|
-
# This method does not perform any modifications on API but
|
83
|
-
# collects all the information that is needed to do so.
|
84
|
-
# Method collects all info from the user and current state in project and compares.
|
85
|
-
# Returns suggestion of what should be deleted and what should be created
|
86
|
-
# If there is some discrepancies in the data (missing values, nonexistent users) it
|
87
|
-
# finishes and collects all the errors at once
|
88
|
-
#
|
89
|
-
# @param filters [Array<Hash>] Filters definition
|
90
|
-
# @return [Array] first is list of MAQL statements
|
91
|
-
def self.maqlify_filters(filters, options = {})
|
92
|
-
project = options[:project]
|
93
|
-
users_cache = options[:users_cache] || create_cache(project.users, :login)
|
94
|
-
labels_cache = create_label_cache(filters, options)
|
95
|
-
small_labels = get_small_labels(labels_cache)
|
96
|
-
lookups_cache = create_lookups_cache(small_labels)
|
97
|
-
|
98
|
-
errors = []
|
99
|
-
results = filters.mapcat do |filter|
|
100
|
-
login = filter[:login]
|
101
|
-
filter[:filters].mapcat do |f|
|
102
|
-
expression, error = create_expression(f, labels_cache, lookups_cache, options)
|
103
|
-
errors << error unless error.empty?
|
104
|
-
profiles_uri = (users_cache[login] && users_cache[login].uri)
|
105
|
-
if profiles_uri && expression
|
106
|
-
[create_user_filter(expression, profiles_uri)]
|
107
|
-
else
|
108
|
-
[]
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
[results, errors]
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
@@ -1,133 +0,0 @@
|
|
1
|
-
# encoding: UTF-8
|
2
|
-
|
3
|
-
module GoodData
|
4
|
-
module UserFilterBuilder
|
5
|
-
# Executes the update for variables. It resolves what is new and needed to update.
|
6
|
-
# @param filters [Array<Hash>] Filter Definitions
|
7
|
-
# @param filters [Variable] Variable instance to be updated
|
8
|
-
# @param options [Hash]
|
9
|
-
# @option options [Boolean] :dry_run If dry run is true. No changes to he proejct are made but list of changes is provided
|
10
|
-
# @return [Array] list of filters that needs to be created and deleted
|
11
|
-
def self.execute_variables(filters, var, options = {})
|
12
|
-
client = options[:client]
|
13
|
-
project = options[:project]
|
14
|
-
dry_run = options[:dry_run]
|
15
|
-
to_create, to_delete = execute(filters, var.user_values, VariableUserFilter, options.merge(type: :variable))
|
16
|
-
return [to_create, to_delete] if dry_run
|
17
|
-
|
18
|
-
# TODO: get values that are about to be deleted and created and update them.
|
19
|
-
# This will make sure there is no downitme in filter existence
|
20
|
-
unless options[:do_not_touch_filters_that_are_not_mentioned]
|
21
|
-
to_delete.each { |_, group| group.each(&:delete) }
|
22
|
-
end
|
23
|
-
data = to_create.values.flatten.map(&:to_hash).map { |var_val| var_val.merge(prompt: var.uri) }
|
24
|
-
data.each_slice(200) do |slice|
|
25
|
-
client.post("/gdc/md/#{project.obj_id}/variables/user", :variables => slice)
|
26
|
-
end
|
27
|
-
[to_create, to_delete]
|
28
|
-
end
|
29
|
-
|
30
|
-
def self.execute_mufs(filters, options = {})
|
31
|
-
client = options[:client]
|
32
|
-
project = options[:project]
|
33
|
-
|
34
|
-
dry_run = options[:dry_run]
|
35
|
-
to_create, to_delete = execute(filters, project.data_permissions, MandatoryUserFilter, options.merge(type: :muf))
|
36
|
-
return [to_create, to_delete] if dry_run
|
37
|
-
|
38
|
-
to_create.peach do |related_uri, group|
|
39
|
-
group.each(&:save)
|
40
|
-
|
41
|
-
res = client.get("/gdc/md/#{project.pid}/userfilters?users=#{related_uri}")
|
42
|
-
items = res['userFilters']['items'].empty? ? [] : res['userFilters']['items'].first['userFilters']
|
43
|
-
|
44
|
-
payload = {
|
45
|
-
'userFilters' => {
|
46
|
-
'items' => [{
|
47
|
-
'user' => related_uri,
|
48
|
-
'userFilters' => items.concat(group.map(&:uri))
|
49
|
-
}]
|
50
|
-
}
|
51
|
-
}
|
52
|
-
client.post("/gdc/md/#{project.pid}/userfilters", payload)
|
53
|
-
end
|
54
|
-
unless options[:do_not_touch_filters_that_are_not_mentioned]
|
55
|
-
to_delete.peach do |related_uri, group|
|
56
|
-
if related_uri
|
57
|
-
res = client.get("/gdc/md/#{project.pid}/userfilters?users=#{related_uri}")
|
58
|
-
items = res['userFilters']['items'].empty? ? [] : res['userFilters']['items'].first['userFilters']
|
59
|
-
payload = {
|
60
|
-
'userFilters' => {
|
61
|
-
'items' => [
|
62
|
-
{
|
63
|
-
'user' => related_uri,
|
64
|
-
'userFilters' => items - group.map(&:uri)
|
65
|
-
}
|
66
|
-
]
|
67
|
-
}
|
68
|
-
}
|
69
|
-
client.post("/gdc/md/#{project.pid}/userfilters", payload)
|
70
|
-
end
|
71
|
-
group.each(&:delete)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
[to_create, to_delete]
|
75
|
-
end
|
76
|
-
|
77
|
-
private
|
78
|
-
|
79
|
-
# Executes the procedure necessary for loading user filters. This method has what
|
80
|
-
# is common for both implementations. Funcion
|
81
|
-
# * makes sure that filters are in normalized form.
|
82
|
-
# * verifies that users are in the project (and domain)
|
83
|
-
# * creates maql expressions of the filters provided
|
84
|
-
# * resolves the filters against current values in the project
|
85
|
-
# @param user_filters [Array] Filters that user is trying to set up
|
86
|
-
# @param project_filters [Array] List of filters currently in the project
|
87
|
-
# @param klass [Class] Class can be aither UserFilter or VariableFilter
|
88
|
-
# @param options [Hash] Filter definitions
|
89
|
-
# @return [Array<Hash>]
|
90
|
-
def self.execute(user_filters, project_filters, klass, options = {})
|
91
|
-
client = options[:client]
|
92
|
-
project = options[:project]
|
93
|
-
|
94
|
-
ignore_missing_values = options[:ignore_missing_values]
|
95
|
-
users_must_exist = options[:users_must_exist] == false ? false : true
|
96
|
-
filters = normalize_filters(user_filters)
|
97
|
-
domain = options[:domain]
|
98
|
-
users = domain ? project.users + domain.users : project.users
|
99
|
-
users_cache = create_cache(users, :login)
|
100
|
-
verify_existing_users(filters, options.merge(users_must_exist: users_must_exist, users_cache: users_cache))
|
101
|
-
user_filters, errors = maqlify_filters(filters, options.merge(users_cache: users_cache, users_must_exist: users_must_exist))
|
102
|
-
fail "Validation failed #{errors}" if !ignore_missing_values && !errors.empty?
|
103
|
-
|
104
|
-
filters = user_filters.map { |data| client.create(klass, data, project: project) }
|
105
|
-
resolve_user_filters(filters, project_filters)
|
106
|
-
end
|
107
|
-
|
108
|
-
private
|
109
|
-
|
110
|
-
# Gets definition of filters from user. They might either come in the full definition
|
111
|
-
# as hash or a simplified version. The simplified version do not cover all the possible
|
112
|
-
# features but it is much simpler to remember and suitable for quick hacking around
|
113
|
-
# @param filters [Array<Array | Hash>]
|
114
|
-
# @return [Array<Hash>]
|
115
|
-
def self.normalize_filters(filters)
|
116
|
-
filters.map do |filter|
|
117
|
-
if filter.is_a?(Hash)
|
118
|
-
filter
|
119
|
-
else
|
120
|
-
{
|
121
|
-
:login => filter.first,
|
122
|
-
:filters => [
|
123
|
-
{
|
124
|
-
:label => filter[1],
|
125
|
-
:values => filter[2..-1]
|
126
|
-
}
|
127
|
-
]
|
128
|
-
}
|
129
|
-
end
|
130
|
-
end
|
131
|
-
end
|
132
|
-
end
|
133
|
-
end
|