twb 3.9.7 → 4.3.1

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.
@@ -42,8 +42,11 @@ module Twb
42
42
 
43
43
  attr_reader :workbook
44
44
  attr_reader :name, :caption, :uiname
45
+ attr_reader :attributes
45
46
  attr_reader :dsclass, :isExtract
46
- attr_reader :connection, :connections, :connHash, :uuid
47
+ attr_reader :connection, :connections, :connHash, :connAttributes
48
+ attr_reader :isPublished
49
+ attr_reader :uuid
47
50
  attr_reader :tables, :joinPairs
48
51
  attr_reader :localFields, :localFieldNames, :localField, :hasField
49
52
  attr_reader :columnFields
@@ -73,6 +76,24 @@ module Twb
73
76
  return self
74
77
  end
75
78
 
79
+ def attributes
80
+ @attributes ||= @attributes = loadAttributes(@node)
81
+ end
82
+
83
+ def connAttributes
84
+ @connAttributes ||= @connAttributes = loadAttributes(@connection)
85
+ end
86
+
87
+ def loadAttributes node
88
+ attributes = {}
89
+ unless node.nil?
90
+ node.attributes.each do |k,v|
91
+ attributes[k] = v.text
92
+ end
93
+ end
94
+ return attributes
95
+ end
96
+
76
97
  def id
77
98
  @id ||= @id = @name
78
99
  end
@@ -145,6 +166,14 @@ module Twb
145
166
  @isExtract ||= @isExtract = !@node.at_xpath('./extract').nil?
146
167
  end
147
168
 
169
+ def isPublished
170
+ @isPublished ||= loadIsPublished
171
+ end
172
+
173
+ def loadIsPublished
174
+ @isPublished = !node.at_xpath('./repository-location').nil?
175
+ end
176
+
148
177
  def loadTables connection
149
178
  @tables = {}
150
179
  nodes = connection.xpath(".//relation[@type='table']")
@@ -269,8 +298,10 @@ module Twb
269
298
  end
270
299
 
271
300
  def fieldAlias fieldName, value
272
- return value unless fieldHasAliases(fieldName)
273
- fldAlias = aliases[fieldName][value]
301
+ emit "fieldAlias: #{fieldName.class} -> #{value.class}"
302
+ loadAliases if @aliases.nil?
303
+ return value if @aliases.nil? || @aliases[fieldName].nil? # unless fieldHasAliases(fieldName)
304
+ fldAlias = @aliases[fieldName][value]
274
305
  fldAlias.nil? ? value : fldAlias
275
306
  end
276
307
 
@@ -336,7 +367,7 @@ module Twb
336
367
 
337
368
  def fieldUIName fieldName
338
369
  loadFieldUINames if @fieldUINames.nil?
339
- @fieldUINames[fieldName]
370
+ @fieldUINames[fieldName].nil? ? fieldName : @fieldUINames[fieldName]
340
371
  end
341
372
 
342
373
  def fieldUINames
@@ -38,7 +38,7 @@ module Twb
38
38
  attr_reader :fields, :remoteFields, :calcFields
39
39
  attr_reader :comments, :uuid
40
40
 
41
- attr_accessor :ttlogfile
41
+ # attr_accessor :ttlogfile
42
42
 
43
43
  @@tableCalcs = [ 'FIRST', 'INDEX', 'LAST', 'SIZE',
44
44
  'LOOKUP', 'PREVIOUS_VALUE',
@@ -53,16 +53,15 @@ module Twb
53
53
  'WINDOW_VAR', 'WINDOW_VARP'
54
54
  ]
55
55
 
56
- @ttlogfile =
57
-
58
56
  def initialize(calcField, datasource=nil)
59
57
  raise ArgumentError.new("FieldCalculation must be initialized with a CalculatedField, has been provided with a #{calcField.class}") if calcField.class != Twb::CalculatedField
58
+ init
60
59
  # initLogger
61
60
  @field = calcField
62
61
  calcNode = calcField.node
63
62
  @istableCalc = false
