table_print 0.2.3 → 1.0.0.rc3

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.
@@ -0,0 +1,12 @@
1
+ require 'returnable'
2
+
3
+ module Kernel
4
+ def tp(data=[], *options)
5
+ start = Time.now
6
+ printer = TablePrint::Printer.new(data, options)
7
+ puts printer.table_print unless data.is_a? Class
8
+ TablePrint::Returnable.new(Time.now - start) # we have to return *something*, might as well be execution time.
9
+ end
10
+
11
+ module_function :tp
12
+ end
data/lib/printable.rb ADDED
@@ -0,0 +1,22 @@
1
+ module TablePrint
2
+ module Printable
3
+ # Sniff the data class for non-standard methods to use as a baseline for display
4
+ def self.default_display_methods(target)
5
+ return target.class.columns.collect(&:name) if target.class.respond_to? :columns
6
+
7
+ methods = []
8
+ target.methods.each do |method_name|
9
+ method = target.method(method_name)
10
+
11
+ if method.owner == target.class
12
+ if method.arity == 0 #
13
+ methods << method_name.to_s
14
+ end
15
+ end
16
+ end
17
+
18
+ methods.delete_if { |m| m[-1].chr == "!" } # don't use dangerous methods
19
+ methods
20
+ end
21
+ end
22
+ end
data/lib/returnable.rb ADDED
@@ -0,0 +1,21 @@
1
+ module TablePrint
2
+ class Returnable
3
+ def initialize(string_value="")
4
+ @string_value = string_value
5
+ end
6
+
7
+ def set(klass, *config)
8
+ TablePrint::Config.set(klass, config)
9
+ "Set table_print config for #{klass}"
10
+ end
11
+
12
+ def clear(klass)
13
+ TablePrint::Config.clear(klass)
14
+ "Cleared table_print config for #{klass}"
15
+ end
16
+
17
+ def to_s
18
+ @string_value
19
+ end
20
+ end
21
+ end
data/lib/row_group.rb ADDED
@@ -0,0 +1,227 @@
1
+ require 'formatter'
2
+ require 'column'
3
+
4
+ module TablePrint
5
+
6
+ module RowRecursion
7
+ attr_accessor :parent
8
+ attr_accessor :children
9
+
10
+ def initialize
11
+ @children = []
12
+ @columns = {}
13
+ end
14
+
15
+ def add_child(child)
16
+ @children << child
17
+ child.parent = self
18
+ end
19
+
20
+ def insert_children(i, children)
21
+ @children.insert(i, children).flatten!
22
+ children.each {|c| c.parent = self }
23
+ self
24
+ end
25
+
26
+ def add_children(children)
27
+ @children.concat children
28
+ children.each { |c| c.parent = self }
29
+ self
30
+ end
31
+
32
+ def child_count
33
+ @children.length
34
+ end
35
+
36
+ def set_column(name, column)
37
+ return parent.set_column(name, column) if parent
38
+ @columns[name.to_s] = column
39
+ end
40
+
41
+ def columns
42
+ return parent.columns if parent
43
+
44
+ raw_column_names.collect{|k, v| column_for(k)}
45
+ end
46
+
47
+ def column_count
48
+ return parent.column_count if parent
49
+ @columns.size
50
+ end
51
+
52
+ def column_for(name)
53
+ return parent.column_for(name) if parent
54
+ column = @columns[name.to_s] ||= Column.new(:name => name)
55
+
56
+ # assign the data sets to the column before we return it
57
+ # do this as late as possible, since new rows could be added at any time
58
+ column.data ||= raw_column_data(column.name)
59
+ column
60
+ end
61
+
62
+ def width
63
+ return parent.width if parent
64
+ columns.collect(&:width).inject(&:+) + (columns.length - 1) * 3 # add (n-1)*3 for the 3-character separator
65
+ end
66
+
67
+ def horizontal_separator
68
+ '-' * header.length # columns don't know how to respect max_width (formatter does that) so just match the header
69
+ end
70
+
71
+ def header
72
+ padded_names = columns.collect do |column|
73
+ f = FixedWidthFormatter.new(column.width)
74
+ f.format(column.name)
75
+ end
76
+
77
+ padded_names.join(" | ").upcase
78
+ end
79
+
80
+ def add_formatter(name, formatter)
81
+ return unless column_for(name)
82
+ column_for(name).add_formatter(formatter)
83
+ end
84
+ end
85
+
86
+ class RowGroup
87
+ include RowRecursion
88
+
89
+ def initialize
90
+ super
91
+ @skip_first_row = false
92
+ end
93
+
94
+ def raw_column_data(column_name)
95
+ @children.collect { |r| r.raw_column_data(column_name) }.flatten
96
+ end
97
+
98
+ def raw_column_names
99
+ return @raw_column_names if @raw_column_names
100
+ @raw_column_names = @children.collect { |r| r.raw_column_names }.flatten.uniq
101
+ end
102
+
103
+ def vis(prefix="")
104
+ puts "#{prefix}group"
105
+ children.each{|c| c.vis(prefix + " ")}
106
+ end
107
+
108
+ def collapse!
109
+ @children.each(&:collapse!)
110
+ end
111
+
112
+ def format
113
+ rows = @children
114
+ rows = @children[1..-1] if @skip_first_row
115
+ rows ||= []
116
+ rows = rows.collect { |row| row.format }.join("\n")
117
+
118
+ return nil if rows.length == 0
119
+ rows
120
+ end
121
+
122
+ def skip_first_row!
123
+ @skip_first_row = true
124
+ end
125
+ end
126
+
127
+ class Row
128
+ attr_reader :cells
129
+
130
+ include RowRecursion
131
+
132
+ def initialize
133
+ super
134
+ @cells = {}
135
+ end
136
+
137
+ # helpful for debugging
138
+ def vis(prefix="")
139
+ puts "#{prefix}row #{cells.inspect.to_s}"
140
+ children.each{|c| c.vis(prefix + " ")}
141
+ end
142
+
143
+ def collapse!
144
+ children.each(&:collapse!) # depth-first. start collapsing from the bottom and work our way up.
145
+
146
+ to_absorb = []
147
+ children.each do |group|
148
+ next unless can_absorb?(group)
149
+ to_absorb << group
150
+ end
151
+
152
+ to_absorb.each do |absorbable_group|
153
+ absorbable_row = absorbable_group.children.shift
154
+ @cells.merge!(absorbable_row.cells)
155
+
156
+ i = children.index(absorbable_group)
157
+ children.delete(absorbable_group) if absorbable_group.children.empty?
158
+ insert_children(i, absorbable_row.children) if absorbable_row.children.any?
159
+ end
160
+ end
161
+
162
+ def set_cell_values(values_hash)
163
+ values_hash.each do |k, v|
164
+ @cells[k.to_s] = v
165
+ end
166
+ self
167
+ end
168
+
169
+ def format
170
+ column_names = columns.collect(&:name)
171
+
172
+ output = [column_names.collect { |name| apply_formatters(name, @cells[name]) }.join(" | ")]
173
+ output.concat @children.collect { |g| g.format }
174
+
175
+ output.join("\n")
176
+ end
177
+
178
+ def absorb_children(column_names, rollup)
179
+ @children.each do |group|
180
+ next unless can_absorb?(group)
181
+ group.skip_first_row!
182
+
183
+ column_names.collect do |name|
184
+ next unless group.children and group.children.length > 0
185
+ value = group.children.first.cells[name]
186
+ rollup[name] = value if value
187
+ end
188
+ end
189
+ end
190
+
191
+ def raw_column_data(column_name)
192
+ output = [@cells[column_name.to_s]]
193
+ output << @children.collect { |g| g.raw_column_data(column_name) }
194
+ output.flatten
195
+ end
196
+
197
+ def raw_column_names
198
+ output = [@cells.keys]
199
+ output << @children.collect { |g| g.raw_column_names }
200
+ output.flatten.uniq
201
+ end
202
+
203
+ def apply_formatters(column_name, value)
204
+ column_name = column_name.to_s
205
+ return value unless column_for(column_name)
206
+
207
+ column = column_for(column_name)
208
+ formatters = column.formatters || []
209
+
210
+ formatters << TimeFormatter.new(column.time_format)
211
+ formatters << NoNewlineFormatter.new
212
+ formatters << FixedWidthFormatter.new(column_for(column_name).width)
213
+
214
+ # successively apply the formatters for a column
215
+ formatters.inject(value) do |value, formatter|
216
+ formatter.format(value)
217
+ end
218
+ end
219
+
220
+ def can_absorb?(group)
221
+ return true if group.child_count == 1
222
+
223
+ return false if @already_absorbed_a_multigroup
224
+ @already_absorbed_a_multigroup = true # only call this method once
225
+ end
226
+ end
227
+ end
data/lib/table_print.rb CHANGED
@@ -1,405 +1,49 @@
1
- # future work:
2
- #
3
- # allow other output venues besides 'puts'
4
- # allow fine-grained formatting
5
- # on-the-fly column definitions (pass a proc as an include, eg 'tp User.all, :include => {:column_name => "Zodiac", :display_method => lambda {|u| find_zodiac_sign(u.birthday)}}')
6
- # allow user to pass ActiveRelation instead of a data array? That could open up so many options!
7
- # a :short_booleans method could save a little space (replace true/false with T/F or 1/0)
8
- # we could do some really smart stuff with polymorphic relationships, eg reusing photo column for blogs AND books!
9
- #
10
- # bugs
11
- #
12
- # handle multibyte (see https://skitch.com/arches/r3cbg/multibyte-bug)
1
+ require 'column'
2
+ require 'config_resolver'
3
+ require 'config'
4
+ require 'fingerprinter'
5
+ require 'formatter'
6
+ require 'hash_extensions'
7
+ require 'kernel_extensions'
8
+ require 'printable'
9
+ require 'row_group'
13
10
 
