gooddata 0.6.50 → 0.6.51

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +12 -0
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +3 -0
  5. data/gooddata.gemspec +2 -1
  6. data/lib/gooddata/bricks/middleware/aws_middleware.rb +4 -0
  7. data/lib/gooddata/bricks/middleware/decode_params_middleware.rb +1 -1
  8. data/lib/gooddata/bricks/middleware/dwh_middleware.rb +1 -0
  9. data/lib/gooddata/bricks/middleware/gooddata_middleware.rb +1 -0
  10. data/lib/gooddata/bricks/middleware/logger_middleware.rb +2 -1
  11. data/lib/gooddata/core/nil_logger.rb +9 -0
  12. data/lib/gooddata/goodzilla/goodzilla.rb +1 -1
  13. data/lib/gooddata/helpers/data_helper.rb +1 -0
  14. data/lib/gooddata/helpers/global_helpers_params.rb +54 -27
  15. data/lib/gooddata/lcm/actions/apply_custom_maql.rb +70 -0
  16. data/lib/gooddata/lcm/actions/associate_clients.rb +17 -4
  17. data/lib/gooddata/lcm/actions/collect_clients.rb +4 -1
  18. data/lib/gooddata/lcm/actions/collect_segment_clients.rb +1 -0
  19. data/lib/gooddata/lcm/actions/collect_segments.rb +15 -2
  20. data/lib/gooddata/lcm/actions/create_segment_masters.rb +2 -2
  21. data/lib/gooddata/lcm/actions/provision_clients.rb +2 -4
  22. data/lib/gooddata/lcm/actions/purge_clients.rb +2 -2
  23. data/lib/gooddata/lcm/actions/synchronize_etls_in_segment.rb +81 -0
  24. data/lib/gooddata/lcm/actions/synchronize_label_types.rb +2 -2
  25. data/lib/gooddata/lcm/actions/synchronize_processes.rb +3 -1
  26. data/lib/gooddata/lcm/data/create_lcm_release.sql.erb +2 -1
  27. data/lib/gooddata/lcm/helpers/check_helper.rb +1 -1
  28. data/lib/gooddata/lcm/lcm.rb +29 -11
  29. data/lib/gooddata/lcm/lcm2.rb +82 -20
  30. data/lib/gooddata/models/domain.rb +22 -1
  31. data/lib/gooddata/models/metadata.rb +13 -8
  32. data/lib/gooddata/models/metadata/attribute.rb +1 -1
  33. data/lib/gooddata/models/metadata/report_definition.rb +1 -0
  34. data/lib/gooddata/models/profile.rb +1 -1
  35. data/lib/gooddata/models/project.rb +162 -38
  36. data/lib/gooddata/models/project_creator.rb +26 -6
  37. data/lib/gooddata/models/project_log_formatter.rb +204 -0
  38. data/lib/gooddata/models/schedule.rb +2 -21
  39. data/lib/gooddata/models/segment.rb +26 -0
  40. data/lib/gooddata/models/style_setting.rb +5 -1
  41. data/lib/gooddata/models/user_filters/user_filter_builder.rb +9 -0
  42. data/lib/gooddata/rest/connection.rb +4 -1
  43. data/lib/gooddata/version.rb +1 -1
  44. data/spec/environment/development.rb +29 -0
  45. data/spec/environment/environment.rb +14 -2
  46. data/spec/environment/{hotfix.rb → testing.rb} +0 -0
  47. data/spec/integration/date_dim_switch_spec.rb +3 -5
  48. data/spec/integration/lcm_spec.rb +24 -21
  49. data/spec/integration/project_spec.rb +16 -0
  50. data/spec/integration/segments_spec.rb +1 -1
  51. data/spec/unit/helpers/global_helpers_spec.rb +26 -2
  52. data/spec/unit/helpers_spec.rb +20 -0
  53. data/spec/unit/models/project_creator_spec.rb +3 -2
  54. metadata +29 -12
  55. data/lib/gooddata/lcm/actions/ensure_titles.rb +0 -54
  56. data/spec/environment/develop.rb +0 -46
@@ -87,17 +87,22 @@ module GoodData
87
87
  from_attribute, to_attribute = mapping.find { |k, _| k.uri == a_uri }
88
88
  vals = from_attribute.values_for(id)