64
63
  # @localEmit = false
65
- # puts "FieldCalculation calcNode.nil? :: #{calcNode.nil?} "
64
+ emit "FieldCalculation calcNode.nil? :: #{calcNode.nil?} "
66
65
  unless calcNode.nil?
67
66
  @node = calcNode.at_xpath('./calculation')
68
67
  @fieldNode = calcField.node
@@ -94,7 +93,7 @@ module Twb
94
93
  @has_formula = @node.has_attribute?('formula')
95
94
  if @has_formula
96
95
  @formula = @node.attribute('formula').text.gsub(/\r\n/,"\n")
97
- # puts "\n-- init: #{@formula}"
96
+ emit "\n-- init: #{@formula}"
98
97
  @formulaUC = @formula.upcase
99
98
  @formulaLines = formula.split(/\n|\r\n/)
100
99
  @formulaFlat = flattenFormula(@formulaLines)
@@ -108,12 +107,6 @@ module Twb
108
107
  end
109
108
  end
110
109
 
111
- # def initLogger(logfile=nil)
112
- # logfilename = docFile(logfile.nil? ? @ttlogfile : logfile)
113
- # @logger = Logger.new(logfilename)
114
- # @logger.level = Logger::DEBUG
115
- # end
116
-
117
110
  def id
118
111
  @id ||= @id = @formulaFlat.hash + calcField.hash
119
112
  end
@@ -122,10 +115,6 @@ module Twb
122
115
  @uuid ||= @uuid = Digest::MD5.hexdigest(@formula)
123
116
  end
124
117
 
125
- # def assessTableCalc formula
126
- # @@tableCalcs.any? { |tc| string.include?(tc) }
127
- # end
128
-
129
118
  def attribText(node, attribute)
130
119
  node.attribute(attribute).nil? ? nil : node.attribute(attribute).text
131
120
  end
@@ -23,7 +23,7 @@ module Twb
23
23
  include TabTool
24
24
 
25
25
  attr_reader :field, :name, :uiname
26
- attr_reader :type
26
+ attr_reader :type, :kind
27
27
  attr_reader :dataSource
28
28
  attr_reader :node, :values
29
29
  attr_reader :inexclude, :inexMode, :includeNull
@@ -32,8 +32,9 @@ module Twb
32
32
  init
33
33
  emit "\nFILTER:\n#{node}\n====="
34
34
  @node = node
35
- filterClass = node['class']
35
+ filterClass = @node['class']
36
36
  @type = filterClass.gsub('-',' ').capitalize
37
+ @kind = @node['kind']
37
38
  fieldCode = node['column']
38
39
  codedField = Twb::CodedField.new(fieldCode)
39
40
  fieldTech = codedField.name
@@ -83,7 +84,7 @@ module Twb
83
84
  when 'quantitative' then resolveQuantitative
84
85
  when 'categorical' then resolveCategoricalValues
85
86
  end
86
- end
87
+ end # def initialize
87
88
 
88
89
  def to_s
89
90
  "%s => %s" % [uiname, values]
@@ -93,7 +94,7 @@ module Twb
93
94
 
94
95
  def recordValue value, valias=nil
95
96
  # puts "recordValue--: #{value} :: @'#{valias}'"
96
- valias = value if valias.nil?
97
+ # valias = value if valias.nil?
97
98
  @values << {:value => value, :alias => valias}
98
99
  # puts "recordValue--: done"
99
100
  end
@@ -194,9 +195,8 @@ module Twb
194
195
  # <filter class='categorical' column='[Sample - Superstore - English (Extract)].[none:Region:nk]'>
195
196
  # <groupfilter from='&quot;East&quot;' function='range' level='[none:Region:nk]' to='&quot;West&quot;' user:ui-domain='relevant' user:ui-enumeration='inclusive' user:ui-marker='enumerate' />
196
197
  # </filter>
197
- #--
198
198
  def resolveCategoricalValues
