gooddata 0.6.19 → 0.6.20

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/Rakefile +17 -3
  4. data/gooddata.gemspec +8 -7
  5. data/lib/gooddata/bricks/middleware/base_middleware.rb +1 -1
  6. data/lib/gooddata/cli/commands/run_ruby_cmd.rb +2 -2
  7. data/lib/gooddata/cli/shared.rb +2 -1
  8. data/lib/gooddata/commands/auth.rb +58 -5
  9. data/lib/gooddata/commands/runners.rb +2 -6
  10. data/lib/gooddata/extensions/big_decimal.rb +4 -0
  11. data/lib/gooddata/extensions/false.rb +11 -0
  12. data/lib/gooddata/extensions/hash.rb +6 -17
  13. data/lib/gooddata/extensions/nil.rb +11 -0
  14. data/lib/gooddata/extensions/numeric.rb +11 -0
  15. data/lib/gooddata/extensions/object.rb +11 -0
  16. data/lib/gooddata/extensions/symbol.rb +11 -0
  17. data/lib/gooddata/extensions/true.rb +11 -0
  18. data/lib/gooddata/helpers/auth_helpers.rb +32 -2
  19. data/lib/gooddata/helpers/data_helper.rb +1 -1
  20. data/lib/gooddata/helpers/global_helpers.rb +98 -31
  21. data/lib/gooddata/mixins/md_finders.rb +15 -15
  22. data/lib/gooddata/mixins/md_object_query.rb +12 -2
  23. data/lib/gooddata/models/blueprint/blueprint_field.rb +2 -2
  24. data/lib/gooddata/models/blueprint/dataset_blueprint.rb +2 -2
  25. data/lib/gooddata/models/blueprint/project_blueprint.rb +3 -3
  26. data/lib/gooddata/models/blueprint/schema_blueprint.rb +1 -1
  27. data/lib/gooddata/models/datawarehouse.rb +1 -0
  28. data/lib/gooddata/models/domain.rb +13 -16
  29. data/lib/gooddata/models/from_wire.rb +0 -2
  30. data/lib/gooddata/models/membership.rb +1 -1
  31. data/lib/gooddata/models/metadata/attribute.rb +1 -1
  32. data/lib/gooddata/models/metadata/dashboard.rb +1 -1
  33. data/lib/gooddata/models/metadata/dataset.rb +1 -1
  34. data/lib/gooddata/models/metadata/dimension.rb +1 -1
  35. data/lib/gooddata/models/metadata/fact.rb +1 -1
  36. data/lib/gooddata/models/metadata/label.rb +16 -17
  37. data/lib/gooddata/models/metadata/metric.rb +1 -1
  38. data/lib/gooddata/models/metadata/report.rb +1 -1
  39. data/lib/gooddata/models/metadata/report_definition.rb +7 -7
  40. data/lib/gooddata/models/metadata/variable.rb +1 -1
  41. data/lib/gooddata/models/model.rb +2 -2
  42. data/lib/gooddata/models/profile.rb +2 -2
  43. data/lib/gooddata/models/project.rb +21 -23
  44. data/lib/gooddata/models/project_role.rb +3 -3
  45. data/lib/gooddata/models/schedule.rb +18 -4
  46. data/lib/gooddata/models/user_filters/mandatory_user_filter.rb +12 -15
  47. data/lib/gooddata/models/user_filters/user_filter.rb +8 -8
  48. data/lib/gooddata/models/user_filters/user_filter_builder.rb +16 -13
  49. data/lib/gooddata/models/user_filters/variable_user_filter.rb +1 -1
  50. data/lib/gooddata/rest/client.rb +4 -2
  51. data/lib/gooddata/rest/connection.rb +37 -30
  52. data/lib/gooddata/rest/connections/rest_client_connection.rb +1 -1
  53. data/lib/gooddata/version.rb +1 -1
  54. data/spec/environment/develop.rb +4 -4
  55. data/spec/environment/hotfix.rb +1 -1
  56. data/spec/environment/release.rb +1 -1
  57. data/spec/integration/full_project_spec.rb +3 -3
  58. data/spec/integration/over_to_user_filters_spec.rb +1 -0
  59. data/spec/integration/project_spec.rb +1 -1
  60. data/spec/integration/user_filters_spec.rb +0 -1
  61. data/spec/unit/commands/command_auth_spec.rb +10 -0
  62. data/spec/unit/extensions/hash_spec.rb +1 -1
  63. data/spec/unit/helpers_spec.rb +0 -8
  64. data/spec/unit/models/domain_spec.rb +1 -9
  65. data/spec/unit/models/from_wire_spec.rb +1 -19
  66. data/spec/unit/models/membership_spec.rb +1 -1
  67. data/spec/unit/models/metadata_spec.rb +1 -1
  68. data/spec/unit/models/profile_spec.rb +23 -47
  69. data/spec/unit/models/schedule_spec.rb +47 -3
  70. metadata +174 -50
  71. data/lib/gooddata/models/from_wire_parse.rb +0 -125
