table_cloth 0.0.1 → 0.1.0
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.
- data/Guardfile +9 -0
- data/README.md +63 -10
- data/lib/generators/table_generator.rb +7 -0
- data/lib/generators/templates/table.rb +27 -0
- data/lib/table_cloth/action.rb +9 -0
- data/lib/table_cloth/action_view_extension.rb +16 -0
- data/lib/table_cloth/base.rb +81 -0
- data/lib/table_cloth/builder.rb +29 -0
- data/lib/table_cloth/column.rb +34 -0
- data/lib/table_cloth/columns/action.rb +17 -0
- data/lib/table_cloth/configuration.rb +22 -4
- data/lib/table_cloth/presenter.rb +54 -0
- data/lib/table_cloth/presenters/default.rb +43 -0
- data/lib/table_cloth/version.rb +1 -1
- data/lib/table_cloth.rb +26 -4
- data/spec/lib/action_view_extension_spec.rb +36 -0
- data/spec/lib/base_spec.rb +104 -0
- data/spec/lib/builder_spec.rb +41 -0
- data/spec/lib/column_spec.rb +53 -0
- data/spec/lib/columns/action_spec.rb +31 -0
- data/spec/lib/configuration_spec.rb +28 -3
- data/spec/lib/presenter_spec.rb +67 -0
- data/spec/lib/presenters/default_spec.rb +117 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/support/dummy_model.rb +5 -0
- data/spec/support/dummy_table.rb +15 -0
- data/spec/support/view_mocks.rb +5 -0
- data/table_cloth.gemspec +5 -0
- metadata +97 -2
data/Guardfile
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'rspec', :version => 2 do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/table_cloth/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
7
|
+
watch('spec/spec_helper.rb') { "spec" }
|
8
|
+
end
|
9
|
+
|
data/README.md
CHANGED
@@ -1,29 +1,82 @@
|
|
1
|
-
#
|
1
|
+
# Table Cloth
|
2
2
|
|
3
|
-
|
3
|
+
Table Cloth gives you an easy to use DSL for creating and rendering tables in rails.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
7
7
|
Add this line to your application's Gemfile:
|
8
8
|
|
9
|
-
gem '
|
9
|
+
gem 'table_cloth'
|
10
10
|
|
11
11
|
And then execute:
|
12
12
|
|
13
13
|
$ bundle
|
14
14
|
|
15
|
-
|
15
|
+
## Usage
|
16
16
|
|
17
|
-
|
17
|
+
Table Cloth can use defined tables in app/tables or you can build them on the fly.
|
18
18
|
|
19
|
-
|
19
|
+
Table models can be generated using rails generators.
|
20
|
+
|
21
|
+
```
|
22
|
+
$ rails g table User
|
23
|
+
```
|
24
|
+
|
25
|
+
It will make this:
|
26
|
+
|
27
|
+
```
|
28
|
+
class UserTable < TableCloth::Base
|
29
|
+
# Define columns with the #column method
|
30
|
+
# column :name, :email
|
31
|
+
|
32
|
+
# Columns can be provided a block
|
33
|
+
#
|
34
|
+
# column :name do |object|
|
35
|
+
# object.downcase
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# Columns can also have conditionals if you want.
|
39
|
+
# The conditions are checked against the table's methods.
|
40
|
+
# As a convience, the table has a #view method which will return the current view context.
|
41
|
+
# This gives you access to current user, params, etc...
|
42
|
+
#
|
43
|
+
# column :email, if: :admin?
|
44
|
+
#
|
45
|
+
# def admin?
|
46
|
+
# view.current_user.admin?
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# Actions give you the ability to create a column for any actions you'd like to provide.
|
50
|
+
# Pass a block with an arity of 2, (object, view context).
|
51
|
+
# You can add as many actions as you want.
|
52
|
+
#
|
53
|
+
# action {|object, view| view.link_to "Edit", edit_object_path(object) }
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
Go ahead and modify it to suit your needs, pick the columns, conditions, actions, etc...
|
58
|
+
|
59
|
+
In your view, you would then use this code:
|
60
|
+
```
|
61
|
+
<%= simple_table_for @users, with: UserTable %>
|
62
|
+
|
63
|
+
```
|
64
|
+
|
65
|
+
The second approach to making tables with Table Cloth is in the view.
|
20
66
|
|
21
|
-
|
67
|
+
```
|
68
|
+
<%= simple_table_for @users do |t| %>
|
69
|
+
<% t.column :name %>
|
70
|
+
<% t.column :email %>
|
71
|
+
<% t.action {|user| link_to "View", user %>
|
72
|
+
<% end %>
|
73
|
+
```
|
22
74
|
|
23
75
|
## Contributing
|
24
76
|
|
25
77
|
1. Fork it
|
26
78
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
-
3.
|
28
|
-
4.
|
29
|
-
5.
|
79
|
+
3. CREATE A SPEC.
|
80
|
+
4. Commit your changes (`git commit -am 'Add some feature'`)
|
81
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
82
|
+
6. Create new Pull Request
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class <%= class_name %>Table < TableCloth::Base
|
2
|
+
# Define columns with the #column method
|
3
|
+
# column :name, :email
|
4
|
+
|
5
|
+
# Columns can be provided a block
|
6
|
+
#
|
7
|
+
# column :name do |object|
|
8
|
+
# object.downcase
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# Columns can also have conditionals if you want.
|
12
|
+
# The conditions are checked against the table's methods.
|
13
|
+
# As a convience, the table has a #view method which will return the current view context.
|
14
|
+
# This gives you access to current user, params, etc...
|
15
|
+
#
|
16
|
+
# column :email, if: :admin?
|
17
|
+
#
|
18
|
+
# def admin?
|
19
|
+
# view.current_user.admin?
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# Actions give you the ability to create a column for any actions you'd like to provide.
|
23
|
+
# Pass a block with an arity of 2, (object, view context).
|
24
|
+
# You can add as many actions as you want.
|
25
|
+
#
|
26
|
+
# action {|object, view| view.link_to "Edit", edit_object_path(object) }
|
27
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module TableCloth
|
2
|
+
module ActionViewExtension
|
3
|
+
def simple_table_for(objects, options={}, &block)
|
4
|
+
view_context = self
|
5
|
+
table = if block_given?
|
6
|
+
TableCloth::Builder.build(objects, view_context, options) do |table|
|
7
|
+
yield table
|
8
|
+
end
|
9
|
+
else
|
10
|
+
TableCloth::Builder.build(objects, view_context, options)
|
11
|
+
end
|
12
|
+
|
13
|
+
table.to_s
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module TableCloth
|
2
|
+
class Base
|
3
|
+
attr_reader :collection, :view
|
4
|
+
|
5
|
+
def initialize(collection, view)
|
6
|
+
@collection = collection
|
7
|
+
@view = view
|
8
|
+
end
|
9
|
+
|
10
|
+
def column_names
|
11
|
+
columns.inject([]) do |names, (column_name, column)|
|
12
|
+
names << column.human_name; names
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def columns
|
17
|
+
self.class.columns.inject({}) do |columns, (column_name, column)|
|
18
|
+
if column.available?(self)
|
19
|
+
columns[column_name] = column
|
20
|
+
end
|
21
|
+
columns
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def has_actions?
|
26
|
+
self.class.has_actions?
|
27
|
+
end
|
28
|
+
|
29
|
+
class << self
|
30
|
+
def presenter(klass=nil)
|
31
|
+
if klass
|
32
|
+
@presenter = klass
|
33
|
+
else
|
34
|
+
@presenter || (superclass.respond_to?(:presenter) ? superclass.presenter : raise("No Presenter"))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def column(*args, &block)
|
39
|
+
options = args.extract_options! || {}
|
40
|
+
options[:proc] = block if block_given?
|
41
|
+
|
42
|
+
args.each do |name|
|
43
|
+
add_column name, Column.new(name, options)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def columns
|
48
|
+
@columns ||= {}
|
49
|
+
if superclass.respond_to? :columns
|
50
|
+
@columns = superclass.columns.merge(@columns)
|
51
|
+
end
|
52
|
+
|
53
|
+
@columns
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_column(name, column)
|
57
|
+
@columns ||= {}
|
58
|
+
@columns[name] = column
|
59
|
+
end
|
60
|
+
|
61
|
+
def action(*args, &block)
|
62
|
+
options = args.extract_options! || {}
|
63
|
+
options[:proc] = block if block_given?
|
64
|
+
|
65
|
+
add_action Action.new(options)
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_action(action)
|
69
|
+
unless has_actions?
|
70
|
+
columns[:actions] = Columns::Action.new(:actions)
|
71
|
+
end
|
72
|
+
|
73
|
+
columns[:actions].actions << action
|
74
|
+
end
|
75
|
+
|
76
|
+
def has_actions?
|
77
|
+
columns[:actions].present?
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module TableCloth
|
2
|
+
class Builder
|
3
|
+
attr_accessor :table, :presenter
|
4
|
+
|
5
|
+
def self.build(objects, view, options={}, &block)
|
6
|
+
if block_given?
|
7
|
+
table = Class.new(TableCloth::Base)
|
8
|
+
block.call(table)
|
9
|
+
else
|
10
|
+
table_class = options.delete(:with)
|
11
|
+
table = table_class.kind_of?(String) ? table_class.constantize : table_class
|
12
|
+
end
|
13
|
+
|
14
|
+
presenter = options.delete(:present_with) || table.presenter
|
15
|
+
|
16
|
+
new.tap do |builder|
|
17
|
+
builder.table = table
|
18
|
+
builder.presenter = presenter.new(objects, table, view)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
presenter.render_table
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module TableCloth
|
2
|
+
class Column
|
3
|
+
attr_reader :options, :name
|
4
|
+
|
5
|
+
def initialize(name, options={})
|
6
|
+
@name = name
|
7
|
+
@options = options
|
8
|
+
end
|
9
|
+
|
10
|
+
def value(object, view)
|
11
|
+
if options[:proc] && options[:proc].respond_to?(:call)
|
12
|
+
view.capture(object, options, view, &options[:proc])
|
13
|
+
else
|
14
|
+
object.send(name)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def human_name
|
19
|
+
name.to_s.humanize
|
20
|
+
end
|
21
|
+
|
22
|
+
def available?(table)
|
23
|
+
if options[:if] && options[:if].is_a?(Symbol)
|
24
|
+
return !!table.send(options[:if])
|
25
|
+
end
|
26
|
+
|
27
|
+
if options[:unless] && options[:unless].is_a?(Symbol)
|
28
|
+
return !table.send(options[:unless])
|
29
|
+
end
|
30
|
+
|
31
|
+
true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module TableCloth
|
2
|
+
module Columns
|
3
|
+
class Action < Column
|
4
|
+
def value(object, view_context)
|
5
|
+
actions_html = actions.inject('') do |links, action|
|
6
|
+
links + "\n" + view_context.capture(object, view_context, &action.options[:proc])
|
7
|
+
end
|
8
|
+
|
9
|
+
view_context.raw(actions_html)
|
10
|
+
end
|
11
|
+
|
12
|
+
def actions
|
13
|
+
@actions ||= []
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -1,9 +1,27 @@
|
|
1
1
|
module TableCloth
|
2
2
|
class Configuration
|
3
|
-
cattr_accessor :
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
cattr_accessor :table
|
4
|
+
cattr_accessor :thead
|
5
|
+
cattr_accessor :th
|
6
|
+
cattr_accessor :tbody
|
7
|
+
cattr_accessor :tr
|
8
|
+
cattr_accessor :td
|
9
|
+
|
10
|
+
self.table = ActiveSupport::OrderedOptions.new
|
11
|
+
self.thead = ActiveSupport::OrderedOptions.new
|
12
|
+
self.th = ActiveSupport::OrderedOptions.new
|
13
|
+
self.tbody = ActiveSupport::OrderedOptions.new
|
14
|
+
self.tr = ActiveSupport::OrderedOptions.new
|
15
|
+
self.td = ActiveSupport::OrderedOptions.new
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def configure(&block)
|
19
|
+
block.arity > 0 ? block.call(self) : yield
|
20
|
+
end
|
21
|
+
|
22
|
+
def config_for(type)
|
23
|
+
self.send(type).to_hash
|
24
|
+
end
|
7
25
|
end
|
8
26
|
end
|
9
27
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module TableCloth
|
2
|
+
class Presenter
|
3
|
+
attr_reader :view_context, :table_definition, :objects,
|
4
|
+
:table
|
5
|
+
|
6
|
+
def initialize(objects, table, view)
|
7
|
+
@view_context = view
|
8
|
+
@table_definition = table
|
9
|
+
@objects = objects
|
10
|
+
@table = table_definition.new(objects, view)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Short hand so your fingers don't hurt
|
14
|
+
def v
|
15
|
+
view_context
|
16
|
+
end
|
17
|
+
|
18
|
+
def render_table
|
19
|
+
raise NoMethodError, "You must override the .render method"
|
20
|
+
end
|
21
|
+
|
22
|
+
def render_header
|
23
|
+
raise NoMethodError, "You must override the .header method"
|
24
|
+
end
|
25
|
+
|
26
|
+
def render_rows
|
27
|
+
raise NoMethodError, "You must override the .rows method"
|
28
|
+
end
|
29
|
+
|
30
|
+
def column_names
|
31
|
+
table.column_names
|
32
|
+
end
|
33
|
+
|
34
|
+
def row_values(object)
|
35
|
+
column_values = table.columns.inject([]) do |values, (key, column)|
|
36
|
+
values << column.value(object, view_context); values
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def rows
|
41
|
+
objects.inject([]) do |row, object|
|
42
|
+
row << row_values(object); row
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def wrapper_tag(type, value=nil, &block)
|
47
|
+
content = if block_given?
|
48
|
+
v.content_tag(type, TableCloth.config_for(type), &block)
|
49
|
+
else
|
50
|
+
v.content_tag(type, value, TableCloth.config_for(type))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module TableCloth
|
2
|
+
module Presenters
|
3
|
+
class Default < ::TableCloth::Presenter
|
4
|
+
def render_table
|
5
|
+
wrapper_tag :table do
|
6
|
+
render_header + render_rows
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def render_rows
|
11
|
+
wrapper_tag :tbody do
|
12
|
+
body = rows.inject('') do |r, values|
|
13
|
+
r + render_row(values)
|
14
|
+
end
|
15
|
+
|
16
|
+
v.raw(body)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def render_row(values)
|
21
|
+
wrapper_tag :tr do
|
22
|
+
row = values.inject('') do |tds, value|
|
23
|
+
tds + wrapper_tag(:td, value)
|
24
|
+
end
|
25
|
+
|
26
|
+
v.raw(row)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def render_header
|
31
|
+
wrapper_tag :thead do
|
32
|
+
wrapper_tag(:tr) do
|
33
|
+
names = column_names.inject('') do |tags, name|
|
34
|
+
tags + wrapper_tag(:th, name)
|
35
|
+
end
|
36
|
+
|
37
|
+
v.raw(names)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/table_cloth/version.rb
CHANGED
data/lib/table_cloth.rb
CHANGED
@@ -1,7 +1,29 @@
|
|
1
|
-
require 'table_cloth/version'
|
2
|
-
require 'table_cloth/configuration'
|
3
1
|
require 'action_view'
|
2
|
+
require 'table_cloth/version'
|
3
|
+
require 'table_cloth/base'
|
4
4
|
|
5
5
|
module TableCloth
|
6
|
-
|
7
|
-
|
6
|
+
autoload :Configuration, 'table_cloth/configuration'
|
7
|
+
autoload :Builder, 'table_cloth/builder'
|
8
|
+
autoload :Column, 'table_cloth/column'
|
9
|
+
autoload :Action, 'table_cloth/action'
|
10
|
+
autoload :Presenter, 'table_cloth/presenter'
|
11
|
+
autoload :ActionViewExtension, 'table_cloth/action_view_extension'
|
12
|
+
|
13
|
+
module Presenters
|
14
|
+
autoload :Default, 'table_cloth/presenters/default'
|
15
|
+
end
|
16
|
+
|
17
|
+
module Columns
|
18
|
+
autoload :Action, 'table_cloth/columns/action'
|
19
|
+
end
|
20
|
+
|
21
|
+
extend self
|
22
|
+
def self.config_for(type)
|
23
|
+
Configuration.config_for(type)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
TableCloth::Base.presenter ::TableCloth::Presenters::Default
|
28
|
+
|
29
|
+
ActionView::Base.send(:include, TableCloth::ActionViewExtension)
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Action View Extension' do
|
4
|
+
let(:action_view) { ActionView::Base.new }
|
5
|
+
let(:objects) do
|
6
|
+
3.times.map do |n|
|
7
|
+
num = n+1
|
8
|
+
DummyModel.new.tap do |d|
|
9
|
+
d.id = num # Wat
|
10
|
+
d.email = "robert#{num}@example.com"
|
11
|
+
d.name = "robert#{num}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'includes to actionview' do
|
17
|
+
action_view.should respond_to :simple_table_for
|
18
|
+
end
|
19
|
+
|
20
|
+
it '.simple_table_for renders a table' do
|
21
|
+
table = action_view.simple_table_for(objects) do |table|
|
22
|
+
table.column :name, :email
|
23
|
+
end
|
24
|
+
|
25
|
+
doc = Nokogiri::HTML(table)
|
26
|
+
doc.at_xpath('//table').should be_present
|
27
|
+
doc.at_xpath('//tr').xpath('.//th').length.should == 2
|
28
|
+
|
29
|
+
trs = doc.at_xpath('//tbody').xpath('.//tr').to_a
|
30
|
+
trs.each_with_index do |tr, index|
|
31
|
+
tds = tr.xpath('.//td')
|
32
|
+
objects[index].name.should == tds[0].text
|
33
|
+
objects[index].email.should == tds[1].text
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TableCloth::Base do
|
4
|
+
subject { Class.new(TableCloth::Base) }
|
5
|
+
let(:view_context) { ActionView::Base.new }
|
6
|
+
|
7
|
+
context 'columns' do
|
8
|
+
it 'has a column method' do
|
9
|
+
subject.should respond_to :column
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'column accepts a name' do
|
13
|
+
expect { subject.column :column_name }.not_to raise_error
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'column accepts options' do
|
17
|
+
expect { subject.column :n, {option: 'value'} }.not_to raise_error
|
18
|
+
end
|
19
|
+
|
20
|
+
it '.columns returns all columns' do
|
21
|
+
subject.column :name
|
22
|
+
subject.columns.size.should == 1
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'excepts multiple column names' do
|
26
|
+
subject.column :name, :email
|
27
|
+
subject.columns.size.should == 2
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'stores a proc if given in options' do
|
31
|
+
subject.column(:name) { 'Wee' }
|
32
|
+
|
33
|
+
column = subject.columns[:name]
|
34
|
+
column.options[:proc].should be_present
|
35
|
+
column.options[:proc].should be_kind_of(Proc)
|
36
|
+
end
|
37
|
+
|
38
|
+
it '.column_names returns all names' do
|
39
|
+
subject.column :name, :email
|
40
|
+
subject.new([], view_context).column_names.should == ['Name', 'Email']
|
41
|
+
end
|
42
|
+
|
43
|
+
it '.column_names includes actions when given' do
|
44
|
+
subject.action { '/' }
|
45
|
+
subject.new([], view_context).column_names.should include 'Actions'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'conditions' do
|
50
|
+
let(:dummy_model) do
|
51
|
+
DummyModel.new.tap do |d|
|
52
|
+
d.id = 1
|
53
|
+
d.email = 'robert@example.com'
|
54
|
+
d.name = 'robert'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'if' do
|
59
|
+
subject { DummyTable.new([dummy_model], view_context) }
|
60
|
+
|
61
|
+
it 'includes the id column when admin' do
|
62
|
+
subject.column_names.should include 'Id'
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'exclused the id column when an admin' do
|
66
|
+
def subject.admin?
|
67
|
+
false
|
68
|
+
end
|
69
|
+
|
70
|
+
subject.column_names.should_not include 'Id'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'unless' do
|
75
|
+
subject { DummyTableUnlessAdmin.new([dummy_model], view_context) }
|
76
|
+
before(:each) do
|
77
|
+
def subject.admin?
|
78
|
+
false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'includes the id when not an admin' do
|
83
|
+
subject.column_names.should include 'Id'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'presenters' do
|
89
|
+
it 'has a presenter method' do
|
90
|
+
subject.should respond_to :presenter
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'actions' do
|
95
|
+
it 'has an action method' do
|
96
|
+
subject.should respond_to :action
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'it adds an acion' do
|
100
|
+
subject.action { '/' }
|
101
|
+
subject.columns[:actions].actions.size.should == 1
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TableCloth::Builder do
|
4
|
+
subject { Class.new(TableCloth::Builder) }
|
5
|
+
let(:view_context) { ActionView::Base.new }
|
6
|
+
|
7
|
+
context '.build' do
|
8
|
+
it 'can build a table on the fly with a block' do
|
9
|
+
new_table = subject.build([], view_context) do |table|
|
10
|
+
table.column :name
|
11
|
+
table.action(:edit) { '/model/1/edit' }
|
12
|
+
end
|
13
|
+
|
14
|
+
new_table.table.columns.length.should == 2
|
15
|
+
new_table.table.columns[:actions].actions.length.should == 1
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'can build a table from a class name' do
|
19
|
+
new_table = subject.build([], view_context, with: DummyTable)
|
20
|
+
new_table.table.should == DummyTable
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'defaults the presenter' do
|
24
|
+
new_table = subject.build([], view_context, with: DummyTable)
|
25
|
+
new_table.presenter.should be_kind_of TableCloth::Presenters::Default
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'can provide a presenter' do
|
29
|
+
random_presenter = Class.new(TableCloth::Presenters::Default)
|
30
|
+
new_table = subject.build([], view_context, with: DummyTable, present_with: random_presenter)
|
31
|
+
new_table.presenter.should be_kind_of random_presenter
|
32
|
+
end
|
33
|
+
|
34
|
+
it '.to_s renders a table' do
|
35
|
+
new_table = subject.build([], view_context, with: DummyTable)
|
36
|
+
body = new_table.to_s
|
37
|
+
doc = Nokogiri::HTML(body)
|
38
|
+
doc.at_xpath('//table').should be_present
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TableCloth::Column do
|
4
|
+
subject { Class.new(TableCloth::Column) }
|
5
|
+
let(:view_context) { ActionView::Base.new }
|
6
|
+
let(:dummy_model) do
|
7
|
+
DummyModel.new.tap do |d|
|
8
|
+
d.id = 1
|
9
|
+
d.email = 'robert@example.com'
|
10
|
+
d.name = 'robert'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'values' do
|
15
|
+
let(:name_column) do
|
16
|
+
TableCloth::Column.new(:name)
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:email_column) do
|
20
|
+
proc = lambda {|object, options, view|
|
21
|
+
object.email
|
22
|
+
}
|
23
|
+
|
24
|
+
TableCloth::Column.new(:my_email, proc: proc)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'returns the name correctly' do
|
28
|
+
name_column.value(dummy_model, view_context).should == 'robert'
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'returns the email from a proc correctly' do
|
32
|
+
email_column.value(dummy_model, view_context).should == 'robert@example.com'
|
33
|
+
end
|
34
|
+
|
35
|
+
context '.available?' do
|
36
|
+
let(:dummy_table) do
|
37
|
+
Class.new(TableCloth::Table) do
|
38
|
+
column :name, if: :admin?
|
39
|
+
|
40
|
+
def admin?
|
41
|
+
view.admin?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'returns true on successful constraint' do
|
47
|
+
table = Class.new(DummyTable).new([dummy_model], view_context)
|
48
|
+
column = TableCloth::Column.new(:name, if: :admin?)
|
49
|
+
column.available?(table).should be_true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TableCloth::Columns::Action do
|
4
|
+
let(:view_context) { ActionView::Base.new }
|
5
|
+
let(:dummy_table) { Class.new(DummyTable) }
|
6
|
+
let(:dummy_model) do
|
7
|
+
DummyModel.new.tap do |d|
|
8
|
+
d.id = 1
|
9
|
+
d.email = 'robert@example.com'
|
10
|
+
d.name = 'robert'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
subject { TableCloth::Columns::Action.new(object, view_context) }
|
15
|
+
|
16
|
+
it '.value returns all actions in HTML' do
|
17
|
+
dummy_table.action {|object, view| view.link_to "Edit", "#{object.id}"}
|
18
|
+
presenter = TableCloth::Presenters::Default.new([dummy_model], dummy_table, view_context)
|
19
|
+
|
20
|
+
doc = Nokogiri::HTML(presenter.render_table)
|
21
|
+
|
22
|
+
actions_column = doc.at_xpath('//tbody')
|
23
|
+
.at_xpath('.//tr')
|
24
|
+
.xpath('.//td')
|
25
|
+
.last
|
26
|
+
|
27
|
+
link = actions_column.at_xpath('.//a')
|
28
|
+
link.should be_present
|
29
|
+
link[:href].should == '1'
|
30
|
+
end
|
31
|
+
end
|
@@ -3,8 +3,33 @@ require 'spec_helper'
|
|
3
3
|
describe TableCloth::Configuration do
|
4
4
|
subject { Class.new(TableCloth::Configuration) }
|
5
5
|
|
6
|
-
it 'configures
|
7
|
-
subject.
|
8
|
-
subject.
|
6
|
+
it 'configures table options' do
|
7
|
+
subject.table.classes = 'table'
|
8
|
+
subject.table.classes.should == 'table'
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'configures thead options' do
|
12
|
+
subject.thead.classes = 'thead'
|
13
|
+
subject.thead.classes.should == 'thead'
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'configures th options' do
|
17
|
+
subject.th.classes = 'th'
|
18
|
+
subject.th.classes.should == 'th'
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'configures tbody options' do
|
22
|
+
subject.tbody.classes = 'tbody'
|
23
|
+
subject.tbody.classes.should == 'tbody'
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'configures tr options' do
|
27
|
+
subject.tr.classes = 'tr'
|
28
|
+
subject.tr.classes.should == 'tr'
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'configures td options' do
|
32
|
+
subject.td.classes = 'td'
|
33
|
+
subject.td.classes.should == 'td'
|
9
34
|
end
|
10
35
|
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TableCloth::Presenter do
|
4
|
+
let(:dummy_table) { Class.new(DummyTable) }
|
5
|
+
let(:dummy_model) do
|
6
|
+
DummyModel.new.tap do |d|
|
7
|
+
d.id = 1
|
8
|
+
d.email = 'robert@example.com'
|
9
|
+
d.name = 'robert'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:objects) do
|
14
|
+
3.times.map do |n|
|
15
|
+
num = n+1
|
16
|
+
DummyModel.new.tap do |d|
|
17
|
+
d.id = num # Wat
|
18
|
+
d.email = "robert#{num}@example.com"
|
19
|
+
d.name = "robert#{num}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:view_context) { ActionView::Base.new }
|
25
|
+
subject { TableCloth::Presenter.new(objects, dummy_table, view_context) }
|
26
|
+
|
27
|
+
it 'returns all values for a row' do
|
28
|
+
subject.row_values(dummy_model).should == [1, 'robert', 'robert@example.com']
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'returns an edit link in the actions column' do
|
32
|
+
dummy_table.action {|object, view| view.link_to 'Edit', '/model/1/edit' }
|
33
|
+
presenter = TableCloth::Presenter.new(objects, dummy_table, view_context)
|
34
|
+
|
35
|
+
column = Nokogiri::HTML(presenter.row_values(dummy_model).last)
|
36
|
+
column.at_xpath('//a')[:href].should == '/model/1/edit'
|
37
|
+
column.at_xpath('//a').text.should == 'Edit'
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'generates the values for all of the rows' do
|
41
|
+
subject.rows.should == [
|
42
|
+
[1, 'robert1', 'robert1@example.com'],
|
43
|
+
[2, 'robert2', 'robert2@example.com'],
|
44
|
+
[3, 'robert3', 'robert3@example.com']
|
45
|
+
]
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'tags' do
|
49
|
+
before(:all) { TableCloth::Configuration.table.class = 'stuff' }
|
50
|
+
it '.wrapper_tag includes config for a tag in block form' do
|
51
|
+
table = subject.wrapper_tag(:table) do
|
52
|
+
'Hello'
|
53
|
+
end
|
54
|
+
table = Nokogiri::HTML(table)
|
55
|
+
|
56
|
+
table.at_xpath('//table')[:class].should == 'stuff'
|
57
|
+
table.at_xpath('//table').text.should include 'Hello'
|
58
|
+
end
|
59
|
+
|
60
|
+
it '.wrapper_tag includes config for a tag without a block' do
|
61
|
+
table = subject.wrapper_tag(:table, 'Hello')
|
62
|
+
table = Nokogiri::HTML(table)
|
63
|
+
table.at_xpath('//table')[:class].should == 'stuff'
|
64
|
+
table.at_xpath('//table').text.should include 'Hello'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TableCloth::Presenters::Default do
|
4
|
+
let(:dummy_table) { DummyTable }
|
5
|
+
let(:dummy_model) do
|
6
|
+
DummyModel.new.tap do |d|
|
7
|
+
d.id = 1
|
8
|
+
d.email = 'robert@example.com'
|
9
|
+
d.name = 'robert'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:objects) do
|
14
|
+
3.times.map do |n|
|
15
|
+
num = n+1
|
16
|
+
DummyModel.new.tap do |d|
|
17
|
+
d.id = num # Wat
|
18
|
+
d.email = "robert#{num}@example.com"
|
19
|
+
d.name = "robert#{num}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:view_context) { ActionView::Base.new }
|
25
|
+
subject { TableCloth::Presenters::Default.new(objects, dummy_table, view_context) }
|
26
|
+
|
27
|
+
it 'creates a table head' do
|
28
|
+
header = subject.render_header
|
29
|
+
doc = Nokogiri::HTML(header)
|
30
|
+
|
31
|
+
thead = doc.xpath('.//thead')
|
32
|
+
thead.should be_present
|
33
|
+
|
34
|
+
tr = thead.xpath('.//tr')
|
35
|
+
tr.should be_present
|
36
|
+
|
37
|
+
th = tr.xpath('.//th')
|
38
|
+
th.should be_present
|
39
|
+
|
40
|
+
th.length.should == 3
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'creates rows' do
|
44
|
+
rows = subject.render_rows
|
45
|
+
doc = Nokogiri::HTML(rows)
|
46
|
+
|
47
|
+
tbody = doc.xpath('//tbody')
|
48
|
+
tbody.should be_present
|
49
|
+
|
50
|
+
tbody.xpath('.//tr').each_with_index do |row, row_index|
|
51
|
+
row.xpath('.//td').each_with_index do |td, td_index|
|
52
|
+
object = objects[row_index]
|
53
|
+
case td_index
|
54
|
+
when 0
|
55
|
+
object.id.to_s == td.text
|
56
|
+
when 1
|
57
|
+
object.name.should == td.text
|
58
|
+
when 2
|
59
|
+
object.email.should == td.text
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'creates an entire table' do
|
66
|
+
doc = Nokogiri::HTML(subject.render_table)
|
67
|
+
table = doc.xpath('//table')
|
68
|
+
table.should be_present
|
69
|
+
|
70
|
+
thead = table.xpath('.//thead')
|
71
|
+
thead.should be_present
|
72
|
+
|
73
|
+
tbody = table.at_xpath('.//tbody')
|
74
|
+
tbody.should be_present
|
75
|
+
|
76
|
+
tbody.xpath('.//tr').length.should == 3
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'configuration' do
|
80
|
+
before(:all) do
|
81
|
+
TableCloth::Configuration.configure do |config|
|
82
|
+
config.table.class = 'table'
|
83
|
+
config.thead.class = 'thead'
|
84
|
+
config.th.class = 'th'
|
85
|
+
config.tbody.class = 'tbody'
|
86
|
+
config.tr.class = 'tr'
|
87
|
+
config.td.class = 'td'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
let(:doc) { Nokogiri::HTML(subject.render_table) }
|
92
|
+
|
93
|
+
it 'tables have a class attached' do
|
94
|
+
doc.at_xpath('//table')[:class].should include 'table'
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'thead has a class attached' do
|
98
|
+
doc.at_xpath('//thead')[:class].should include 'thead'
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'th has a class attached' do
|
102
|
+
doc.at_xpath('//th')[:class].should include 'th'
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'tbody has a class attached' do
|
106
|
+
doc.at_xpath('//tbody')[:class].should include 'tbody'
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'tr has a class attached' do
|
110
|
+
doc.at_xpath('//tr')[:class].should include 'tr'
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'td has a class attached' do
|
114
|
+
doc.at_xpath('//td')[:class].should include 'td'
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
require 'table_cloth'
|
2
|
+
require 'awesome_print'
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'pry'
|
5
|
+
|
6
|
+
Dir['./spec/support/**/*.rb'].each {|f| require f }
|
7
|
+
|
8
|
+
ActionView::Base.send :include, TableClothViewMocks
|
9
|
+
|
1
10
|
RSpec.configure do |config|
|
2
11
|
config.treat_symbols_as_metadata_keys_with_true_values = true
|
3
12
|
config.run_all_when_everything_filtered = true
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class DummyTable < TableCloth::Base
|
2
|
+
column :id, if: :admin?
|
3
|
+
column :name
|
4
|
+
column :email
|
5
|
+
|
6
|
+
def admin?
|
7
|
+
view.admin?
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class DummyTableUnlessAdmin < TableCloth::Base
|
12
|
+
column :id, unless: :admin?
|
13
|
+
column :name
|
14
|
+
column :email
|
15
|
+
end
|
data/table_cloth.gemspec
CHANGED
@@ -18,5 +18,10 @@ Gem::Specification.new do |gem|
|
|
18
18
|
gem.require_paths = ["lib"]
|
19
19
|
|
20
20
|
gem.add_development_dependency('rspec', '~> 2.11')
|
21
|
+
gem.add_development_dependency('awesome_print')
|
22
|
+
gem.add_development_dependency('nokogiri')
|
23
|
+
gem.add_development_dependency('pry')
|
24
|
+
gem.add_development_dependency('guard-rspec')
|
25
|
+
|
21
26
|
gem.add_dependency('actionpack', '~> 3.2')
|
22
27
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: table_cloth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-10-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -27,6 +27,70 @@ dependencies:
|
|
27
27
|
- - ~>
|
28
28
|
- !ruby/object:Gem::Version
|
29
29
|
version: '2.11'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: awesome_print
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: nokogiri
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: pry
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: guard-rspec
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
30
94
|
- !ruby/object:Gem::Dependency
|
31
95
|
name: actionpack
|
32
96
|
requirement: !ruby/object:Gem::Requirement
|
@@ -53,14 +117,35 @@ files:
|
|
53
117
|
- .gitignore
|
54
118
|
- .rspec
|
55
119
|
- Gemfile
|
120
|
+
- Guardfile
|
56
121
|
- LICENSE.txt
|
57
122
|
- README.md
|
58
123
|
- Rakefile
|
124
|
+
- lib/generators/table_generator.rb
|
125
|
+
- lib/generators/templates/table.rb
|
59
126
|
- lib/table_cloth.rb
|
127
|
+
- lib/table_cloth/action.rb
|
128
|
+
- lib/table_cloth/action_view_extension.rb
|
129
|
+
- lib/table_cloth/base.rb
|
130
|
+
- lib/table_cloth/builder.rb
|
131
|
+
- lib/table_cloth/column.rb
|
132
|
+
- lib/table_cloth/columns/action.rb
|
60
133
|
- lib/table_cloth/configuration.rb
|
134
|
+
- lib/table_cloth/presenter.rb
|
135
|
+
- lib/table_cloth/presenters/default.rb
|
61
136
|
- lib/table_cloth/version.rb
|
137
|
+
- spec/lib/action_view_extension_spec.rb
|
138
|
+
- spec/lib/base_spec.rb
|
139
|
+
- spec/lib/builder_spec.rb
|
140
|
+
- spec/lib/column_spec.rb
|
141
|
+
- spec/lib/columns/action_spec.rb
|
62
142
|
- spec/lib/configuration_spec.rb
|
143
|
+
- spec/lib/presenter_spec.rb
|
144
|
+
- spec/lib/presenters/default_spec.rb
|
63
145
|
- spec/spec_helper.rb
|
146
|
+
- spec/support/dummy_model.rb
|
147
|
+
- spec/support/dummy_table.rb
|
148
|
+
- spec/support/view_mocks.rb
|
64
149
|
- table_cloth.gemspec
|
65
150
|
homepage: http://www.github.com/bobbytables/table_cloth
|
66
151
|
licenses: []
|
@@ -88,5 +173,15 @@ specification_version: 3
|
|
88
173
|
summary: Table Cloth provides an easy and intuitive DSL for creating tables in rails
|
89
174
|
views.
|
90
175
|
test_files:
|
176
|
+
- spec/lib/action_view_extension_spec.rb
|
177
|
+
- spec/lib/base_spec.rb
|
178
|
+
- spec/lib/builder_spec.rb
|
179
|
+
- spec/lib/column_spec.rb
|
180
|
+
- spec/lib/columns/action_spec.rb
|
91
181
|
- spec/lib/configuration_spec.rb
|
182
|
+
- spec/lib/presenter_spec.rb
|
183
|
+
- spec/lib/presenters/default_spec.rb
|
92
184
|
- spec/spec_helper.rb
|
185
|
+
- spec/support/dummy_model.rb
|
186
|
+
- spec/support/dummy_table.rb
|
187
|
+
- spec/support/view_mocks.rb
|