tabletastic 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -126,11 +126,10 @@ will produce html like:
126
126
  For even greater flexibility, you can pass +data+ a block:
127
127
 
128
128
  <% table_for(@posts) do |t| %>
129
- <% t.data do %>
129
+ <% t.data :actions => :all do %>
130
130
  <%= t.cell(:title, :cell_html => {:class => "titlestring"}) %>
131
131
  <%= t.cell(:body, :heading => "Content") {|p| truncate(p.body, 30)} %>
132
132
  <%= t.cell(:author) {|p| p.author && link_to(p.author.name, p.author) } %>
133
- <%= t.cell(:edit, :heading => '') {|p| link_to "Edit", edit_post_path(p) } %>
134
133
  <% end -%>
135
134
  <% end %>
136
135
 
@@ -143,6 +142,8 @@ will product html like:
143
142
  <th>Content</th>
144
143
  <th>Author</th>
145
144
  <th></th>
145
+ <th></th>
146
+ <th></th>
146
147
  </tr>
147
148
  </thead>
148
149
  <tbody>
@@ -152,9 +153,15 @@ will product html like:
152
153
  <td>
153
154
  <a href="/authors/1">Jim Bean</a>
154
155
  </td>
155
- <td>
156
+ <td class="actions show_link">
157
+ <a href="/posts/1">Show</a>
158
+ </td>
159
+ <td class="actions edit_link">
156
160
  <a href="/posts/1/edit">Edit</a>
157
161
  </td>
162
+ <td class="actions destroy_link">
163
+ <a href="/posts/1/edit">Destroy</a> <!-- inline javascript omitted -->
164
+ </td>
158
165
  </tr>
159
166
  <tr class="post even" id="post_2">
160
167
  <td class="titlestring">Second Post</td>
@@ -162,17 +169,29 @@ will product html like:
162
169
  <td>
163
170
  <a href="/authors/2">Jack Daniels</a>
164
171
  </td>
165
- <td>
172
+ <td class="actions show_link">
173
+ <a href="/posts/2">Show</a>
174
+ </td>
175
+ <td class="actions edit_link">
166
176
  <a href="/posts/2/edit">Edit</a>
167
177
  </td>
178
+ <td class="actions destroy_link">
179
+ <a href="/posts/2/edit">Destroy</a> <!-- inline javascript omitted -->
180
+ </td>
168
181
  </tr>
169
182
  <tr class="post odd" id="post_3">
170
183
  <td class="titlestring">Something else</td>
171
184
  <td>Blah!</td>
172
185
  <td></td>
173
- <td>
186
+ <td class="actions show_link">
187
+ <a href="/posts/3">Show</a>
188
+ </td>
189
+ <td class="actions edit_link">
174
190
  <a href="/posts/3/edit">Edit</a>
175
191
  </td>
192
+ <td class="actions destroy_link">
193
+ <a href="/posts/3/edit">Destroy</a> <!-- inline javascript omitted -->
194
+ </td>
176
195
  </tr>
177
196
  </tbody>
178
197
  </table>
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.1.1
data/lib/tabletastic.rb CHANGED
@@ -2,40 +2,85 @@ module Tabletastic
2
2
 
3
3
  # returns and outputs a table for the given active record collection
4
4
  def table_for(collection, *args)
5
+ klass = default_class_for(collection)
5
6
  options = args.extract_options!
6
7
  options[:html] ||= {}
7
- options[:html][:id] ||= get_id_for(collection)
8
+ options[:html][:id] ||= get_id_for(klass)
8
9
  concat(tag(:table, options[:html], true))
9
- yield TableBuilder.new(collection, self)
10
+ yield TableBuilder.new(collection, klass, self)
10
11
  concat("</table>")
11
12
  end
12
13
 
13
- def get_id_for(collection)
14
- !collection.empty? && collection.first.class.to_s.tableize
14
+ private
15
+ # Finds the class representing the objects within the collection
16
+ def default_class_for(collection)
17
+ if collection.respond_to?(:proxy_reflection)
18
+ collection.proxy_reflection.klass
19
+ elsif !collection.empty?
20
+ collection.first.class
21
+ end
22
+ end
23
+
24
+ def get_id_for(klass)
25
+ klass.to_s.tableize
15
26
  end