@@ -203,7 +203,11 @@ module GoodData
203
203
  label.find_value_uri(v)
204
204
  end
205
205
  rescue
206
- errors << [label.title, v]
206
+ errors << {
207
+ type: :error,
208
+ label: label.title,
209
+ value: v
210
+ }
207
211
  nil
208
212
  end
209
213
  end
@@ -226,10 +230,10 @@ module GoodData
226
230
  # Encapuslates the creation of filter
227
231
  def self.create_user_filter(expression, related)
228
232
  {
229
- 'related' => related,
230
- 'level' => :user,
231
- 'expression' => expression,
232
- 'type' => :filter
233
+ related: related,
234
+ level: :user,
235
+ expression: expression,
236
+ type: :filter
233
237
  }
234
238
  end
235
239
 
@@ -251,21 +255,21 @@ module GoodData
251
255
  lookups_cache = create_lookups_cache(small_labels)
252
256
  attrs_cache = create_attrs_cache(filters, options)
253
257
 
254
- errors = []
255
- results = filters.pmapcat do |filter|
258
+ results = filters.flat_map do |filter|
256
259
  login = filter[:login]
257
260
  filter[:filters].pmapcat do |f|
258
- expression, error = create_expression(f, labels_cache, lookups_cache, attrs_cache, options)
259
- errors << error unless error.empty?
261
+ expression, errors = create_expression(f, labels_cache, lookups_cache, attrs_cache, options)
260
262
  profiles_uri = (users_cache[login] && users_cache[login].uri)
261
263
  if profiles_uri && expression
262
- [create_user_filter(expression, profiles_uri)]
264
+ [create_user_filter(expression, profiles_uri)] + errors
263
265
  else
264
- []
266
+ [] + errors
265
267
  end
266
268
  end
267
269
  end
268
- [results, errors]
270
+ results.group_by { |i| i[:type] }
271
+ .values_at(:filter, :error)
272
+ .map { |i| i || [] }
269
273
  end
270
274
 
271
275
  def self.resolve_user_filter(user = [], project = [])
@@ -436,7 +440,6 @@ module GoodData
436
440
  end
437
441
 
438
442
  fail "Validation failed #{errors}" if !ignore_missing_values && !errors.empty?
439
-
440
443
  filters = user_filters.map { |data| client.create(klass, data, project: project) }
441
444
  resolve_user_filters(filters, project_filters)
442
445
  end
@@ -9,7 +9,7 @@ module GoodData
9
9
  # @return [String]
10
10
  def save
11
11
  res = client.post(uri, :variable => @json)
12
- @json['uri'] = res['uri']
12
+ @json[:uri] = res['uri']
13
13
  self
14
14
  end
15
15
  end
@@ -67,7 +67,7 @@ module GoodData
67
67
  password = ENV['GD_GEM_PASSWORD']
68
68
  end
69
69
 
70
- username = username.symbolize_keys if username.is_a?(Hash)
70
+ username = GoodData::Helpers.symbolize_keys(username) if username.is_a?(Hash)
71
71
 
72
72
  new_opts = opts.dup
73
73
  if username.is_a?(Hash) && username.key?(:sst_token)
@@ -144,7 +144,7 @@ module GoodData
144
144
  @factory = ObjectFactory.new(self)
145
145
  end
146
146
 
147
- def create_project(options = { title: 'Project', auth_token: ENV['GD_PROJECT_TOKEN'] })
147
+ def create_project(options = { title: 'Project' })
148
148
  GoodData::Project.create({ client: self }.merge(options))
149
149
  end
150
150
 