14
- class TablePrint
11
+ module TablePrint
12
+ class Printer
15
13
 
16
- attr_accessor :columns, :display_methods, :separator
17
-
18
- def initialize(options = {})
19
- # TODO: make options for things like column order
20
- end
21
-
22
- def tp(data, options = {})
23
- # TODO: show documentation if invoked with no arguments
24
- # TODO: use *args instead of options
25
-
26
- self.separator = options[:separator] || " | "
27
-
28
- stack = Array(data).compact
29
-
30
- if stack.empty?
31
- return "No data."
32
- end
33
-
34
- self.display_methods = get_display_methods(data.first, options) # these are all strings now
35
- unless self.display_methods.length > 0
36
- return stack.inspect.to_s
37
- end
38
-
39
- # make columns for all the display methods
40
- self.columns = {}
41
- self.display_methods.each do |m|
42
- self.columns[m] = ColumnHelper.new(data, m, options[m] || options[m.to_sym])
43
- end
44
-
45
- output = [] # a list of rows. we'll join this with newlines when we're done
46
-
47
- # column headers
48
- row = []
49
- self.display_methods.each do |m|
50
- row << self.columns[m].formatted_header
51
- end
52
- output << row.join(self.separator)
53
-
54
- # a row of hyphens to separate the headers from the data
55
- output << ("-" * output.first.length)
56
-
57
- while stack.length > 0
58
- format_row(stack, output)
59
- end
60
-
61
- output.join("\n")
62
- end
63
-
64
- private
65
-
66
- def format_row(stack, output)
67
-
68
- # method_chain is a dot-delimited list of methods, eg "user.blogs.url". It represents the path from the top-level
69
- # objects to the data_obj.
70
- data_obj, method_chain = stack.shift
71
-
72
- # top level objects don't have a method_chain, give them one so we don't have to null-check everywhere
73
- method_chain ||= ""
74
-
75
- # represent method_chain strings we've seen for this row as a tree of hash keys.
76
- # eg, if we have columns for "user.blogs.url" and "user.blogs.title", we only want to add one set of user.blogs to the stack
77
- method_hash = {}
78
-
79
- # if no columns in this row produce any data, we don't want to append it to the output. eg, if our only columns are
80
- # ["id", "blogs.title"] we don't want to print a blank row for every blog we iterate over. We want to entirely skip
81
- # printing a row for that level of the hierarchy.
82
- found_data = false
83
-
84
- # dive right in!
85
- row = []
86
- self.display_methods.each do |m|
87
- column = self.columns[m]
88
-
89
- # If this column happens to begin a recursion, get those objects on the stack. Pass in the stack-tracking info
90
- # we've saved: method_chain and method_hash.
91
- column.add_stack_objects(stack, data_obj, method_chain, method_hash)
92
-
93
- # all rows show all cells. Even if there's no data we still have to generate an empty cell of the proper width
94
- row << column.formatted_cell_value(data_obj, method_chain)
95
- found_data = true unless row[-1].strip.empty?
96
- end
97
-
98
- output << row.join(self.separator) if found_data
99
- end
100
-
101
- # Sort out the user options into a set of display methods we're going to show. This always returns strings.
102
- def get_display_methods(data_obj, options)
103
- # determine what methods we're going to use
104
-
105
- # using options:
106
- # TODO: maybe rename these a little? cascade/include are somewhat mixed with respect to rails lexicon
107
- # :except - use the default set of methods but NOT the ones passed here
108
- # :include - use the default set of methods AND the ones passed here
109
- # :only - discard the default set of methods in favor of this list
110
- # :cascade - show all methods in child objects
111
-
112
- if options.has_key? :only
113
- display_methods = Array(options[:only]).map { |m| m.to_s }
114
- return display_methods if display_methods.length > 0
115
- else
116
- display_methods = get_default_display_methods(data_obj) # start with what we can deduce
117
- display_methods.concat(Array(options[:include])).map! { |m| m.to_s } # add the includes
118
- display_methods = (display_methods - Array(options[:except]).map! { |m| m.to_s }) # remove the excepts
119
- end
120
-
121
- display_methods.uniq.compact
122
- end
123
-
124
- # Sniff the data class for non-standard methods to use as a baseline for display
125
- def get_default_display_methods(data_obj)
126
- # ActiveRecord
127
- return data_obj.class.columns.collect { |c| c.name } if defined?(ActiveRecord) and data_obj.is_a? ActiveRecord::Base
128
-
129
- methods = []
130
- data_obj.methods.each do |method_name|
131
- method = data_obj.method(method_name)
132
-
133
- if method.owner == data_obj.class
134
- if method.arity == 0 #
135
- methods << method_name.to_s
136
- end
137
- end
138
- end
139
-
140
- methods.delete_if { |m| m[-1].chr == "=" } # don't use assignment methods
141
- methods.delete_if { |m| m[-1].chr == "!" } # don't use dangerous methods
142
- methods.map! { |m| m.to_s } # make any symbols into strings
143
- methods
144
- end
145
-
146
- class ColumnHelper
147
- attr_accessor :field_length, :max_field_length, :method, :name, :options
148
-
149
- def initialize(data, method, options = {})
150
- self.method = method
151
- self.options = options || {} # could have been passed an explicit nil
152
-
153
- self.name = self.options[:name] || method.gsub("_", " ").gsub(".", " > ")
154
-
155
- self.max_field_length = self.options[:max_field_length] || 30
156
- self.max_field_length = [self.max_field_length, 1].max # numbers less than one are meaningless
157
-
158
- initialize_field_length(data)
159
- end
160
-
161
- def formatted_header
162
- "%-#{self.field_length}s" % truncate(self.name.upcase)
163
- end
164
-
165
- def formatted_cell_value(data_obj, method_chain)
166
- cell_value = ""
167
-
168
- # top-level objects don't have method chain. Need to check explicitly whether our method is top-level, otherwise
169
- # if the last method in our chain matches a top-level method we could accidentally print its data in our column.
170
- #
171
- # The method chain is what we've been building up as we were "recursing" through previous objects. You could think of
172
- # it as a prefix for this row. Eg, we could be looping through the columns with a method_chain of "locker.assets",
173
- # indicating that we've recursed down from user to locker and are now interested in printing assets. So
174
- #
175
- unless method_chain == "" and self.method.include? "."
176
- our_method_chain = self.method.split(".")
177
- our_method = our_method_chain.pop
178
-
179
- # check whether the method_chain fully qualifies the path to this particular object. If this is the bottom level
180
- # of object in the tree, and the method_chain matches all the way down, then it's finally time to print this cell.
181
- if method_chain == our_method_chain.join(".")
182
- if data_obj.respond_to? our_method
183
- cell_value = data_obj.send(our_method).to_s.gsub("\n", " ")
184
- end
185
- end
186
- end
187
- "%-#{self.field_length}s" % truncate(cell_value.to_s)
14
+ def self.table_print(data, options={})
15
+ p = new(data, options)
16
+ p.table_print
188
17
  end