16
27
 
17
28
  class TableBuilder
18
29
  @@association_methods = %w[display_name full_name name title username login value to_s]
19
30
  attr_accessor :field_labels
31
+ attr_reader :collection, :klass
20
32
 
21
- def initialize(collection, template)
22
- @collection, @template = collection, template
33
+ def initialize(collection, klass, template)
34
+ @collection, @klass, @template = collection, klass, template
23
35
  end
24
36
 
25
37
  # builds up the fields that the table will include,
26
38
  # returns table head and body with all data
27
- def data(*args, &block)
39
+ #
40
+ # Can be used one of three ways:
41
+ #
42
+ # * Alone, which will try to detect all content columns on the resource
43
+ # * With an array of methods to call on each element in the collection
44
+ # * With a block, which assumes you will use +cell+ method to build up
45
+ # the table
46
+ #
47
+ #
48
+ def data(*args, &block) # :yields: tablebody
49
+ options = args.extract_options!
28
50
  if block_given?
29
51
  yield self
52
+ action_cells(options[:actions])
30
53
  @template.concat(head)
31
54
  @template.concat(body)
32
55
  else
33
- @fields = args unless args.empty?
56
+ @fields = args.empty? ? fields : args
34
57
  @field_labels = fields.map { |f| f.to_s.humanize }
58
+ action_cells(options[:actions])
35
59
  [head, body].join("")
36
60
  end
37
61
  end
38
62
 
63
+ # individually specify a column, which will build up the header,
64
+ # and method or block to call on each resource in the array
65
+ #
66
+ # Should always be called within the block of +data+
67
+ #
68
+ # For example:
69
+ #
70
+ # t.cell :blah
71
+ #
72
+ # will simply call +blah+ on each resource
73
+ #
74
+ # You can also provide a block, which allows for other helpers
75
+ # or custom formatting. Since by default erb will just call +to_s+
76
+ # on an any element output, you can more greatly control the output:
77
+ #
78
+ # t.cell(:price) {|resource| number_to_currency(resource)}
79
+ #
80
+ # would output something like:
81
+ #
82
+ # <td>$1.50</td>
83
+ #
39
84
  def cell(*args, &block)
40
85
  options = args.extract_options!
41
86
  @field_labels ||= []
@@ -43,12 +88,16 @@ module Tabletastic
43
88
 
44
89
  method_or_attribute = args.first.to_sym
45
90
 
91
+ method_or_attribute_or_proc = if block_given?
92
+ block.to_proc
93
+ else
94
+ method_or_attribute
95
+ end
96
+
46
97
  if cell_html = options.delete(:cell_html)
47
- @fields << [method_or_attribute, cell_html]
48
- elsif block_given?
49
- @fields << block.to_proc
98
+ @fields << [method_or_attribute_or_proc, cell_html]
50
99
  else
51
- @fields << method_or_attribute
100
+ @fields << method_or_attribute_or_proc
52
101
  end
53
102
 
54
103
  if heading = options.delete(:heading)
@@ -56,8 +105,9 @@ module Tabletastic
56
105
  else
57
106
  @field_labels << method_or_attribute.to_s.humanize
58
107
  end
59
-
60
- return "" # Since this will likely be called with <%= erb %>, this suppresses strange output
108
+ # Since this will likely be called with <%= %> (aka 'concat'), explicitly return an empty string
109
+ # This suppresses unwanted output
110
+ return ""
61
111
  end
62
112
 
63
113
  def head
@@ -85,12 +135,12 @@ module Tabletastic
85
135
  @collection.inject("") do |rows, record|
86
136
  rowclass = @template.cycle("odd","even")
87
137
  rows += @template.content_tag_for(:tr, record, :class => rowclass) do
88
- tds_for_row(record)
138
+ cells_for_row(record)
89
139
  end
90
140
  end
91
141
  end
92
142
 
93
- def tds_for_row(record)
143
+ def cells_for_row(record)
94
144
  fields.inject("") do |cells, field_or_array|
95
145
  field = field_or_array
96
146
  if field_or_array.is_a?(Array)