@@ -246,6 +246,8 @@ module GoodData
246
246
 
247
247
  url = project.links['uploads']
248
248
  fail 'Project WebDAV not supported in this Data Center' unless url
249
+
250
+ GoodData.logger.warn 'Beware! Project webdav is deprecated and should not be used.'
249
251
  url
250
252
  end
251
253
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'terminal-table'
4
4
  require 'securerandom'
5
+ require 'monitor'
6
+ require 'thread_safe'
5
7
 
6
8
  require_relative '../version'
7
9
  require_relative '../exceptions/exceptions'
@@ -10,7 +12,9 @@ module GoodData
10
12
  module Rest
11
13
  # Wrapper of low-level HTTP/REST client/library
12
14
  class Connection
13
- DEFAULT_URL = ENV['GD_SERVER'] || 'https://secure.gooddata.com'
15
+ include MonitorMixin
16
+
17
+ DEFAULT_URL = 'https://secure.gooddata.com'
14
18
  LOGIN_PATH = '/gdc/account/login'
15
19
  TOKEN_PATH = '/gdc/account/token'
16
20
  KEYS_TO_SCRUB = [:password, :verifyPassword, :authorizationToken]
@@ -98,7 +102,8 @@ module GoodData
98
102
  attr_reader :user
99
103
 
100
104
  def initialize(opts)
101
- @stats = {}
105
+ super()
106
+ @stats = ThreadSafe::Hash.new
102
107
 
103
108
  headers = opts[:headers] || {}
104
109
  @webdav_headers = DEFAULT_WEBDAV_HEADERS.merge(headers)
@@ -115,7 +120,7 @@ module GoodData
115
120
 
116
121
  # Connect using username and password
117
122
  def connect(username, password, options = {})
118
- server = options[:server] || DEFAULT_URL
123
+ server = options[:server] || Helpers::AuthHelper.read_server
119
124
  options = DEFAULT_LOGIN_PAYLOAD.merge(options)
120
125
  headers = options[:headers] || {}
121
126
 
@@ -278,10 +283,10 @@ module GoodData
278
283
  # @param uri [String] Target URI
279
284
  def get(uri, options = {}, &user_block)
280
285
  options = log_info(options)
281
- GoodData.logger.debug "GET: #{@server.url}#{uri}"
286
+ GoodData.logger.debug "GET: #{@server.url}#{uri}, #{options}"
282
287
  profile "GET #{uri}" do
283
288
  b = proc do
284
- params = fresh_request_params(options[:request_id])
289
+ params = fresh_request_params(options[:request_id]).merge(options)
285
290
  begin
286
291
  @server[uri].get(params, &user_block)
287
292
  rescue RestClient::Exception => e
@@ -526,7 +531,7 @@ module GoodData
526
531
  def scrub_params(params, keys)
527
532
  keys = keys.reduce([]) { |a, e| a.concat([e.to_s, e.to_sym]) }
528
533
 
529
- new_params = params.deep_dup
534
+ new_params = GoodData::Helpers.deep_dup(params)
530
535
  GoodData::Helpers.hash_dfs(new_params) do |k, _key|
531
536
  keys.each do |key_to_scrub|
532
537
  k[key_to_scrub] = ('*' * k[key_to_scrub].length) if k && k.key?(key_to_scrub) && k[key_to_scrub]
@@ -558,37 +563,39 @@ module GoodData
558
563
  ]
559
564
 
560
565
  def update_stats(title, delta)
561
- orig_title = title
566
+ synchronize do
567
+ orig_title = title
562
568
 
563
- placeholders = true
569
+ placeholders = true
564
570
 
565
- if placeholders
566
- PH_MAP.each do |pm|
567
- break if title.gsub!(pm[1], pm[0])
571
+ if placeholders
572
+ PH_MAP.each do |pm|
573
+ break if title.gsub!(pm[1], pm[0])
574
+ end
568
575
  end
569
- end
570
576
 
571
- stat = stats[title]
572
- if stat.nil?
573
- stat = {
574
- :min => delta,
575
- :max => delta,
576
- :total => 0,
577
- :avg => 0,
578
- :calls => 0,
579
- :entries => []
580
- }
581
- end
577
+ stat = stats[title]
578
+ if stat.nil?
579
+ stat = {
580
+ :min => delta,
581
+ :max => delta,
582
+ :total => 0,
583
+ :avg => 0,
584
+ :calls => 0,
585
+ :entries => []
586
+ }
587
+ end
582
588
 
583
- stat[:min] = delta if delta < stat[:min]
584
- stat[:max] = delta if delta > stat[:max]
585
- stat[:total] += delta
586
- stat[:calls] += 1
587
- stat[:avg] = stat[:total] / stat[:calls]
589
+ stat[:min] = delta if delta < stat[:min]
590
+ stat[:max] = delta if delta > stat[:max]
591
+ stat[:total] += delta
592
+ stat[:calls] += 1
593
+ stat[:avg] = stat[:total] / stat[:calls]
588
594
 
589
- stat[:entries] << orig_title if placeholders
595
+ stat[:entries] << orig_title if placeholders
590
596
 
591
- stats[title] = stat
597
+ stats[title] = stat
598
+ end
592
599
  end
593
600
 
594
601
  def webdav_dir_exists?(url)
@@ -24,7 +24,7 @@ module GoodData
24
24
 
25
25
  # Connect using username and password
26
26
  def connect(username, password, options = {})
27
- server = options[:server] || DEFAULT_URL
27
+ server = options[:server] || Helpers::AuthHelper.read_server
28
28
  @server = RestClient::Resource.new server, DEFAULT_LOGIN_PAYLOAD
29
29
 
30
30
  super
@@ -2,7 +2,7 @@
2
2
 
3
3
  # GoodData Module
4
4
  module GoodData
5
- VERSION = '0.6.19'
5
+ VERSION = '0.6.20'
6
6
 
7
7
  class << self
8
8
  # Version
@@ -8,19 +8,19 @@ module GoodData
8
8
  end
9
9
 
10
10
  module ProcessHelper
11
- PROCESS_ID = '2e2cbe45-02fd-4a1a-b735-a37d65ff267d'
12
- DEPLOY_NAME = 'graph.grf'
11
+ PROCESS_ID = '94e7be67-5f68-405d-bdeb-93006d50482d'
12
+ DEPLOY_NAME = 'graph/graph.grf'
13
13
  end
14
14
 
15
15
  module ProjectHelper
16
- PROJECT_ID = 'i66l5qezxd96syjo9hgbie8earysh6b7'
16
+ PROJECT_ID = 'k8rzngunca3t9dywmxhqzpgwlui3yg0m'
17
17
  PROJECT_URL = "/gdc/projects/#{PROJECT_ID}"
18
18
  PROJECT_TITLE = 'GoodTravis'
19
19
  PROJECT_SUMMARY = 'No summary'
20
20
  end
21
21
 
22
22
  module ScheduleHelper
23
- SCHEDULE_ID = '556c580ee4b05b1a534f3997'
23
+ SCHEDULE_ID = '55953261e4b0a92792febe4e'
24
24
  end
25
25
  end
26
26
  end
@@ -8,7 +8,7 @@ module GoodData
8
8
  end
9
9
 
10
10
  module ProjectHelper
11
- PROJECT_ID = 'vc8mctilky1xu6uqclafvan8b0x1mv3k'
11
+ PROJECT_ID = 'i640il7dyatqmvak24zzr09ypt3ghqu2'
12
12
  PROJECT_URL = "/gdc/projects/#{PROJECT_ID}"
13
13
  PROJECT_TITLE = 'GoodTravis'
14
14
  PROJECT_SUMMARY = 'No summary'
@@ -8,7 +8,7 @@ module GoodData
8
8
  end
9
9
 
10
10
  module ProjectHelper
11
- PROJECT_ID = 'vc8mctilky1xu6uqclafvan8b0x1mv3k'
11
+ PROJECT_ID = 'i640il7dyatqmvak24zzr09ypt3ghqu2'
12
12
  PROJECT_URL = "/gdc/projects/#{PROJECT_ID}"
13
13
  PROJECT_TITLE = 'GoodTravis'
14
14
  PROJECT_SUMMARY = 'No summary'
@@ -260,8 +260,8 @@ describe "Full project implementation", :constraint => 'slow' do
260
260
  end
261
261
 
262
262
  it "should be possible to get all metrics with full objects" do