189
18
 
190
- # Determine if we need to add some stuff to the stack. If so, put it on top and update the tracking objects.
191
- def add_stack_objects(stack, data_obj, method_chain, method_hash)
192
-
193
- return unless self.add_to_stack?(method_chain, method_hash)
194
-
195
- # TODO: probably a less awkward string method to do this
196
- # current_method is the method we're going to call on our data_obj. Say our column is "locker.assets.url" and
197
- # our chain is "locker", current_method would be "assets"
198
- current_method = get_current_method(method_chain)
199
-
200
- new_stack_objects = []
201
- if current_method != "" and data_obj.respond_to? current_method
202
- new_stack_objects = data_obj.send(current_method)
203
- end
204
-
205
- # Now that we've seen "locker.assets", no need to add it to the stack again for this row! Save it off in method_hash
206
- # so when we hit "locker.assets.caption" we won't add the same assets again.
207
- new_method_chain = method_chain == "" ? current_method : "#{method_chain}.#{current_method}"
208
- method_hash[new_method_chain] = {}
209
-
210
- # TODO: probably a cool array method to do this
211
- # finally - update the stack with the object(s) we found
212
- Array(new_stack_objects).reverse_each do |stack_obj|
213
- stack.unshift [stack_obj, new_method_chain]
214
- end
215
- end
216
-
217
- def add_to_stack?(method_chain, method_hash = {})
218
-
219
- # Check whether we're involved in this row. method_chain lets us know the path we took to find the current set of
220
- # data objects. If our method doesn't act upon those objects, bail.
221
- # eg, if these objects are the result of calling "locker.assets" on top-level user objects, but our method is "blogs.title",
222
- # all we're going to be doing on this row is pushing out empty cells.
223
- return unless self.method.start_with? method_chain
224
-
225
- # check whether another column has already added our objects. if we hit "locker.assets.url" already and we're
226
- # "locker.assets.caption", the assets are already on the stack. Don't want to add them again.
227
- new_method_chain = method_chain == "" ? get_current_method(method_chain) : "#{method_chain}.#{get_current_method(method_chain)}"
228
- return if method_hash.has_key? new_method_chain
229
-
230
- # OK! this column relates to the data object and hasn't been beaten to the punch. But do we have more levels to recurse, or
231
- # is this object on the bottom rung and just needs formatting?
232
-
233
- # if this is the top level, all we need to do is check for a dot, indicating a chain of methods
234
- if method_chain == ""
235
- return method.include? "."
236
- end
237
-
238
- # if this isn't the top level, subtract out the part of the chain we've already called before we check for further chaining
239
- test_method = String.new(method[method_chain.length, method.length])
240
- test_method = test_method[1, test_method.length] if test_method.start_with? "."
241
-
242
- test_method.include? "."
19
+ def initialize(data, options={})
20
+ @data = [data].flatten.compact
21
+ @options = options
22
+ @columns = nil
243
23
  end