@@ -113,9 +163,34 @@ module Tabletastic
113
163
  result.send(to_string) if to_string
114
164
  end
115
165
 
166
+ # Used internally to build up cells for common CRUD actions
167
+ def action_cells(actions)
168
+ return if actions.blank?
169
+ actions = [actions] if !actions.respond_to?(:each)
170
+ actions = [:show, :edit, :destroy] if actions == [:all]
171
+ actions.each do |action|
172
+ action_link(action.to_sym)
173
+ end
174
+ end
175
+
176
+ # Dynamically builds links for the action
177
+ def action_link(action)
178
+ html_class = "actions #{action.to_s}_link"
179
+ self.cell(action, :heading => "", :cell_html => {:class => html_class}) do |resource|
180
+ case action
181
+ when :show
182
+ @template.link_to("Show", resource)
183
+ when :edit
184
+ @template.link_to("Edit", @template.polymorphic_path(resource, :action => :edit))
185
+ when :destroy
186
+ @template.link_to("Destroy", resource, :method => :delete)
187
+ end
188
+ end
189
+ end
190
+
116
191
  def fields
117
192
  return @fields if defined?(@fields)
118
- @fields = @collection.empty? ? [] : active_record_fields_for_object(@collection.first)
193
+ @fields = @collection.empty? ? [] : active_record_fields
119
194
  end
120
195
 
121
196
  protected
@@ -125,13 +200,14 @@ module Tabletastic
125
200
  end
126
201
 
127
202
 
128
- def active_record_fields_for_object(obj)
203
+ def active_record_fields
204
+ return [] if klass.blank?
129
205
  # normal content columns
130
- fields = obj.class.content_columns.map(&:name)
206
+ fields = klass.content_columns.map(&:name)
131
207
 
132
208
  # active record associations
133
- associations = obj.class.reflect_on_all_associations(:belongs_to) if obj.class.respond_to?(:reflect_on_all_associations)
134
- if associations
209
+ if klass.respond_to?(:reflect_on_all_associations)
210
+ associations = klass.reflect_on_all_associations(:belongs_to)
135
211
  associations = associations.map(&:name)
136
212
  fields += associations
137
213
  end
@@ -141,16 +217,18 @@ module Tabletastic
141
217
  fields = fields.map(&:to_sym)
142
218
  end
143
219
 
220
+ def content_tag(name, content = nil, options = nil, escape = true, &block)
221
+ @template.content_tag(name, content, options, escape, &block)
222
+ end
223
+
224
+ private
225
+
144
226
  def send_or_call(object, duck)
145
- if duck.is_a?(Proc)
227
+ if duck.respond_to?(:call)
146
228
  duck.call(object)
147
229
  else
148
230
  object.send(duck)
149
231
  end
150
232
  end
151
-
152
- def content_tag(name, content = nil, options = nil, escape = true, &block)
153
- @template.content_tag(name, content, options, escape, &block)
154
- end
155
233
  end
156
234
  end
@@ -129,6 +129,57 @@ describe "Tabletastic#table_for" do
129
129
  end
130
130
  end
131
131
 