263
- metrics1 = @project.metrics(:all, full: false)
264
- expect(metrics1.first.class).to be Hash
263
+ metrics = @project.metrics(:all)
264
+ expect(metrics.first.class).to be GoodData::Metric
265
265
  end
266
266
 
267
267
  it "should be able to get a metric by identifier" do
@@ -412,7 +412,7 @@ describe "Full project implementation", :constraint => 'slow' do
412
412
  attribute = @project.attributes('attr.devs.dev_id')
413
413
  label = attribute.primary_label
414
414
  value = label.values.first
415
- different_value = label.values[1]
415
+ different_value = label.values.drop(1).first
416
416
  fact = @project.facts('fact.commits.lines_changed')
417
417
  metric = @project.create_metric("SELECT SUM([#{fact.uri}]) WHERE [#{attribute.uri}] = [#{value[:uri]}]")
418
418
  metric.replace_value(label, value[:value], different_value[:value])
@@ -2,6 +2,7 @@ require 'gooddata'
2
2
 
3
3
  describe "Variables implementation", :constraint => 'slow' do
4
4
  before(:all) do
5
+ GoodData.logging_http_on
5
6
  @spec = JSON.parse(File.read("./spec/data/blueprints/m_n_model.json"), :symbolize_names => true)
6
7
  @client = ConnectionHelper::create_default_connection
7
8
  @blueprint = GoodData::Model::ProjectBlueprint.new(@spec)
@@ -132,7 +132,7 @@ describe GoodData::Project, :constraint => 'slow' do
132
132
 
133
133
  describe '#set_user_roles' do
134
134
  it 'Properly updates user roles as needed' do
135
- users_to_import = @domain.users.sample(5).map {|u| { user: u, role: 'admin' }}
135
+ users_to_import = @domain.users.drop(rand(100)).take(5).map {|u| { user: u, role: 'admin' }}
136
136
  @project.import_users(users_to_import, domain: @domain, whitelists: [/gem_tester@gooddata.com/])
137
137
  users_without_owner = @project.users.reject { |u| u.login == ConnectionHelper::DEFAULT_USERNAME }.pselect { |u| u.role.title == 'Admin' }
138
138
 
@@ -44,7 +44,6 @@ describe "User filters implementation", :constraint => 'slow' do
44
44
  @project.add_data_permissions(filters)
45
45
  metric.execute.should == 6
46
46
  r = @project.compute_report(left: [metric], top: [@label.attribute])
47
- r.include_column?(['tomas@gooddata.com', 1]).should == true
48
47
 
49
48
  r.include_column?(['tomas@gooddata.com', 1]).should == true
50
49
  r.include_column?(['jirka@gooddata.com', 5]).should == true
@@ -11,6 +11,8 @@ describe GoodData::Command::Auth do
11
11
  :email => 'joedoe@example.com',
12
12
  :password => 'secretPassword',
13
13
  :token => 't0k3n1sk0',
14
+ :environment => 'DEVELOPMENT',
15
+ :server => 'https://secure.gooddata.com'
14
16
  }
15
17
 
16
18
  DEFAULT_CREDENTIALS_OVER = {
@@ -59,6 +61,8 @@ describe GoodData::Command::Auth do
59
61
  @input << DEFAULT_CREDENTIALS[:email] << "\n"
60
62
  @input << DEFAULT_CREDENTIALS[:password] << "\n"
61
63
  @input << DEFAULT_CREDENTIALS[:token] << "\n"
64
+ @input << DEFAULT_CREDENTIALS[:environment] << "\n"
65
+ @input << DEFAULT_CREDENTIALS[:server] << "\n"
62
66
  @input.rewind
63
67
 
64
68
  GoodData::Command::Auth.ask_for_credentials
@@ -105,6 +109,8 @@ describe GoodData::Command::Auth do
105
109
  @input << DEFAULT_CREDENTIALS[:email] << "\n"
106
110
  @input << DEFAULT_CREDENTIALS[:password] << "\n"
107
111
  @input << DEFAULT_CREDENTIALS[:token] << "\n"
112
+ @input << DEFAULT_CREDENTIALS[:environment] << "\n"
113
+ @input << DEFAULT_CREDENTIALS[:server] << "\n"
108
114
  @input << 'y' << "\n"
109
115
  @input.rewind
110
116
 
@@ -118,6 +124,8 @@ describe GoodData::Command::Auth do
118
124
  @input << DEFAULT_CREDENTIALS[:email] << "\n"
119
125
  @input << DEFAULT_CREDENTIALS[:password] << "\n"
120
126
  @input << DEFAULT_CREDENTIALS[:token] << "\n"
127
+ @input << DEFAULT_CREDENTIALS[:environment] << "\n"
128
+ @input << DEFAULT_CREDENTIALS[:server] << "\n"
121
129
  @input << 'y' << "\n"
122
130
  @input.rewind
123
131
 
@@ -132,6 +140,8 @@ describe GoodData::Command::Auth do
132
140
  @input << DEFAULT_CREDENTIALS_OVER[:email] << "\n"
133
141
  @input << DEFAULT_CREDENTIALS_OVER[:password] << "\n"
134
142
  @input << DEFAULT_CREDENTIALS_OVER[:token] << "\n"
143
+ @input << DEFAULT_CREDENTIALS[:environment] << "\n"
144
+ @input << DEFAULT_CREDENTIALS[:server] << "\n"
135
145
  @input << 'n' << "\n"
136
146
  @input.rewind
137
147
 
@@ -10,7 +10,7 @@ describe Hash do
10
10
  }