244
24
 
245
- private
246
-
247
- # cut off field_value based on our previously determined width
248
- def truncate(field_value)
249
- copy = String.new(field_value)
250
- if copy.length > self.field_length
251
- copy = copy[0..self.field_length-1]
252
- copy[-3..-1] = "..." unless self.field_length <= 3 # don't use ellipses when the string is tiny
25
+ def table_print
26
+ return "No data." if @data.empty?
27
+ group = TablePrint::RowGroup.new
28
+ columns.each do |c|
29
+ group.set_column(c.name, c)
253
30
  end
254
- copy
255
- end
256
-
257
- # determine how wide this column is going to be
258
- def initialize_field_length(data)
259
- # skip all this nonsense if we've been explicitly told what to do
260
- if self.options[:field_length] and self.options[:field_length] > 0
261
- self.field_length = self.options[:field_length]
262
- else
263
- self.field_length = self.name.length # it has to at least be long enough for the column header!
264
31
 
265
- find_data_length(data, self.method, Time.now)
32
+ @data.each do |data|
33
+ group.add_children(Fingerprinter.new.lift(columns, data))
266
34
  end
267
35
 
268
- self.field_length = [self.field_length, self.max_field_length].min # never bigger than the max
269
- end
270
-
271
- # recurse through the data set using the method chain to find the longest field (or until time's up)
272
- def find_data_length(data, method, start)
273
- return if (Time.now - start) > 2
274
- return if method.nil?
275
- return if data.nil?
276
- return if self.field_length >= self.max_field_length
277
-
278
- Array(data).each do |data_obj|
279
- next_method = method.split(".").first
36
+ group.collapse!
37
+ return "No data." if group.columns.empty?
280
38
 
281
- return unless data_obj.respond_to? next_method
282
-
283
- if next_method == method # done!
284
- self.field_length = [self.field_length, data_obj.send(next_method).to_s.length].max
285
- else # keep going
286
- find_data_length(data_obj.send(next_method), method[(next_method.length + 1)..-1], start)
287
- end
288
- end
39
+ [group.header, group.horizontal_separator, group.format].join("\n")
289
40
  end
290
41
 
291
- # add the next level to the method_chain
292
- def get_current_method(method_chain)
293
- if self.method.start_with? method_chain
294
- current_method = String.new(self.method)
295
- current_method = current_method[method_chain.length, current_method.length]
296
- current_method.split(".").detect { |m| m != "" }
297
- end
42
+ def columns
43
+ return @columns if @columns
44
+ defaults = TablePrint::Printable.default_display_methods(@data.first)
45
+ c = TablePrint::ConfigResolver.new(@data.first.class, defaults, @options)
46
+ @columns = c.columns
298
47
  end
299
48
  end
300
-
301
- end
302
-
303
- module Kernel
304
- def tp(data, options = {})
305
- start = Time.now
306
- table_print = TablePrint.new
307
- puts table_print.tp(Array(data), options)
308
- Time.now - start # we have to return *something*, might as well be execution time.
309
- end
310
-
311
- module_function :tp
312
49
  end
313
-
314
- ## Some nested classes to make development easier! Make sure you don't commit these uncommented.
315
- #
316
- #class TestClass
317
- # attr_accessor :title, :name, :blogs, :locker
318
- #
319
- # def initialize(title, name, blogs, locker)
320
- # self.title = title
321
- # self.name = name
322
- # self.blogs = blogs
323
- # self.locker = locker
324
- # end
325
- #end
326
- #
327
- #class Blog
328
- # attr_accessor :title, :summary
329
- #
330
- # def initialize(title, summary)
331
- # self.title = title
332
- # self.summary = summary
333
- # end
334
- #end
335
- #
336
- #class Locker
337
- # attr_accessor :assets
338
- #
339
- # def initialize(assets)
340
- # self.assets = assets
341
- # end
342
- #end
343
- #
344
- #class Asset
345
- # attr_accessor :url, :caption
346
- #
347
- # def initialize(url, caption)
348
- # self.url = url
349
- # self.caption = caption
350
- # end
351
- #end
352
- #
353
- #stack = [
354
- #
355
- # TestClass.new("one title", "one name", [
356
- # Blog.new("one blog title1", "one blog sum1"),
357
- # Blog.new("one blog title2", "one blog sum2"),
358
- # Blog.new("one blog title3", "one blog sum3"),
359
- # ],
360
- # Locker.new([
361
- # Asset.new("one asset url1", "one asset cap1"),
362
- # Asset.new("one asset url2", "one asset cap2"),
363
- # Asset.new("one asset url3", "one asset cap3"),
364
- # ])
365
- # ),
366
- # TestClass.new("two title", "two name", [
367
- # Blog.new("two blog title1", "two blog sum1"),
368
- # Blog.new("two blog title2", "two blog sum2"),
369
- # Blog.new("two blog title3", "two blog sum3"),
370
- # ],
371
- # Locker.new([
372
- # Asset.new("two asset url1", "two asset cap1"),
373
- # Asset.new("two asset url2", "two asset cap2"),
374
- # Asset.new("two asset url3", "two asset cap3"),
375
- # ])
376
- # ),
377
- # TestClass.new("three title", "three name", [
378
- # Blog.new("three blog title1", "three blog sum1"),
379
- # Blog.new("three blog title2", "three blog sum2"),
380
- # Blog.new("three blog title3", "three blog sum3"),
381
- # ],
382
- # Locker.new([
383
- # Asset.new("three asset url1", "three asset cap1"),
384
- # Asset.new("three asset url2", "three asset cap2"),
385
- # Asset.new("three asset url3", "three asset cap3"),
386
- # ])
387
- # ),
388
- # TestClass.new("four title", "four name", [
389
- # Blog.new("four blog title1", "four blog sum1"),
390
- # Blog.new("four blog title2", "four blog sum2"),
391
- # Blog.new("four blog title3", "four blog sum3"),
392
- # ],
393
- # Locker.new([
394
- # Asset.new("four asset url1", "four asset cap1"),
395
- # Asset.new("four asset url2", "four asset cap2"),
396
- # Asset.new("four asset url3", "four asset cap3"),
397
- # ])
398
- # ),
399
- #]
400
-
401
- #tp stack, :include => ["blogs.title", "blogs.summary", "locker.assets.url", "locker.assets.caption"]
402
-
403
-
404
-
405
-