89
89
  labels = to_attribute.labels
90
- results = labels.to_enum.mapcat do |l|
91
- vals.map do |v|
92
- begin
93
- l.find_value_uri(v)
94
- rescue
95
- nil
90
+
91
+ result = nil
92
+ catch :found_value do
93
+ labels.each do |l|
94
+ vals.each do |v|
95
+ throw :found_value if result
96
+ result = begin
97
+ l.find_value_uri(v)
98
+ rescue
99
+ nil
100
+ end
96
101
  end
97
102
  end
98
103
  end
99
- fail "Unable to find replacement for #{a_uri}" if results.compact.empty?
100
- [a_uri, id, results.compact.first]
104
+ fail "Unable to find replacement for #{a_uri}" unless result
105
+ [a_uri, id, result]
101
106
  end
102
107
  replaceable_vals.map { |a, id, r| ["#{a}/elements?id=#{id}", r] }
103
108
  end
@@ -116,7 +116,7 @@ module GoodData
116
116
  # @return [Array] list of values for certain element. Returned in the same order as is the order of labels
117
117
  def values_for(element_id)
118
118
  # element_id = element_id.is_a?(String) ? element_id.match(/\?id=(\d)/)[1] : element_id
119
- labels.map do |label|
119
+ labels.pmap do |label|
120
120
  label.find_element_value(element_id)
121
121
  end
122
122
  end
@@ -251,6 +251,7 @@ module GoodData
251
251
  x = GoodData::MdObject.replace_quoted(self, mapping)
252
252
  x = GoodData::MdObject.replace_bracketed(x, mapping)
253
253
  vals = GoodData::MdObject.find_replaceable_values(self, mapping)
254
+ GoodData::MdObject.replace_quoted(x, vals)
254
255
  GoodData::MdObject.replace_bracketed(x, vals)
255
256
  end
256
257
 
@@ -60,7 +60,7 @@ module GoodData
60
60
  return id if id.instance_of?(GoodData::Profile) || id.respond_to?(:profile?) && id.profile?
61
61
 
62
62
  if id.to_s !~ %r{^(\/gdc\/account\/profile\/)?[a-zA-Z\d]+$}
63
- fail(ArgumentError, 'wrong type of argument. Should be either project ID or path')
63
+ fail(ArgumentError, 'wrong type of argument. Should be either profile ID or path')
64
64
  end
65
65
 
66
66
  id = id.match(/[a-zA-Z\d]+$/)[0] if id =~ %r{/}
@@ -10,6 +10,7 @@ require 'fileutils'
10
10
  require 'multi_json'
11
11
  require 'pmap'
12
12
  require 'zip'
13
+ require 'net/smtp'
13
14
 
14
15
  require_relative '../exceptions/no_project_error'
15
16
 
@@ -23,6 +24,7 @@ require_relative '../mixins/uri_getter'
23
24
 
24
25
  require_relative 'membership'
25
26
  require_relative 'process'
27
+ require_relative 'project_log_formatter'
26
28
  require_relative 'project_role'
27
29
  require_relative 'blueprint/blueprint'
28
30
 
@@ -78,9 +80,7 @@ module GoodData
78
80
  if id == :all
79
81
  Project.all({ client: GoodData.connection }.merge(opts))
80
82
  else
81
- if id.to_s !~ %r{^(\/gdc\/(projects|md)\/)?[a-zA-Z\d]+$}
82
- fail(ArgumentError, 'wrong type of argument. Should be either project ID or path')
83
- end
83
+ fail(ArgumentError, 'wrong type of argument. Should be either project ID or path') unless project_id_or_path?(id)
84
84
 
85
85
  id = id.match(/[a-zA-Z\d]+$/)[0] if id =~ %r{/}
86
86
 
@@ -90,6 +90,10 @@ module GoodData
90
90
  end
91
91
  end
92
92
 
93
+ def project_id_or_path?(id)
94
+ id.to_s =~ %r{^(\/gdc\/(projects|md)\/)?[a-zA-Z\d]+$}
95
+ end
96
+
93
97
  # Clones project along with etl and schedules
94
98
  #
95
99
  # @param project [Project] Project to be cloned from
@@ -184,10 +188,40 @@ module GoodData
184
188
  }
185
189
  end
186
190
 
191
+ def transfer_output_stage(from_project, to_project, options)
192
+ if from_project.processes.any? { |p| p.type == :dataload }
193
+ if to_project.processes.any? { |p| p.type == :dataload }
194
+ to_project.add.output_stage.schema = from_project.add.output_stage.schema
195
+ to_project.add.output_stage.output_stage_prefix = from_project.add.output_stage.output_stage_prefix
196
+ to_project.add.output_stage.save
197
+ else
198
+ from_prj_output_stage = from_project.add.output_stage
199
+ from_server = from_project.client.connection.server.url
200
+ to_server = to_project.client.connection.server.url
201
+ if from_server != to_server && options[:ads_output_stage_uri].nil?
202
+ raise "Cannot transfer output stage from #{from_server} to #{to_server}. " \
203
+ 'It is not possible to transfer output stages between ' \
204
+ 'different domains. Please specify an address of an output ' \
205
+ 'stage that is in the same domain as the target project ' \
206
+ 'using the "ads_output_stage_uri" parameter.'
207
+ end
208
+
209
+ to_project.add.output_stage = GoodData::AdsOutputStage.create(
210
+ client: to_project.client,
211
+ ads: options[:ads_output_stage_uri] || from_prj_output_stage.schema,
212
+ client_id: from_prj_output_stage.client_id,
213
+ output_stage_prefix: from_prj_output_stage.output_stage_prefix,
214
+ project: to_project
215
+ )
216
+ end
217
+ end
218
+ end
219
+
187
220
  # Clones project along with etl and schedules.
188
221
  #
189
222
  # @param client [GoodData::Rest::Client] GoodData client to be used for connection
190
223
  # @param from_project [GoodData::Project | GoodData::Segment | GoodData:Client | String] Object to be cloned from. Can be either segment in which case we take the master, client in which case we take its project, string in which case we treat is as an project object or directly project
224
+ # @param to_project [GoodData::Project | GoodData::Segment | GoodData:Client | String]
191
225
  def transfer_etl(client, from_project, to_project)
192
226
  from_project = case from_project
193
227
  when GoodData::Client
@@ -210,7 +244,12 @@ module GoodData
210
244
  transfer_schedules(from_project, to_project)
211
245
  end
212
246
 
213
- def transfer_processes(from_project, to_project)
247
+ # @param from_project The source project
248
+ # @param to_project The target project
249
+ # @param options Optional parameters
250
+ # @option ads_output_stage_uri Uri of the source output stage. It must be in the same domain as the target project.
251
+ def transfer_processes(from_project, to_project, options = {})
252
+ options = GoodData::Helpers.symbolize_keys(options)
214
253
  to_project_processes = to_project.processes
215
254
  from_project.processes.uniq(&:name).each do |process|
216
255
  fail "The process name #{process.name} must be unique in transfered project #{to_project}" if to_project_processes.count { |p| p.name == process.name } > 1
@@ -232,16 +271,8 @@ module GoodData
232
271
  end
233
272
  end
234
273
 
235
- if from_project.processes.any? { |p| p.type == :dataload }
236
- if to_project_processes.any? { |p| p.type == :dataload }
237
- to_project.add.output_stage.schema = from_project.add.output_stage.schema
238
- to_project.add.output_stage.output_stage_prefix = from_project.add.output_stage.output_stage_prefix
239
- to_project.add.output_stage.save
240
- else
241
- from_prj_output_stage = from_project.add.output_stage
242
- to_project.add.output_stage = GoodData::AdsOutputStage.create(client: to_project.client, ads: from_prj_output_stage.schema, client_id: from_prj_output_stage.client_id, output_stage_prefix: from_prj_output_stage.output_stage_prefix, project: to_project)
243
- end
244
- end
274
+ transfer_output_stage(from_project, to_project, options)
275
+
245
276
  res = (from_project.processes + to_project.processes).map { |p| [p, p.name, p.type] }
246
277
  res.group_by { |x| [x[1], x[2]] }
247
278
  .select { |_, procs| procs.length == 1 && procs[2] != :dataload }
@@ -249,6 +280,27 @@ module GoodData
249
280
  .peach(&:delete)
250
281
  end
251
282
 
