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