199
- emit "resolveCategoricalValues"
199
+ emit "########################## resolveCategoricalValues"
200
200
  emit "@measureNames: #{@measureNames}"
201
201
  if @node.element_children.empty?
202
202
  # <filter class='categorical' column='[Sample - Superstore].[Top Customers by Profit (copy)]' />
@@ -337,8 +337,8 @@ module Twb
337
337
  recordValue range, range
338
338
  end
339
339
  t.each do |name,alia|
340
- # emit "%%%% range Name: %-20s ALIAS: %-s " % [name, alia]
341
- recordValue name, @dataSource.fieldAlias(@uiname,name)
340
+ # emit true, "++++ range Name: %-20s ALIAS: %-s " % [name, alia]
341
+ # recordValue name, @dataSource.fieldAlias(@uiname,name)
342
342
  end
343
343
  end
344
344
  end
@@ -346,6 +346,7 @@ module Twb
346
346
  end
347
347
 
348
348
  def filtersFromRangeNode node
349
+ emit "########################## filtersFromRangeNode"
349
350
  unless @twbDomainsLoaded
350
351
  loadDomains
351
352
  end
@@ -368,7 +369,7 @@ module Twb
368
369
  end
369
370
 
370
371
  def filtersInRange from, to
371
- emit "filtersInRange"
372
+ emit "########################## filtersInRange"
372
373
  # results = {}
373
374
  dsFields = @twbFielddomains[@dataSource.uiname]
374
375
  emit "dsFields : #{dsFields}"
@@ -16,12 +16,14 @@
16
16
  require 'logger'
17
17
  require 'nokogiri'
18
18
  require 'digest/md5'
19
+ require 'yaml'
19
20
 
20
21
  module TabTool
21
22
 
22
23
  @@licensed = false
23
24
 
24
- TTDOCDIR = './ttdoc'
25
+ @@TTDOCDIR = './ttdoc'
26
+ @configFile = './ttconfig.yml'
25
27
 
26
28
  attr_accessor :ttdocdir, :logger, :loglevel, :logfilename
27
29
  attr_accessor :uuid, :type, :id, :properties
@@ -42,9 +44,9 @@ module TabTool
42
44
  end
43
45
 
44
46
  def initDocDir
45
- # return if TTDOCDIR.nil?
46
- # return if ''.eql?($ttdocdir) && ''.eql?(TTDOCDIR)
47
- @docDir = TTDOCDIR
47
+ # return if @@TTDOCDIR.nil?
48
+ # return if ''.eql?($ttdocdir) && ''.eql?(@@TTDOCDIR)
49
+ @docDir = @@TTDOCDIR
48
50
  return if Dir.exists?(@docDir)
49
51
  if File.exists? @docDir
50
52
  @docDir = ''
@@ -55,9 +57,12 @@ module TabTool
55
57
  end
56
58
 
57
59
  def initLogger
58
- logFileName = docFile("#{self.class.to_s.split('::').last}.ttlog")
59
- @logger = Logger.new(logFileName, 2, 1000*1024)
60
- @logger.level = Logger::DEBUG
60
+ @logger = nil
61
+ if config(:log)
62
+ logFileName = docFile("#{self.class.to_s.split('::').last}.ttlog")
63
+ @logger = Logger.new(logFileName)
64
+ @logger.level = Logger::DEBUG
65
+ end
61
66
  return @logger
62
67
  end
63
68
 
@@ -77,6 +82,23 @@ module TabTool
77
82
  @funcdoc.nil? ? {:class=>'n/a', :blurb=>'generic TabTool blurb', :description=>'A useful Tableau Tool.'} : @funcdoc
78
83
  end
79
84
 
85
+ def hasConfig param
86
+ loadConfig if @configParams.nil?
87
+ @configParams[param] ? true : false
88
+ end
89
+
90
+ def config param
91
+ hasConfig(param) ? @configParams[param] : nil
92
+ end
93
+
94
+ def loadConfig
95
+ @configParams = if File.exist?('./ttconfig.yml')
96
+ YAML.load( File.open('./ttconfig.yml').read )
97
+ else
98
+ {}
99
+ end
100
+ end
101
+
80
102
  def docfiles
81
103
  @docfiles ||= @docfiles = []
82
104
  end
@@ -95,21 +117,28 @@ module TabTool
95
117
  return maxlen
96
118
  end
97
119
 
98
- def docfilesdoc
120
+ def docfilesdoc(pad=' ',desc='For documentation and generated data see the following:')
99
121
  lines = SortedSet.new
122
+ paddesc = "#{pad}#{desc}"
100
123
  unless @docfiles.nil? || @docfiles.empty?
101
124
  nameLen = docFileMaxNameLen
102
125
  docfiles.each do |dfi|
103
- lines << " - %-#{nameLen}s %-s " % [ dfi[:name], dfi[:description] ]
126
+ lines << "#{pad}- %-#{nameLen}s %-s " % [ dfi[:name], dfi[:description] ]
104
127
  end
105
128
  end
106
- docLines = lines.empty? ? [] : [' ',' For documentation and generated data see the following:',' ']
129
+ docLines = lines.empty? || paddesc =~ /^[ ]+/ ? [] : [' ',"#{pad}#{desc}"]
107
130
  lines.each do |l|
108
131
  docLines << l
109
132
  end
110
133
  return docLines
111
134
  end
112
135
 
136
+ def docfilesdocto_s(pad=' ',desc='For documentation and generated data see the following:')
137
+ str = ''
138
+ docfilesdoc(pad,desc).each { |l| str += "#{l}\n"}
139
+ return str
140
+ end
141
+
113
142
  # def metrics
114
143
  # {}
115
144
  # end
@@ -154,6 +183,5 @@ module TabTool
154
183
  # @logger.close unless @logger.nil? || @logger.closed?
155
184
  end
156
185
 
157
-
158
186
  end # module TabTool
159
187
 
@@ -1,4 +1,3 @@
1
-
2
1
  # Copyright (C) 2014, 2018 Chris Gerrard
3
2
  #
4
3
  # This program is free software: you can redistribute it and/or modify
@@ -25,11 +24,14 @@ module Twb
25
24
  #
26
25
  class Workbook < TabClass
27
26
 
28
- attr_reader :workbooknode
29
- attr_reader :name, :dir
27
+ attr_reader :workbooknode, :node
28
+ attr_reader :name, :dir, :type
30
29
  attr_reader :modtime, :version, :build
30
+ #--
31
31
  attr_reader :datasources, :datasource
32
32
  attr_reader :datasourceNames, :datasourceUINames, :dataSourceNamesMap
33
+ attr_reader :orphanDataSources # i.e. not referenced in any Worksheet
34
+ #--
33
35
  attr_reader :dashboards, :storyboards, :worksheets
34
36
  attr_reader :parameters, :actions
35
37
  attr_reader :valid, :ndoc
@@ -66,18 +68,21 @@ module Twb
66
68
  Zip::File.open(twbxWithDir) do |zip_file|
67
69
  twb = zip_file.glob('*.twb').first
68
70
  @ndoc = Nokogiri::XML(twb.get_input_stream)
71
+ @type = :twbx
69
72
  processDoc
70
73
  end
71
74
  end
72
75
 
73
76
  def processTWB(twbFile)
74
- @ndoc = Nokogiri::XML(open(twbFile))
77
+ @ndoc = Nokogiri::XML(open(twbFile))
78
+ @type = :twb
75
79
  processDoc
76
80
  end
77
81
 
78
82
  def processDoc
79
- @workbooknode = @ndoc.at_xpath('//workbook')
80
- @version = @ndoc.xpath('/workbook/@version').first.text
83
+ @node = @ndoc.at_xpath('//workbook')
84
+ @workbooknode = @node
85
+ @version = @node.nil? ? nil : @node["version"]
81
86
  loaddatasources
82
87
  loadWorksheets
83
88
  loadDashboards
@@ -204,6 +209,20 @@ module Twb
204
209
  @dashboards[name]
205
210
  end
206
211
 
212
+ def orphanDataSources
213
+ @orphanDataSources ||= identifyOrphandatasoUrceS
214
+ end
215
+
216
+ def identifyOrphandatasoUrceS
217
+ sheetDataSources = Set.new
218
+ @worksheets.values.each do |sheet|
219
+ sheet.datasources.each do |ds|
220
+ sheetDataSources << ds.uiname
221
+ end
222
+ end
223
+ @orphanDataSources = @datasourceUINames - sheetDataSources
224
+ end
225
+
207
226
  def storyboards
208
227
  @storyboards.values
209
228
  end
@@ -283,12 +302,15 @@ module Twb
283
302
  # Write the TWB to a file, with an optional name.
284
303
  # Can be used to write over the existing TWB (dangerous), or to a new file (preferred).
285
304
  def write(name=@name)
286
- $f = File.open(name,'w')
287
- if $f
288
- $f.puts @ndoc
289
- $f.close
305
+ case @type
306
+ when :twb
307
+ writeTwb(name)
308
+ when :twbx
309
+ writeTwbx(name)
310
+ else
311
+ emit "Cannot write this Workbook - it has an invalid type: #{@type}"
312
+ raise "Cannot write this Workbook - it has an invalid type: #{@type}"
290
313
  end
291
- return name
292
314
  end
293
315
 
294
316
  # Write the TWB to a file, appending the base name with the provided string.
@@ -298,7 +320,22 @@ module Twb
298
320
  write newName
299
321
  end
300
322
 
301
- private
323
+ private
324
+
325
+ def writeTwb(name=@name)
326
+ $f = File.open(name,'w')
327
+ if $f
328
+ $f.puts @ndoc
329
+ $f.close
330
+ end
331
+ return name
332
+ end
333
+
334
+ def writeTwbx(name=@name)
335
+ emit "Writing the Workbook, need implementation"
336
+ end
337
+
338
+
302
339
 
303
340
  def loadParameters
304
341
  @parameters = {}
@@ -24,16 +24,20 @@ module Twb
24
24
 
25
25
  @@hasher = Digest::SHA256.new
26
26
 
27
+ @fieldEncodingsXPath = './table/panes/pane//encodings'
28
+
27
29
  attr_reader :node, :name, :datasourcenames, :datasources
28
30
  attr_reader :panesCount
29
- attr_reader :fields, :rowFields, :colFields, :paneFields, :datasourceFields
31
+ attr_reader :fields, :rowFields, :colFields, :paneFields, :datasourceFields, :pageFields, :encodedFields
30
32
  attr_reader :filters
33
+ attr_reader :tooltip
31
34
  attr_reader :hidden, :visible
32
35
 
33
36
  def initialize sheetNode, twb
34
37
  @twb = twb
35
38
  @node = sheetNode
36
- @name = @node.attr('name')
39
+ @name = @node['name']
40
+ emit "########################## Worksheet initialize name: #{@name}"
37
41
  loadDataSourceNames
38
42
  loadFields
39
43
  return self
@@ -77,6 +81,10 @@ module Twb
77
81
  @filters ||= loadFilters
78
82
  end
79
83
 
84
+ def tooltip
85
+ @tooltip ||= loadTooltip
86
+ end
87
+
80
88
  def hidden
81
89
  @hidden ||= resolveHidden
82
90
  end
@@ -105,8 +113,39 @@ module Twb
105
113
  @fields[:rows] = @rowFields
106
114
  @colFields ||= loadRowColFields(:cols) # returns map of data source => (Set of) field names
107
115
  @fields[:cols] = @colFields
116
+ loadFieldEncodings
117
+ end
118
+
119
+ def loadFieldEncodings
120
+ @encodedFields = Hash.new { |h,k| h[k] = [] }
121
+ enodes = node.xpath('.//table/panes/pane/encodings')
122
+ enodes.each do |enode|
123
+ enode.children.each do |child|
124
+ unless child['column'].nil?
125
+ @encodedFields[child.name] << CodedField.new(child['column'])
126
+ end
127
+ end
128
+ end
129
+ @encodedFields.each do |type, fields|
130
+ @fields[type] = fields
131
+ end
108
132
  end