283
+ def transfer_user_groups(from_project, to_project)
284
+ from_project.user_groups.each do |ug|
285
+ # migrate groups
286
+ new_group = to_project.user_groups.select { |group| group.name == ug.name }.first
287
+ new_group ||= UserGroup.create(:name => ug.name, :description => ug.description, :project => to_project)
288
+ new_group.project = to_project
289
+ new_group.description = ug.description
290
+ new_group.save
291
+ # migrate dashboard "grantees"
292
+ dashboards = from_project.dashboards
293
+ dashboards.each do |dashboard|
294
+ new_dashboard = to_project.dashboards.select { |dash| dash.title == dashboard.title }.first
295
+ next unless new_dashboard
296
+ grantee = dashboard.grantees['granteeURIs']['items'].select { |item| item['aclEntryURI']['grantee'].split('/').last == ug.links['self'].split('/').last }.first
297
+ next unless grantee
298
+ permission = grantee['aclEntryURI']['permission']
299
+ new_dashboard.grant(:member => new_group, :permission => permission)
300
+ end
301
+ end
302
+ end
303
+
252
304
  # Clones project along with etl and schedules.
253
305
  #
254
306
  # @param client [GoodData::Rest::Client] GoodData client to be used for connection
@@ -680,7 +732,7 @@ module GoodData
680
732
  def delete_all_data(options = {})
681
733
  return false unless options[:force]
682
734
  begin
683
- datasets.pmap(&:delete_data)
735
+ datasets.reject(&:date_dimension?).pmap(&:delete_data)
684
736
  rescue MaqlExecutionError => e
685
737
  # This is here so that we do not throw out exceptions on synchornizing date dimensions
686
738
  # Currently there is no reliable way how to tell it is a date dimension
@@ -916,6 +968,7 @@ module GoodData
916
968
  def initialize(json)
917
969
  super
918
970
  @json = json
971
+ @log_formatter = GoodData::ProjectLogFormatter.new(self)
919
972
  end
920
973
 
921
974
  # Invites new user to project
@@ -1252,14 +1305,16 @@ module GoodData
1252
1305
  GoodData.logger.info "Saving #{new_item.uri}"
1253
1306
  new_item.save
1254
1307
  end
1308
+ else
1309
+ GoodData.logger.info "Ignore #{item.uri}"
1255
1310
  end
1256
1311
  end
1257
1312
  end
1258
1313
 
1259
1314
  GoodData.logger.info 'Replacing hidden metrics'
1260
- local_metrics = rds.pmapcat { |rd| rd.using('metric') }.select { |m| m['deprecated'] == '1' }
1315
+ local_metrics = mapping.map { |a, _| a }.pmapcat { |a| a.usedby('metric') }.select { |m| m['deprecated'] == '1' }.map { |m| m['link'] }.uniq
1261
1316
  puts "Found #{local_metrics.count} metrics"
1262
- local_metrics.pmap { |m| metrics(m['link']) }.peach do |item|
1317
+ local_metrics.pmap { |m| metrics(m) }.peach do |item|
1263
1318
  new_item = item.replace(mapping)
1264
1319
  if new_item.json != item.json
1265
1320
  if dry_run
@@ -1268,6 +1323,8 @@ module GoodData
1268
1323
  GoodData.logger.info "Saving #{new_item.uri}"
1269
1324
  new_item.save
1270
1325
  end
1326
+ else
1327
+ GoodData.logger.info "Ignore #{item.uri}"
1271
1328
  end
1272
1329
  end
1273
1330
 
@@ -1466,7 +1523,7 @@ module GoodData
1466
1523
  whitelisted_new_users, whitelisted_users = whitelist_users(new_users.map(&:to_hash), users_list, options[:whitelists])
1467
1524
 
1468
1525
  # First check that if groups are provided we have them set up
1469
- check_groups(new_users.map(&:to_hash).flat_map { |u| u[:user_group] || [] }.uniq)
1526
+ check_groups(new_users.map(&:to_hash).flat_map { |u| u[:user_group] || [] }.uniq, options)
1470
1527
 
1471
1528
  # conform the role on list of new users so we can diff them with the users coming from the project
1472
1529
  diffable_new_with_default_role = whitelisted_new_users.map do |u|
@@ -1477,12 +1534,12 @@ module GoodData
1477
1534
  intermediate_new = diffable_new_with_default_role.map do |u|