132
+ context "with options[:actions]" do
133
+ it "includes path to post for :show" do
134
+ table_for(@posts) do |t|
135
+ concat(t.data(:actions => :show))
136
+ end
137
+ output_buffer.should have_table_with_tag("a[@href=/posts/#{@post.id}]")
138
+ output_buffer.should have_table_with_tag("th", "")
139
+ end
140
+
141
+ it "should have a cell with default class 'actions' and the action name" do
142
+ table_for(@posts) do |t|
143
+ concat(t.data(:actions => :show))
144
+ end
145
+ output_buffer.should have_tag("td.actions.show_link") do |td|
146
+ td.should have_tag("a")
147
+ end
148
+ end
149
+
150
+ it "includes path to post for :edit" do
151
+ table_for(@posts) do |t|
152
+ concat(t.data(:actions => :edit))
153
+ end
154
+ output_buffer.should have_tag("a[@href=/posts/#{@post.id}/edit]", "Edit")
155
+ end
156
+
157
+ it "includes path to post for :destroy" do
158
+ table_for(@posts) do |t|
159
+ concat(t.data(:actions => :destroy))
160
+ end
161
+ output_buffer.should have_table_with_tag("a[@href=/posts/#{@post.id}]", "Destroy")
162
+ output_buffer.should have_table_with_tag("th", "")
163
+ end
164
+
165
+ it "includes path to post for :show and :edit" do
166
+ table_for(@posts) do |t|
167
+ concat(t.data(:actions => [:show, :edit]))
168
+ end
169
+ output_buffer.should have_tag("td:nth-child(3) a[@href=/posts/#{@post.id}]", "Show")
170
+ output_buffer.should have_tag("td:nth-child(4) a[@href=/posts/#{@post.id}/edit]", "Edit")
171
+ end
172
+
173
+ it "includes path to post for :all" do
174
+ table_for(@posts) do |t|
175
+ concat(t.data(:actions => :all))
176
+ end
177
+ output_buffer.should have_tag("td:nth-child(3) a[@href=/posts/#{@post.id}]", "Show")
178
+ output_buffer.should have_tag("td:nth-child(4) a[@href=/posts/#{@post.id}/edit]", "Edit")
179
+ output_buffer.should have_tag("td:nth-child(5) a[@href=/posts/#{@post.id}]", "Destroy")
180
+ end
181
+ end
182
+
132
183
  context "with a list of attributes" do
133
184
  before do
134
185
  table_for(@posts) do |t|
@@ -142,6 +193,20 @@ describe "Tabletastic#table_for" do
142
193
  output_buffer.should_not have_table_with_tag("th", "Body")
143
194
  end
144
195
  end
196
+
197
+ context "with a list of attributes and options[:actions]" do
198
+ it "includes path to post for :show" do
199
+ table_for(@posts) do |t|
200
+ concat(t.data(:title, :created_at, :actions => :show))
201
+ end
202
+ output_buffer.should have_tag("th:nth-child(1)", "Title")
203
+ output_buffer.should have_tag("th:nth-child(2)", "Created at")
204
+ output_buffer.should have_tag("th:nth-child(3)", "")
205
+ output_buffer.should_not have_tag("th", "Body")
206
+
207
+ output_buffer.should have_tag("td:nth-child(3) a[@href=/posts/#{@post.id}]")
208
+ end
209
+ end
145
210
  end
146
211
 
147
212
  context "with a block" do
@@ -205,6 +270,18 @@ describe "Tabletastic#table_for" do
205
270
  end
206
271
  end
207
272
 
273
+ context "with options[:actions]" do
274
+ it "includes path to post for :show" do
275
+ table_for(@posts) do |t|
276
+ t.data(:actions => :show) do
277
+ concat(t.cell(:title))
278
+ concat(t.cell(:body))
279
+ end
280
+ end
281
+ output_buffer.should have_table_with_tag("td:nth-child(3) a[@href=/posts/#{@post.id}]")
282
+ end
283
+ end
284
+
208
285
  context "and normal/association columns" do
209
286
  before do
210
287
  ::Post.stub!(:reflect_on_all_associations).with(:belongs_to).and_return([@mock_reflection_belongs_to_author])
@@ -236,7 +313,7 @@ describe TableBuilder do
236
313
  mock_everything
237
314
  ::Post.stub!(:content_columns).and_return([mock('column', :name => 'title'), mock('column', :name => 'body'), mock('column', :name => 'created_at')])
238
315
  @posts = [@post, Post.new]
239
- @builder = TableBuilder.new(@posts, nil)
316
+ @builder = TableBuilder.new(@posts, ::Post, nil)
240
317
  end
241
318
 
242
319
  it "should detect attributes" do
data/tabletastic.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{tabletastic}
8
- s.version = "0.1.0"
8
+ s.version = "0.1.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Joshua Davey"]
12
- s.date = %q{2009-11-17}
12
+ s.date = %q{2009-11-22}
13
13
  s.description = %q{A table builder for active record collections that produces semantically rich and accessible markup}
14
14
  s.email = %q{josh@joshuadavey.com}
15
15
  s.extra_rdoc_files = [
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tabletastic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Davey
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-11-17 00:00:00 -06:00
12
+ date: 2009-11-22 00:00:00 -06:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency