table_print 0.2.3 → 1.0.0.rc3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
-