1478
1535
  u[:role] = u[:role].map do |r|
1479
1536
  role = get_role(r, role_list)
1480
- role && role.uri
1537
+ role ? role.uri : r
1481
1538
  end
1482
1539
 
1483
1540
  u[:role_title] = u[:role].map do |r|
1484
1541
  role = get_role(r, role_list)
1485
- role && role.title
1542
+ role ? role.title : r
1486
1543
  end
1487
1544
 
1488
1545
  if u[:role].all?(&:nil?)
@@ -1515,29 +1572,37 @@ module GoodData
1515
1572
  return diff_results if options[:dry_run]
1516
1573
 
1517
1574
  # Create new users
1518
- u = diff[:added].map { |x| { user: x, role: x[:role] } }
1519
-
1520
1575
  results = []
1521
1576
  GoodData.logger.warn("Creating #{diff[:added].count} users in project (#{pid})")
1522
- results.concat(create_users(u, roles: role_list, project_users: whitelisted_users))
1577
+ to_create = diff[:added].map { |x| { user: x, role: x[:role] } }
1578
+ created_users_result = create_users(to_create, roles: role_list, project_users: whitelisted_users)
1579
+ @log_formatter.log_created_users(created_users_result, diff[:added])
1580
+ results.concat(created_users_result)
1581
+ send_mail_to_new_users(diff[:added], options[:email_options]) if options[:email_options] && !options[:email_options].empty? && !diff[:added].empty?
1523
1582
 
1524
1583
  # # Update existing users
1525
1584
  GoodData.logger.warn("Updating #{diff[:changed].count} users in project (#{pid})")
1526
- list = diff[:changed].map { |x| { user: x[:new_obj], role: x[:new_obj][:role] || x[:new_obj][:roles] } }
1527
- results.concat(set_users_roles(list, roles: role_list, project_users: whitelisted_users))
1585
+ to_update = diff[:changed].map { |x| { user: x[:new_obj], role: x[:new_obj][:role] || x[:new_obj][:roles] } }
1586
+ updated_users_result = set_users_roles(to_update, roles: role_list, project_users: whitelisted_users)
1587
+ @log_formatter.log_updated_users(updated_users_result, diff[:changed], role_list)
1588
+ results.concat(updated_users_result)
1528
1589
 
1529
1590
  # Remove old users
1530
- to_remove = diff[:removed].reject { |user| user[:status] == 'DISABLED' || user[:status] == :disabled }
1531
- GoodData.logger.warn("Disabling #{to_remove.count} users from project (#{pid})")
1532
- results.concat(disable_users(to_remove, roles: role_list, project_users: whitelisted_users))
1591
+ to_disable = diff[:removed].reject { |user| user[:status] == 'DISABLED' || user[:status] == :disabled }
1592
+ GoodData.logger.warn("Disabling #{to_disable.count} users from project (#{pid})")
1593
+ disabled_users_result = disable_users(to_disable, roles: role_list, project_users: whitelisted_users)
1594
+ @log_formatter.log_disabled_users(disabled_users_result)
1595
+ results.concat(disabled_users_result)
1533
1596
 
1534
1597
  # Remove old users completely
1535
1598
  if options[:remove_users_from_project]
1536
- to_remove = (to_remove + users(disabled: true).to_a).map(&:to_hash).uniq do |user|
1599
+ to_remove = (to_disable + users(disabled: true).to_a).map(&:to_hash).uniq do |user|
1537
1600
  user[:uri]
1538
1601
  end
1539
1602
  GoodData.logger.warn("Removing #{to_remove.count} users from project (#{pid})")
1540
- results.concat(remove_users(to_remove))
1603
+ removed_users_result = remove_users(to_remove)
1604
+ @log_formatter.log_removed_users(removed_users_result)
1605
+ results.concat(removed_users_result)
1541
1606
  end
1542
1607
 
1543
1608
  # reassign to groups
@@ -1551,9 +1616,9 @@ module GoodData
1551
1616
  a
1552
1617
  end
1553
1618
  mappings.group_by { |_, g| g }.each do |g, mapping|
1554
- # find group + set users
1555
- # CARE YOU DO NOT KNOW URI
1556
- user_groups(g).set_members(mapping.map { |user, _| user }.map { |login| users_lookup[login] && users_lookup[login].uri })
1619
+ remote_users = mapping.map { |user, _| user }.map { |login| users_lookup[login] && users_lookup[login].uri }.reject(&:nil?)
1620
+ next if remote_users.empty?
1621
+ user_groups(g).set_members(remote_users)
1557
1622
  end
1558
1623
  mentioned_groups = mappings.map(&:last).uniq
1559
1624
  groups_to_cleanup = user_groups.reject { |g| mentioned_groups.include?(g.name) }
@@ -1589,15 +1654,21 @@ module GoodData
1589
1654
  client.delete(url)
1590
1655
  [{ type: :successful, operation: :user_deleted_from_project, user: u }]
1591
1656
  rescue => e
1592
- [{ type: :failed, message: e.message }]
1657
+ [{ type: :failed, message: e.message, user: u }]
1593
1658
  end
1594
1659
  end
1595
1660
  end
1596
1661
 
1597
- def check_groups(specified_groups)
1662
+ def check_groups(specified_groups, options = {})
1598
1663
  groups = user_groups.map(&:name)
1599
1664
  missing_groups = specified_groups - groups
1600
- fail "All groups have to be specified before you try to import users. Groups that are currently in project are #{groups.join(',')} and you asked for #{missing_groups.join(',')}" unless missing_groups.empty?
1665
+ if options[:create_non_existing_user_groups]
1666
+ missing_groups.each do |g|
1667
+ create_group(name: g, description: g)
1668
+ end
1669
+ else
1670
+ fail "All groups have to be specified before you try to import users. Groups that are currently in project are #{groups.join(',')} and you asked for #{missing_groups.join(',')}" unless missing_groups.empty?
1671
+ end
1601
1672
  end
1602
1673
 
1603
1674
  # Update user
@@ -1632,7 +1703,7 @@ module GoodData
1632
1703
  login, roles = resolve_roles(user, desired_roles, options.merge(project_users: project_users, roles: role_list))
1633
1704
  [{ :type => :successful, user: login, roles: roles }]
1634
1705
  rescue => e
1635
- [{ :type => :failed, :reason => e.message, user: login, roles: roles }]
1706
+ [{ :type => :failed, :reason => e.message, user: user, roles: desired_roles }]
1636
1707
  end
1637
1708
  end
1638
1709
 
@@ -1705,7 +1776,7 @@ module GoodData
1705
1776
  desired_roles = Array(desired_roles)
1706
1777
  roles = desired_roles.map do |role_name|
1707
1778
  role = get_role(role_name, role_list)
1708
- fail ArgumentError, "Invalid role '#{role_name}' specified for user '#{user.email}'" if role.nil?
1779
+ fail ArgumentError, "Invalid role '#{role_name}' specified for user '#{GoodData::Helpers.last_uri_part(user)}'" if role.nil?
1709
1780
  role.uri
1710
1781
  end
1711
1782
  [user, roles]
@@ -1754,6 +1825,59 @@ module GoodData
1754
1825
 
1755
1826
  private
1756
1827
 