109
133
 
134
+ # def loadFieldEncodings node
135
+ # $encodedFields = Hash.new { |h,k| h[k] = [] }
136
+ # enodes = node.xpath('.//table/panes/pane/encodings')
137
+ # enodes.each do |enode|
138
+ # enode.children.each do |child|
139
+ # unless child['column'].nil?
140
+ # $encodedFields[child.name] << CodedField.new(child['column'])
141
+ # end
142
+ # end
143
+ # end
144
+ # $encodedFields.each do |type, fields|
145
+ # $fields[type] = fields
146
+ # end
147
+ # end
148
+
110
149
  def addDSFields fields, usage
111
150
  fields.each do
112
151
  end
@@ -119,12 +158,16 @@ module Twb
119
158
  def loadRowColFields(type)
120
159
  fields = []
121
160
  xpath = case type
122
- when :rows then './/rows'
123
- when :cols then './/cols'
161
+ when :rows then './table/rows'
162
+ when :cols then './table/cols'
124
163
  end
125
164
  return fields if xpath.nil?
126
165
  node = @node.at_xpath(xpath)
127
- RowsColsSplitter.new(node).fields.each do |fcode|
166
+ emit "def loadRowColFields:: type: #{type} \t node: #{node.class} \t path: #{node.nil? ? '<<nil>>' : node.path }"
167
+ emit "node : #{node.inspect}"
168
+ nodeFields = RowsColsSplitter.new(node).fields
169
+ emit "nodeFields: #{nodeFields}"
170
+ nodeFields.each do |fcode|
128
171
  fields << CodedField.new(fcode)
129
172
  end
130
173
  return fields
@@ -139,18 +182,32 @@ module Twb
139
182
  panes = @node.xpath('.//pane')
140
183
  end
141
184
 
185
+ def pageFields
186
+ @pageFields ||= loadpageFields
187
+ end
188
+
189
+ def loadpageFields
190
+ @pageFields = [] # { |h,k| h[k] = [] }
191
+ nodes = node.xpath('.//table/pages/column')
192
+ nodes.each do |node|
193
+ @pageFields << CodedField.new(node.text)
194
+ end
195
+ return @pageFields
196
+ end
197
+
142
198
  def datasourcenames
143
199
  @datasources.keys
144
200
  end
145
201
 
146
202
  def resolveHidden
147
- windowNode = node.at_xpath("//windows/window[@name='#{@name}']")
203
+ windowNode = node.at_xpath("//windows/window[@name=\"#{@name}\"]")
148
204
  @hidden = !windowNode.nil? && 'true' == windowNode['hidden']
149
205
  end
150
206
 
151
207
  private
152
208
 
153
209
  def loadFilters
210
+ emit ""########################## loadFilters"
154
211
  @filters = []
155
212
  filterNodes = @node.xpath('./table/view//filter[@column]')
156
213
  filterNodes.each do |fnode|
@@ -159,6 +216,10 @@ module Twb
159
216
  end
160
217
  return @filters
161
218
  end
219
+
220
+ def loadTooltip
221
+ @tooltip = @node.xpath('./table/panes/pane/customized-tooltip')
222
+ end
162
223
 
163
224
  end # class Worksheet
164
225
 
@@ -201,7 +262,9 @@ module Twb
201
262
  return fields if node.nil?
202
263
  unless node.text.nil?
203
264
  codes = node.text.split(/[\])] [\/+*] [(]?\[/)
204
- codes.each {|c| @fields << c.gsub(/^[(]*[\[]*|[)\]]*$/,'')}
265
+ codes.each do |c|
266
+ @fields << c.gsub(/^[(]*[\[]*|[)\]]*$/,'')
267
+ end
205
268
  end
206
269
  end
207
270
  end # class RowsColsSplitter