11
11
  }
12
12
  y = x.dup
13
- deep_y = x.deep_dup
13
+ deep_y = GoodData::Helpers.deep_dup(x)
14
14
 
15
15
  y[:a].object_id.should === x[:a].object_id
16
16
  deep_y[:a].object_id.should_not === x[:a].object_id
@@ -37,14 +37,6 @@ describe GoodData::Helpers do
37
37
  end
38
38
  end
39
39
 
40
- describe '#sanitize_string' do
41
- it 'works' do
42
- expect = 'helloworld'
43
- result = GoodData::Helpers.sanitize_string('Hello World')
44
- result.should == expect
45
- end
46
- end
47
-
48
40
  describe "#decode_params" do
49
41
  it 'decodes the data params from json' do
50
42
  params = {
@@ -57,15 +57,7 @@ describe GoodData::Domain do
57
57
  describe '#users' do
58
58
  it 'Should list users' do
59
59
  users = @domain.users
60
- expect(users).to be_instance_of(Array)
61
- users.each do |user|
62
- expect(user).to be_an_instance_of(GoodData::Profile)
63
- end
64
- end
65
-
66
- it 'Accepts pagination options - limit' do
67
- users = @domain.users(:all, limit: 10)
68
- expect(users).to be_instance_of(Array)
60
+ expect(users).to be_instance_of(Enumerator)
69
61
  users.each do |user|
70
62
  expect(user).to be_an_instance_of(GoodData::Profile)
71
63
  end
@@ -287,25 +287,7 @@ describe GoodData::Model::FromWire do
287
287
  expect(GoodData::Model.check_gd_data_type("decimal(10,5)")).to eq true
288
288
  expect(GoodData::Model.check_gd_data_type("decimal(10, 5)")).to eq true
289
289
  end
290
- #
291
- # it "should be able to omit titles if they are superfluous" do
292
- # view = MultiJson.load(File.read('./spec/data/superfluous_titles_view.json'))
293
- # blueprint = FromWire.from_wire(view)
294
- # expect(blueprint.datasets.count).to eq 1
295
- # expect(blueprint.datasets.first.find_column_by_name('current_status', nil).key?(:title)).to eq false
296
- # expect(blueprint.datasets.mapcat { |ds| ds.columns }.any? {|col| col[:name].titleize == col[:title]}).to eq false
297
- # end
298
- #
299
- # it "should enable sorting" do
300
- # skip("UAAA")
301
- # end
302
- #
303
- # it "should generate the same thing it parsed" do
304
- # a = @model_view['projectModelView']['model']['projectModel']['datasets'][3]
305
- # b = @blueprint.to_wire
306
- # # expect(b).to eq a
307
- # end
308
- #
290
+
309
291
  it "should be able to parse description from both attributes and facts" do
310
292
  expect(@blueprint.find_dataset('dataset.opportunity').anchor.description).to eq 'This is opportunity attribute description'
311
293
  expect(@blueprint.find_dataset('dataset.stage_history').facts.find {|f| f.id == 'fact.stage_history.stage_velocity'}.description).to eq 'Velocity description'