1828
+ def send_mail_to_new_users(users, email_options)
1829
+ password = email_options[:email_password]
1830
+ from = email_options[:email_from]
1831
+ raise 'Missing sender email, please specify parameter "email_from"' unless from
1832
+ raise 'Missing authentication password, please specify parameter "email_password"' unless password
1833
+ template = get_email_template(email_options)
1834
+ smtp = Net::SMTP.new('relay1.na.intgdc.com', 25)
1835
+ smtp.enable_starttls OpenSSL::SSL::SSLContext.new("TLSv1_2_client")
1836
+ smtp.start('notifications.gooddata.com', 'gdc', password, :plain)
1837
+ users.each do |user|
1838
+ smtp.send_mail(get_email_body(template, user), from, user[:login])
1839
+ end
1840
+ end
1841
+
1842
+ def get_email_template(options)
1843
+ bucket = options[:email_template_bucket]
1844
+ path = options[:email_template_path]
1845
+ access_key = options[:email_template_access_key]
1846
+ secret_key = options[:email_template_secret_key]
1847
+ raise "Unable to connect to AWS. Parameter \"email_template_bucket\" seems to be empty" unless bucket
1848
+ raise "Unable to connect to AWS. Parameter \"email_template_path\" is missing" unless path
1849
+ raise "Unable to connect to AWS. Parameter \"email_template_access_key\" is missing" unless access_key
1850
+ raise "Unable to connect to AWS. Parameter \"email_template_secret_key\" is missing" unless secret_key
1851
+ args = {
1852
+ access_key_id: access_key,
1853
+ secret_access_key: secret_key,
1854
+ max_retries: 15,
1855
+ http_read_timeout: 120,
1856
+ http_open_timeout: 120
1857
+ }
1858
+
1859
+ server_side_encryption = options['email_server_side_encryption'] || false
1860
+ args['s3_server_side_encryption'] = :aes256 if server_side_encryption
1861
+
1862
+ s3 = AWS::S3.new(args)
1863
+ bucket = s3.buckets[bucket]
1864
+ process_email_template(bucket, path)
1865
+ end
1866
+
1867
+ def process_email_template(bucket, path)
1868
+ type = path.split('/').last.include?('.html') ? 'html' : 'txt'
1869
+ body = bucket.objects[path].read
1870
+ body.prepend("MIME-Version: 1.0\nContent-type: text/html\n") if type == 'html'
1871
+ body
1872
+ end
1873
+
1874
+ def get_email_body(template, user)
1875
+ template.gsub('${name}', "#{user[:first_name]} #{user[:last_name]}")
1876
+ .gsub('${role}', user[:role_title].count == 1 ? user[:role_title].first : user[:role_title].to_s)
1877
+ .gsub('${user_group}', user[:user_group].count == 1 ? user[:user_group].first : user[:user_group].to_s)
1878
+ .gsub('${project}', Project[user[:pid]].title)
1879
+ end
1880
+
1757
1881
  def generate_user_payload(user_uri, status = 'ENABLED', roles_uri = nil)
1758
1882
  payload = {
1759
1883
  'user' => {
@@ -49,7 +49,10 @@ module GoodData
49
49
  result = client.post(uri, bp.to_wire)
50
50
  response = client.poll_on_code(result['asyncTask']['link']['poll'])
51
51
 
52
- maqls = pick_correct_chunks(response['projectModelDiff']['updateScripts'], opts)
52
+ chunks = response['projectModelDiff']['updateScripts']
53
+ return [] if chunks.empty?
54
+
55
+ maqls = pick_correct_chunks(chunks, opts)
53
56
  replaced_maqls = apply_replacements_on_maql(maqls, replacements)
54
57
 
55
58
  unless dry_run
@@ -126,14 +129,31 @@ module GoodData
126
129
  { cascade_drops: chunk['updateScript']['cascadeDrops'], preserve_data: chunk['updateScript']['preserveData'], maql: chunk['updateScript']['maqlDdlChunks'], orig: chunk }
127
130
  end
128
131
 
129
- results = GoodData::Helpers.join(rules, stuff, [:cascade_drops, :preserve_data], [:cascade_drops, :preserve_data], inner: true).sort_by { |l| l[:priority] } || []
132
+ results_from_api = GoodData::Helpers.join(rules, stuff, [:cascade_drops, :preserve_data], [:cascade_drops, :preserve_data], inner: true).sort_by { |l| l[:priority] } || []
130
133
 
131
- preference.each do |k, v|
132
- results = results.find_all do |result|
133
- result[k] == v
134
+ if preference.empty?
135
+ [results_from_api.first[:orig]]
136
+ else
137
+ results = results_from_api.dup
138
+ preference.each do |k, v|
139
+ results = results.select do |result|
140
+ result[k] == v
141
+ end
142
+ end
143
+ if results.empty?
144
+ available_chunks = results_from_api
145
+ .map do |result|
146
+ {
147
+ cascade_drops: result[:cascade_drops],
148
+ preserve_data: result[:preserve_data]
149
+ }
150
+ end
151
+ .map(&:to_s)
152
+ .join(', ')
153
+ fail "Synchronize LDM cannot proceed. Adjust your update_preferences and try again. Available chunks with preference: #{available_chunks}"
134
154
  end
155
+ results.map { |result| result[:orig] }
135
156
  end
136
- (preference.empty? ? [results.first].compact : results).map { |result| result[:orig] }
137
157
  end
138
158
 
139
159
  private