sortable_table 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/README +7 -0
- data/Rakefile +38 -0
- data/TODO +0 -0
- data/VERSION +1 -0
- data/example/controllers/cablecar/users_controller.rb +49 -0
- data/example/models/cablecar/contact_info.rb +9 -0
- data/example/models/cablecar/user.rb +7 -0
- data/example/views/cablecar/users/index.html.erb +1 -0
- data/images/arrow_down.gif +0 -0
- data/images/arrow_up.gif +0 -0
- data/images/logo_opaque.png +0 -0
- data/init.rb +33 -0
- data/install.rb +1 -0
- data/lib/sortable.rb +299 -0
- data/lib/sortable_helper.rb +81 -0
- data/sortable_table.gemspec +63 -0
- data/test/example_test_routing.rb +16 -0
- data/test/test_helper.rb +15 -0
- data/test/users_controller_test.rb +346 -0
- data/views/sortable/_table.html.erb +74 -0
- data/views/sortable/index.erb +1 -0
- metadata +78 -0
data/README
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Sortable
|
|
2
|
+
========
|
|
3
|
+
|
|
4
|
+
See my blog posting on sortable here: http://javathehutt.blogspot.com/2009/06/mo-simple-sortable-tables-in-rails.html
|
|
5
|
+
|
|
6
|
+
For more usage information see sortable.rb and sortable_helper.rb. Also see the example app in the example dir as
|
|
7
|
+
well as the tests that go along with it.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require 'rake'
|
|
2
|
+
require 'rake/testtask'
|
|
3
|
+
require 'rake/rdoctask'
|
|
4
|
+
|
|
5
|
+
desc 'Default: run unit tests.'
|
|
6
|
+
task :default => :test
|
|
7
|
+
|
|
8
|
+
desc 'Test the sortable plugin.'
|
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
|
10
|
+
t.libs << 'lib'
|
|
11
|
+
t.pattern = 'test/**/*_test.rb'
|
|
12
|
+
t.verbose = true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc 'Generate documentation for the sortable plugin.'
|
|
16
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
|
17
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
18
|
+
rdoc.title = 'Sortable'
|
|
19
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
|
20
|
+
rdoc.rdoc_files.include('README')
|
|
21
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
require 'jeweler'
|
|
26
|
+
Jeweler::Tasks.new do |gemspec|
|
|
27
|
+
gemspec.name = "sortable_table"
|
|
28
|
+
gemspec.summary = "Rails plugin to produce a sortable, paginated, searchable table for any model"
|
|
29
|
+
gemspec.description = "Rails plugin to produce a sortable, paginated, searchable table for any model"
|
|
30
|
+
gemspec.email = ""
|
|
31
|
+
gemspec.homepage = "http://github.com/kovacs/sortable"
|
|
32
|
+
gemspec.authors = ["Michael Kovacs"]
|
|
33
|
+
end
|
|
34
|
+
Jeweler::GemcutterTasks.new
|
|
35
|
+
rescue LoadError
|
|
36
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
|
37
|
+
end
|
|
38
|
+
|
data/TODO
ADDED
|
File without changes
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Cablecar
|
|
2
|
+
class UsersController < ActionController::Base
|
|
3
|
+
|
|
4
|
+
# By default this will create an index action that uses the default sortable table partial to render the list of objects
|
|
5
|
+
sortable_table Cablecar::User
|
|
6
|
+
|
|
7
|
+
# The following index action demonstrates various ways that you can fetch and display your objects in a paginated, sortable,
|
|
8
|
+
# searchable table. Each clause in the elsif chain below could be its own index action on its own but all are included here
|
|
9
|
+
# for demo and testing purposes. Each example is triggered by passing the appropriate param value to trigger the desired
|
|
10
|
+
# clause to demo.
|
|
11
|
+
def index
|
|
12
|
+
if params[:filter_example]
|
|
13
|
+
# This demonstrates the ability to add additional conditions to fetching the list of objects. This will filter
|
|
14
|
+
# the results to the list of active users
|
|
15
|
+
if params[:active]
|
|
16
|
+
conditions = 'cablecar_users.status = "active"'
|
|
17
|
+
end
|
|
18
|
+
get_sorted_objects(params, :conditions => conditions)
|
|
19
|
+
elsif params[:complex_example]
|
|
20
|
+
# This example demonstrates a more complicated example that displays the flexibility of creating the sortable table.
|
|
21
|
+
# Here we are showing an attribute on a related object as a sortable column in the table, namely the contact_info name
|
|
22
|
+
# column. We're also restricting the visible columns to 3 columns as well as defining the sort order for each column
|
|
23
|
+
# as well as the default sort column. We're also specifying what columns should be used when a search is performed.
|
|
24
|
+
# Finally we're also setting the number of results to show per page.
|
|
25
|
+
get_sorted_objects(params, :include_relations => [:contact_info],
|
|
26
|
+
:table_headings => [['Username', 'username'], ['Status', 'status'], ['Name', 'name']],
|
|
27
|
+
:sort_map => {:username => [['cablecar_users.username', 'DESC'],
|
|
28
|
+
['cablecar_users.status', 'DESC']],
|
|
29
|
+
:status => ['cablecar_users.status', 'ASC'],
|
|
30
|
+
:name => ['cablecar_contact_infos.name', 'DESC']},
|
|
31
|
+
:default_sort => ['name', 'ASC'],
|
|
32
|
+
:search_array => ['cablecar_users.username', 'cablecar_contact_infos.name'],
|
|
33
|
+
:per_page => 15)
|
|
34
|
+
elsif params[:no_pagination]
|
|
35
|
+
@hide_pagination = true
|
|
36
|
+
get_sorted_objects(params)
|
|
37
|
+
else
|
|
38
|
+
if params[:use_default]
|
|
39
|
+
super
|
|
40
|
+
else
|
|
41
|
+
# If you wish to override how the objects are fetched this is the simplest way to do so
|
|
42
|
+
# The key is that you simply need to call get_sorted_objects with your params and any optional overrides for
|
|
43
|
+
# sortable defaults. See sortable.rb for more details on overrides.
|
|
44
|
+
get_sorted_objects(params)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class Cablecar::ContactInfo < ActiveRecord::Base
|
|
2
|
+
set_table_name 'cablecar_contact_infos'
|
|
3
|
+
has_one :user, :class_name => Cablecar::User.to_s
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
# to solve circular dependency
|
|
7
|
+
Cablecar::User.class_eval do
|
|
8
|
+
belongs_to :contact_info, :class_name => Cablecar::ContactInfo.to_s
|
|
9
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= sortable_table :paginate => !@hide_pagination%>
|
|
Binary file
|
data/images/arrow_up.gif
ADDED
|
Binary file
|
|
Binary file
|
data/init.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require 'sortable'
|
|
2
|
+
ActionController::Base.send(:include, Sortable)
|
|
3
|
+
|
|
4
|
+
VIEW_PATH = File.join(RAILS_ROOT, 'vendor', 'plugins', 'sortable', 'views')
|
|
5
|
+
ActionController::Base.append_view_path(VIEW_PATH) unless ActionController::Base.view_paths.include?(VIEW_PATH)
|
|
6
|
+
|
|
7
|
+
require 'sortable_helper'
|
|
8
|
+
ActionView::Base.send(:include, SortableHelper)
|
|
9
|
+
|
|
10
|
+
if RAILS_ENV == 'test'
|
|
11
|
+
# only load the example files/routes in development mode
|
|
12
|
+
# TODO easy way to load all controller/model paths instead? Don't know off the top of my head but am sure it's easy
|
|
13
|
+
require File.join(File.dirname(__FILE__), 'example', 'controllers', 'cablecar', 'users_controller')
|
|
14
|
+
require File.join(File.dirname(__FILE__), 'example', 'models', 'cablecar', 'user')
|
|
15
|
+
require File.join(File.dirname(__FILE__), 'example', 'models', 'cablecar', 'contact_info')
|
|
16
|
+
|
|
17
|
+
# install the routes
|
|
18
|
+
require File.join(File.dirname(__FILE__), 'test', 'example_test_routing')
|
|
19
|
+
|
|
20
|
+
# install the example app view path
|
|
21
|
+
VIEW_PATH = File.join(RAILS_ROOT, 'vendor', 'plugins', 'sortable', 'example', 'views')
|
|
22
|
+
ActionController::Base.append_view_path(VIEW_PATH) unless ActionController::Base.view_paths.include?(VIEW_PATH)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
COPY_IMAGES = true
|
|
26
|
+
|
|
27
|
+
if COPY_IMAGES
|
|
28
|
+
require "ftools"
|
|
29
|
+
|
|
30
|
+
File.copy(File.join(File.dirname(__FILE__), 'images', 'logo_opaque.png'), File.join(RAILS_ROOT, 'public', 'images'))
|
|
31
|
+
File.copy(File.join(File.dirname(__FILE__), 'images', 'arrow_up.gif'), File.join(RAILS_ROOT, 'public', 'images'))
|
|
32
|
+
File.copy(File.join(File.dirname(__FILE__), 'images', 'arrow_down.gif'), File.join(RAILS_ROOT, 'public', 'images'))
|
|
33
|
+
end
|
data/install.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Install hook code here
|
data/lib/sortable.rb
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
require 'generator'
|
|
2
|
+
|
|
3
|
+
module Sortable
|
|
4
|
+
def self.included(base)
|
|
5
|
+
base.extend(ClassMethods)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
module ClassMethods
|
|
9
|
+
# Class method to setup a controller to create a sortable table.
|
|
10
|
+
#
|
|
11
|
+
# usage: sortable_table class_to_tabularize, optional_params
|
|
12
|
+
#
|
|
13
|
+
# simplest example possible:
|
|
14
|
+
#
|
|
15
|
+
# In your controller...
|
|
16
|
+
#
|
|
17
|
+
# sortable_table User
|
|
18
|
+
#
|
|
19
|
+
# This will create a sortable table over all your objects and display all columns.
|
|
20
|
+
# The simplest way to trim down the columns to display is to pass in a parameter specifying which columns
|
|
21
|
+
# on your object you'd like to display:
|
|
22
|
+
#
|
|
23
|
+
# sortable_table User, :display_columns => ['id', 'email', 'name', 'state', 'created_at']
|
|
24
|
+
#
|
|
25
|
+
# This will show the same sortable, searchable, paginated table with only these columns
|
|
26
|
+
#
|
|
27
|
+
# If you need a bit more control over how the objects are fetchd and displayed this is
|
|
28
|
+
# the next simplest example:
|
|
29
|
+
#
|
|
30
|
+
# Override the index action in your controller:
|
|
31
|
+
#
|
|
32
|
+
# def index
|
|
33
|
+
# get_sorted_objects(params)
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# In your index action template (within the same controller) put in a helper call to show a sortable table. You can
|
|
37
|
+
# create your own table partial to be used to display the objects. See the sortable/views/sortable/_table.html.erb
|
|
38
|
+
# (which is the default template used by the plugin) for an example.
|
|
39
|
+
#
|
|
40
|
+
# <%= sortable_table %>
|
|
41
|
+
#
|
|
42
|
+
# The view method will automatically generate a paginated, sortable table for the class type declared in the controller
|
|
43
|
+
#
|
|
44
|
+
# optional_params:
|
|
45
|
+
#
|
|
46
|
+
# :per_page - Number of records to show per page. Default is 10
|
|
47
|
+
#
|
|
48
|
+
# The next section deals with how to change what's displayed in the table. The first and simplest option is :display_columns.
|
|
49
|
+
# The more flexible option is to use :table_headings and :sort_map. First we'll go over the simpler option.
|
|
50
|
+
#
|
|
51
|
+
# :display_columns - Specifies which columns that you'd like to display in the table.
|
|
52
|
+
#
|
|
53
|
+
# For more flexibility you can use :table_headings and :sort_map. You would most likely use this when you want to display
|
|
54
|
+
# attributes from more than one object in the same table or if you need more flexibility with regards to sort rules.
|
|
55
|
+
# :table_headings - The table heading label and sort key. Default is all the column names for the given class
|
|
56
|
+
# :sort_map - The mapping between the sort key and the table.column to sort on. Default is all the columns for the given class
|
|
57
|
+
# :include_relations - Relations to include if you choose to display a column in a related object's table
|
|
58
|
+
#
|
|
59
|
+
# :default_sort - The default sorting column and direction when displaying the table without any sort params. Default is 'id DESC'
|
|
60
|
+
#
|
|
61
|
+
# Note: You *must* override both :table_headings and :sort_map if you do choose to override so that
|
|
62
|
+
# the contents of the column headings match up with the contents of the sort_map they associate with.
|
|
63
|
+
# Also if you override :default_sort you'll need to change the :table_headings and :sort_map if the new :default_sort
|
|
64
|
+
# column doesn't currently reside within the :table_heading and :sort_map collections
|
|
65
|
+
#
|
|
66
|
+
# Example of modifying :table_headings or :sort_map :
|
|
67
|
+
# :table_headings => [['Name', 'name'], ['Status', 'status']]
|
|
68
|
+
# :sort_map => {'name' => ['users.name'],
|
|
69
|
+
# 'status' => ['users.status']}
|
|
70
|
+
#
|
|
71
|
+
# Note that both 'name' and 'status' are sort keys that map to both the table heading label and the
|
|
72
|
+
# database table.column combination that the heading refers to.
|
|
73
|
+
#
|
|
74
|
+
# Also note that :default_sort now needs to change as well since the table no longer contains the :default_sort
|
|
75
|
+
# column of 'id':
|
|
76
|
+
#
|
|
77
|
+
# :default_sort => ['name', 'DESC']
|
|
78
|
+
#
|
|
79
|
+
# Example of modifying :table_headings to include a column from a related object:
|
|
80
|
+
# :table_headings => [['Name', 'name'], ['Status', 'status'], ['Role', 'role']]
|
|
81
|
+
# :sort_map => {'name' => ['users.name'],
|
|
82
|
+
# 'status' => ['users.status'],
|
|
83
|
+
# 'role' => ['roles.role']}
|
|
84
|
+
#
|
|
85
|
+
# Note that we've now added 'roles.role' to the list of columns to display and sort on. In order to
|
|
86
|
+
# make the find work properly we also need to include the related object, so we pass in the following param:
|
|
87
|
+
# :include_relations => [:role]
|
|
88
|
+
#
|
|
89
|
+
# Perhaps we want to sort by role ascending by default as well. We'd pass the param value:
|
|
90
|
+
# :default_sort => ['role', 'ASC']
|
|
91
|
+
#
|
|
92
|
+
# and the table is now sortable by a related object's column and is the default sort value for the table.
|
|
93
|
+
#
|
|
94
|
+
# Search. You can specify what columns are searchable on your objects as follows:
|
|
95
|
+
# :search_array => ['cablecar_users.username', 'cablecar_users.name']
|
|
96
|
+
#
|
|
97
|
+
# Now search queries will only search username and name for users. By default search is enabled for all columns
|
|
98
|
+
# that are being displayed in the table. This allows you to expand or constrain those values.
|
|
99
|
+
#
|
|
100
|
+
def sortable_table(klass, options={})
|
|
101
|
+
@@sortable_table_options ||={}
|
|
102
|
+
|
|
103
|
+
sort_map = HashWithIndifferentAccess.new
|
|
104
|
+
|
|
105
|
+
if options[:table_headings] &&
|
|
106
|
+
options[:sort_map]
|
|
107
|
+
table_headings = options[:table_headings]
|
|
108
|
+
sort_map.merge!(options[:sort_map])
|
|
109
|
+
else
|
|
110
|
+
display_columns = options[:display_columns].blank? ? klass.column_names : options[:display_columns]
|
|
111
|
+
table_headings = []
|
|
112
|
+
klass.column_names.each do |att|
|
|
113
|
+
if display_columns.include?(att)
|
|
114
|
+
table_headings << [att.humanize, att]
|
|
115
|
+
sort_map[att] = ["#{klass.table_name}.#{att}", 'DESC']
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
default_sort = options[:default_sort].nil? ? ['id', 'DESC'] : options[:default_sort]
|
|
121
|
+
per_page = options[:per_page].nil? ? 10 : options[:per_page]
|
|
122
|
+
include_relations = options[:include_relations].nil? ? [] : options[:include_relations]
|
|
123
|
+
|
|
124
|
+
search_array = options[:search_array].nil? ? sort_map.values.collect {|v| v[0]} : options[:search_array]
|
|
125
|
+
|
|
126
|
+
@@sortable_table_options[controller_name] = {:class => klass,
|
|
127
|
+
:table_headings => table_headings,
|
|
128
|
+
:default_sort => default_sort,
|
|
129
|
+
:sort_map => sort_map,
|
|
130
|
+
:search_array => search_array,
|
|
131
|
+
:per_page => per_page,
|
|
132
|
+
:include_relations => include_relations}
|
|
133
|
+
|
|
134
|
+
module_eval do
|
|
135
|
+
include Sortable::InstanceMethods
|
|
136
|
+
def sortable_class
|
|
137
|
+
@@sortable_table_options[controller_name][:class]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def sortable_table_headings
|
|
141
|
+
@@sortable_table_options[controller_name][:table_headings]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def sortable_default_sort
|
|
145
|
+
@@sortable_table_options[controller_name][:default_sort]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def sortable_sort_map
|
|
149
|
+
@@sortable_table_options[controller_name][:sort_map]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def sortable_search_array
|
|
153
|
+
@@sortable_table_options[controller_name][:search_array]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def sortable_per_page
|
|
157
|
+
@@sortable_table_options[controller_name][:per_page]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def sortable_include_relations
|
|
161
|
+
@@sortable_table_options[controller_name][:include_relations]
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
module InstanceMethods
|
|
169
|
+
# default impl for listing a collection of objects. Override this action in your controller to fetch objects
|
|
170
|
+
# differently and/or to render a different template.
|
|
171
|
+
def index
|
|
172
|
+
get_sorted_objects(params)
|
|
173
|
+
render :template => 'sortable/index'
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Users can also pass in optional conditions that are used by the finder method call. For example if only wanted to
|
|
177
|
+
# show the items that had a certain status value you could pass in a condition 'mytable.status == 300' for example
|
|
178
|
+
# as the conditions parameter and when the finder is called the sortable table will only display objects that meet those
|
|
179
|
+
# conditions. Additionally you can paginate and sort the objects that are returned and apply the conditions to them.
|
|
180
|
+
def get_sorted_objects(params, options={})
|
|
181
|
+
objects = options[:objects].nil? ? sortable_class : options[:objects]
|
|
182
|
+
include_rel = options[:include_relations].nil? ? sortable_include_relations : options[:include_relations]
|
|
183
|
+
@headings = options[:table_headings].nil? ? sortable_table_headings : options[:table_headings]
|
|
184
|
+
sort_map = options[:sort_map].nil? ? sortable_sort_map : HashWithIndifferentAccess.new(options[:sort_map])
|
|
185
|
+
default_sort = options[:default_sort].nil? ? sortable_default_sort : options[:default_sort]
|
|
186
|
+
conditions = options[:conditions].nil? ? '' : options[:conditions]
|
|
187
|
+
search_array = options[:search_array].nil? ? sortable_search_array : options[:search_array]
|
|
188
|
+
|
|
189
|
+
conditions = process_search(params, conditions, search_array)
|
|
190
|
+
items_per_page = options[:per_page].nil? ? sortable_per_page : options[:per_page]
|
|
191
|
+
|
|
192
|
+
@sort_map = sort_map
|
|
193
|
+
sort = process_sort(params, sort_map, default_sort)
|
|
194
|
+
page = params[:page]
|
|
195
|
+
page ||= 1
|
|
196
|
+
# fetch the objects, paginated and sorted as desired along with any extra filtering conditions
|
|
197
|
+
get_paginated_objects(objects, sort, include_rel, conditions, page, items_per_page)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
def get_paginated_objects(objects, sort, include_rel, conditions, page, items_per_page)
|
|
202
|
+
@objects = objects.paginate(:include => include_rel,
|
|
203
|
+
:order => sort,
|
|
204
|
+
:conditions => conditions,
|
|
205
|
+
:page => page,
|
|
206
|
+
:per_page => items_per_page)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def process_search(params, conditions, search_array)
|
|
210
|
+
if !params[:q].blank?
|
|
211
|
+
columns_to_search = ''
|
|
212
|
+
values = Array.new
|
|
213
|
+
g = Generator.new(search_array)
|
|
214
|
+
g.each do |col|
|
|
215
|
+
columns_to_search += col + ' LIKE ? '
|
|
216
|
+
columns_to_search += 'OR ' unless g.end?
|
|
217
|
+
values<< "%#{params[:q]}%"
|
|
218
|
+
end
|
|
219
|
+
conditions += ' and' if !conditions.blank?
|
|
220
|
+
conditions = [conditions + ' (' + columns_to_search + ')'] + values
|
|
221
|
+
end
|
|
222
|
+
return conditions
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def value_provided?(params, name)
|
|
226
|
+
!params[name].nil? && !params[name].empty?
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def process_sort(params, sort_map, default_sort)
|
|
230
|
+
if params['sort']
|
|
231
|
+
sort = process_sort_param(params['sort'], sort_map)
|
|
232
|
+
else
|
|
233
|
+
# fetch the table.column from the sortmap for the given sort key and append the sort direction
|
|
234
|
+
sort = sort_map[default_sort[0]][0] + ' ' + default_sort[1]
|
|
235
|
+
|
|
236
|
+
# These variables are used in the sort_link_helper and sort_td_class_helper to build the column link headings
|
|
237
|
+
# and create the proper CSS class for the column heading for the case where there is no sort param.
|
|
238
|
+
@default_sort = default_sort[0]
|
|
239
|
+
if default_sort[1] && default_sort[1] == 'DESC'
|
|
240
|
+
@default_sort_key = default_sort[0] + '_reverse'
|
|
241
|
+
@sortclass = 'sortup'
|
|
242
|
+
else
|
|
243
|
+
@default_sort_key = default_sort[0]
|
|
244
|
+
@sortclass = 'sortdown'
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
return sort
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def process_sort_param(sort, sort_map)
|
|
251
|
+
mapKey = get_sort_key(sort)
|
|
252
|
+
|
|
253
|
+
if sort_map[mapKey].nil?
|
|
254
|
+
raise Exception.new("Invalid sort parameter passed #{sort}")
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
result = ''
|
|
258
|
+
sort_array = sort_map[mapKey]
|
|
259
|
+
# this adds support for more than one sort criteria for a given column
|
|
260
|
+
# for example, status DESC, created_at ASC
|
|
261
|
+
if sort_array[0].class == Array
|
|
262
|
+
g = Generator.new(sort_array)
|
|
263
|
+
g.each do |sort_value|
|
|
264
|
+
result = get_sort_direction(sort, sort_value)
|
|
265
|
+
result += ', ' unless g.end?
|
|
266
|
+
end
|
|
267
|
+
else
|
|
268
|
+
result = get_sort_direction(sort, sort_array)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
return result
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def get_sort_direction(sort, sort_value)
|
|
275
|
+
result = ''
|
|
276
|
+
column = sort_value[0]
|
|
277
|
+
direction = sort_value[1]
|
|
278
|
+
if /_reverse$/.match(sort)
|
|
279
|
+
if direction == 'DESC'
|
|
280
|
+
direction = 'ASC'
|
|
281
|
+
else
|
|
282
|
+
direction = 'DESC'
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
result += column + ' ' + direction
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def get_sort_key(sort)
|
|
289
|
+
i = sort.index('_reverse')
|
|
290
|
+
if i
|
|
291
|
+
mapKey = sort[0, i]
|
|
292
|
+
else
|
|
293
|
+
mapKey = sort
|
|
294
|
+
end
|
|
295
|
+
return mapKey
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module SortableHelper
|
|
2
|
+
# Helper method to generate a sortable table in the view
|
|
3
|
+
#
|
|
4
|
+
# usage: <%= sortable_table(optional_params) %>
|
|
5
|
+
#
|
|
6
|
+
# optional_params:
|
|
7
|
+
#
|
|
8
|
+
# :paginate - Whether or not to display pagination links for the table. Default is true.
|
|
9
|
+
# :partial - Name of the partial containing the table to display. Default is the table partial in the sortable
|
|
10
|
+
# plugin: 'sortable/table'
|
|
11
|
+
#
|
|
12
|
+
# If you choose to create your own partial in place of the one distributed with the sortable plugin
|
|
13
|
+
# your partial has the following available to it for generating the table:
|
|
14
|
+
# @headings contains the table heading values
|
|
15
|
+
# @objects contains the collection of objects to be displayed in the table
|
|
16
|
+
#
|
|
17
|
+
# :search - Whether or not to include a search field for the table. Default is true.
|
|
18
|
+
#
|
|
19
|
+
def sortable_table(options={})
|
|
20
|
+
paginate = options[:paginate].nil? ? true : options[:paginate]
|
|
21
|
+
partial = options[:partial].nil? ? 'sortable/table' : options[:partial]
|
|
22
|
+
search = options[:search].nil? ? true : options[:search]
|
|
23
|
+
|
|
24
|
+
result = render(:partial => partial, :locals => {:search => search})
|
|
25
|
+
result += will_paginate(@objects).to_s if paginate
|
|
26
|
+
result
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def sort_td_class_helper(param)
|
|
31
|
+
result = 'sortup' if params[:sort] == param
|
|
32
|
+
result = 'sortdown' if params[:sort] == param + "_reverse"
|
|
33
|
+
result = @sortclass if @default_sort && @default_sort == param
|
|
34
|
+
return result
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def sort_link_helper(action, text, param, params, extra_params={})
|
|
39
|
+
options = build_url_params(action, param, params, extra_params)
|
|
40
|
+
html_options = {:title => "Sort by this field"}
|
|
41
|
+
|
|
42
|
+
link_to(text, options, html_options)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def build_url_params(action, param, params, extra_params={})
|
|
46
|
+
key = param
|
|
47
|
+
if @default_sort_key && @default_sort == param
|
|
48
|
+
key = @default_sort_key
|
|
49
|
+
else
|
|
50
|
+
key += "_reverse" if params[:sort] == param
|
|
51
|
+
end
|
|
52
|
+
params = {:sort => key,
|
|
53
|
+
:page => nil, # when sorting we start over on page 1
|
|
54
|
+
:q => params[:q]}
|
|
55
|
+
params.merge!(extra_params)
|
|
56
|
+
|
|
57
|
+
return {:action => action, :params => params}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def row_cell_link(new_location)
|
|
61
|
+
mouseover_pointer + "onclick='window.location=\"#{new_location}\"'"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def mouseover_pointer
|
|
65
|
+
"onmouseover='this.style.cursor = \"pointer\"' onmouseout='this.style.cursor=\"auto\"'"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def table_header
|
|
69
|
+
result = "<tr class='tableHeaderRow'>"
|
|
70
|
+
@headings.each do |heading|
|
|
71
|
+
sort_class = sort_td_class_helper heading[1]
|
|
72
|
+
result += "<td"
|
|
73
|
+
result += " class='#{sort_class}'" if !sort_class.blank?
|
|
74
|
+
result += ">"
|
|
75
|
+
result += sort_link_helper @action, heading[0], heading[1], params
|
|
76
|
+
result += "</td>"
|
|
77
|
+
end
|
|
78
|
+
result += "</tr>"
|
|
79
|
+
return result
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Generated by jeweler
|
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
|
3
|
+
# Instead, edit Jeweler::Tasks in rakefile, and run the gemspec command
|
|
4
|
+
# -*- encoding: utf-8 -*-
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |s|
|
|
7
|
+
s.name = %q{sortable_table}
|
|
8
|
+
s.version = "0.1.0"
|
|
9
|
+
|
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
|
11
|
+
s.authors = ["Michael Kovacs"]
|
|
12
|
+
s.date = %q{2010-01-12}
|
|
13
|
+
s.description = %q{Rails plugin to produce a sortable, paginated, searchable table for any model}
|
|
14
|
+
s.email = %q{}
|
|
15
|
+
s.extra_rdoc_files = [
|
|
16
|
+
"README",
|
|
17
|
+
"TODO"
|
|
18
|
+
]
|
|
19
|
+
s.files = [
|
|
20
|
+
"README",
|
|
21
|
+
"Rakefile",
|
|
22
|
+
"TODO",
|
|
23
|
+
"VERSION",
|
|
24
|
+
"example/controllers/cablecar/users_controller.rb",
|
|
25
|
+
"example/models/cablecar/contact_info.rb",
|
|
26
|
+
"example/models/cablecar/user.rb",
|
|
27
|
+
"example/views/cablecar/users/index.html.erb",
|
|
28
|
+
"images/arrow_down.gif",
|
|
29
|
+
"images/arrow_up.gif",
|
|
30
|
+
"images/logo_opaque.png",
|
|
31
|
+
"init.rb",
|
|
32
|
+
"install.rb",
|
|
33
|
+
"lib/sortable.rb",
|
|
34
|
+
"lib/sortable_helper.rb",
|
|
35
|
+
"sortable_table.gemspec",
|
|
36
|
+
"test/example_test_routing.rb",
|
|
37
|
+
"test/test_helper.rb",
|
|
38
|
+
"test/users_controller_test.rb",
|
|
39
|
+
"views/sortable/_table.html.erb",
|
|
40
|
+
"views/sortable/index.erb"
|
|
41
|
+
]
|
|
42
|
+
s.homepage = %q{http://github.com/kovacs/sortable}
|
|
43
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
|
44
|
+
s.require_paths = ["lib"]
|
|
45
|
+
s.rubygems_version = %q{1.3.5}
|
|
46
|
+
s.summary = %q{Rails plugin to produce a sortable, paginated, searchable table for any model}
|
|
47
|
+
s.test_files = [
|
|
48
|
+
"test/example_test_routing.rb",
|
|
49
|
+
"test/test_helper.rb",
|
|
50
|
+
"test/users_controller_test.rb"
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
if s.respond_to? :specification_version then
|
|
54
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
|
55
|
+
s.specification_version = 3
|
|
56
|
+
|
|
57
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
|
58
|
+
else
|
|
59
|
+
end
|
|
60
|
+
else
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ENV["RAILS_ENV"] = 'test'
|
|
2
|
+
|
|
3
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
|
|
4
|
+
|
|
5
|
+
require 'test/unit'
|
|
6
|
+
require 'test_help'
|
|
7
|
+
|
|
8
|
+
class Test::Unit::TestCase
|
|
9
|
+
|
|
10
|
+
self.fixture_path = File.expand_path( File.join(File.dirname(__FILE__), 'fixtures') )
|
|
11
|
+
|
|
12
|
+
self.use_transactional_fixtures = true
|
|
13
|
+
self.use_instantiated_fixtures = false
|
|
14
|
+
|
|
15
|
+
end
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
|
2
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
|
|
3
|
+
|
|
4
|
+
def setup_sortable_db
|
|
5
|
+
old_stdout = $stdout
|
|
6
|
+
|
|
7
|
+
# AR keeps printing annoying schema statements
|
|
8
|
+
$stdout = StringIO.new
|
|
9
|
+
|
|
10
|
+
ActiveRecord::Base.logger
|
|
11
|
+
ActiveRecord::Schema.define(:version => 1) do
|
|
12
|
+
create_table :cablecar_users do |t|
|
|
13
|
+
t.column :username, :string
|
|
14
|
+
t.column :status, :string
|
|
15
|
+
t.column :contact_info_id, :integer
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
create_table :cablecar_contact_infos do |t|
|
|
19
|
+
t.column :name, :string
|
|
20
|
+
t.column :phone, :string
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
$stdout = old_stdout
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
setup_sortable_db
|
|
28
|
+
|
|
29
|
+
require File.dirname(__FILE__) + '/../example/controllers/cablecar/users_controller'
|
|
30
|
+
|
|
31
|
+
# Re-raise errors caught by the controller.
|
|
32
|
+
class Cablecar::UsersController
|
|
33
|
+
def rescue_action(e) raise e end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class UsersControllerTest < Test::Unit::TestCase
|
|
38
|
+
|
|
39
|
+
def teardown_db
|
|
40
|
+
ActiveRecord::Base.connection.tables.each do |table|
|
|
41
|
+
ActiveRecord::Base.connection.drop_table(table)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#fixtures :widgets
|
|
46
|
+
|
|
47
|
+
def setup
|
|
48
|
+
@controller = Cablecar::UsersController.new
|
|
49
|
+
@request = ActionController::TestRequest.new
|
|
50
|
+
@response = ActionController::TestResponse.new
|
|
51
|
+
|
|
52
|
+
30.times do |n|
|
|
53
|
+
c = Cablecar::ContactInfo.new(:name => "name#{n}")
|
|
54
|
+
c.user = Cablecar::User.new(:username => "user#{n}")
|
|
55
|
+
c.save!
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def teardown
|
|
60
|
+
teardown_db
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_should_show_paginated_table_using_default_action
|
|
64
|
+
get :index, :use_default => true
|
|
65
|
+
assert_equal 10, assigns(:objects).size
|
|
66
|
+
|
|
67
|
+
verify_sortable_table_html
|
|
68
|
+
verify_pagination_html
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_should_show_paginated_table
|
|
72
|
+
get :index
|
|
73
|
+
assert_equal 10, assigns(:objects).size
|
|
74
|
+
|
|
75
|
+
verify_sortable_table_html
|
|
76
|
+
verify_pagination_html
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_should_show_sorable_table_without_pagination
|
|
80
|
+
get :index, :no_pagination => true
|
|
81
|
+
assert_equal 10, assigns(:objects).size
|
|
82
|
+
|
|
83
|
+
verify_sortable_table_html
|
|
84
|
+
assert_select 'div.pagination', false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_should_show_paginated_table_with_overrides_and_related_columns
|
|
88
|
+
get :index, :complex_example => true
|
|
89
|
+
assert_equal 15, assigns(:objects).size
|
|
90
|
+
assert_select 'thead' do
|
|
91
|
+
assert_select 'tr' do
|
|
92
|
+
assert_select 'td', :count => 3
|
|
93
|
+
assert_select 'td.sortdown', 'Name'
|
|
94
|
+
assert_select 'td.sortdown' do
|
|
95
|
+
assert_select 'a[href=/cablecar/users?sort=name]', 'Name'
|
|
96
|
+
assert_select 'a[title=Sort by this field]', 'Name'
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
assert_select 'tbody' do
|
|
101
|
+
assert_select 'tr', :count => 15
|
|
102
|
+
verify_name_user_row_data(11, 22)
|
|
103
|
+
end
|
|
104
|
+
verify_pagination_html(2)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def test_should_sortdown_up_by_related_field
|
|
108
|
+
get :index, :sort => 'name', :complex_example => true
|
|
109
|
+
assert_equal 15, assigns(:objects).size
|
|
110
|
+
assert_select 'thead' do
|
|
111
|
+
assert_select 'tr' do
|
|
112
|
+
assert_select 'td', :count => 3
|
|
113
|
+
assert_select 'td.sortup', 'Name'
|
|
114
|
+
assert_select 'td.sortup' do
|
|
115
|
+
assert_select 'a[href=/cablecar/users?sort=name_reverse]', 'Name'
|
|
116
|
+
assert_select 'a[title=Sort by this field]', 'Name'
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
assert_select 'tbody' do
|
|
121
|
+
assert_select 'tr', :count => 15
|
|
122
|
+
verify_name_user_row_data(4, 10)
|
|
123
|
+
verify_name_user_row_data(23, 30)
|
|
124
|
+
end
|
|
125
|
+
verify_pagination_html(2)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def test_should_sortup_up_by_field_with_desc_default_sort
|
|
129
|
+
get :index, :sort => 'name_reverse', :complex_example => true
|
|
130
|
+
assert_equal 15, assigns(:objects).size
|
|
131
|
+
assert_select 'thead' do
|
|
132
|
+
assert_select 'tr' do
|
|
133
|
+
assert_select 'td', :count => 3
|
|
134
|
+
assert_select 'td.sortdown', 'Name'
|
|
135
|
+
assert_select 'td.sortdown' do
|
|
136
|
+
assert_select 'a[href=/cablecar/users?sort=name]', 'Name'
|
|
137
|
+
assert_select 'a[title=Sort by this field]', 'Name'
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
assert_select 'tbody' do
|
|
142
|
+
assert_select 'tr', :count => 15
|
|
143
|
+
verify_name_user_row_data(11, 22)
|
|
144
|
+
end
|
|
145
|
+
verify_pagination_html(2)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def test_should_sortup_up_by_field_with_asc_default_sort
|
|
149
|
+
get :index, :sort => 'status_reverse', :complex_example => true
|
|
150
|
+
assert_equal 15, assigns(:objects).size
|
|
151
|
+
assert_select 'thead' do
|
|
152
|
+
assert_select 'tr' do
|
|
153
|
+
assert_select 'td', :count => 3
|
|
154
|
+
assert_select 'td.sortdown', 'Status'
|
|
155
|
+
assert_select 'td.sortdown' do
|
|
156
|
+
assert_select 'a[href=/cablecar/users?sort=status]', 'Status'
|
|
157
|
+
assert_select 'a[title=Sort by this field]', 'Status'
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
assert_select 'tbody' do
|
|
162
|
+
assert_select 'tr', :count => 15
|
|
163
|
+
verify_name_user_row_data(1, 15)
|
|
164
|
+
end
|
|
165
|
+
verify_pagination_html(2)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def test_should_sortup_up_by_field_with_more_than_one_sort_param
|
|
169
|
+
get :index, :sort => 'username_reverse', :complex_example => true
|
|
170
|
+
assert_equal 15, assigns(:objects).size
|
|
171
|
+
assert_select 'thead' do
|
|
172
|
+
assert_select 'tr' do
|
|
173
|
+
assert_select 'td', :count => 3
|
|
174
|
+
assert_select 'td.sortdown', 'Username'
|
|
175
|
+
assert_select 'td.sortdown' do
|
|
176
|
+
assert_select 'a[href=/cablecar/users?sort=username]', 'Username'
|
|
177
|
+
assert_select 'a[title=Sort by this field]', 'Username'
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
assert_select 'tbody' do
|
|
182
|
+
assert_select 'tr', :count => 15
|
|
183
|
+
verify_name_user_row_data(1, 15)
|
|
184
|
+
end
|
|
185
|
+
verify_pagination_html(2)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def test_should_fail_to_sort_with_invalid_sort_param
|
|
189
|
+
assert_raise Exception do |ex|
|
|
190
|
+
get :index, :sort => 'username_invalid', :complex_example => true
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def test_should_override_class_defaults
|
|
195
|
+
Cablecar::UsersController.class_eval do
|
|
196
|
+
sortable_table Cablecar::User, {:include_relations => [:contact_info],
|
|
197
|
+
:table_headings => [['Username', 'username'], ['Status', 'status'], ['Name', 'name']],
|
|
198
|
+
:sort_map => {:username => [['cablecar_users.username', 'DESC'], ['cablecar_users.status', 'DESC']],
|
|
199
|
+
:status => ['cablecar_users.status', 'ASC'],
|
|
200
|
+
:name => ['cablecar_contact_infos.name', 'DESC']},
|
|
201
|
+
:default_sort => ['name', 'ASC'],
|
|
202
|
+
:per_page => 15}
|
|
203
|
+
end
|
|
204
|
+
get :index, :sort => 'name_reverse'
|
|
205
|
+
assert_equal 15, assigns(:objects).size
|
|
206
|
+
assert_select 'thead' do
|
|
207
|
+
assert_select 'tr' do
|
|
208
|
+
assert_select 'td', :count => 3
|
|
209
|
+
assert_select 'td.sortdown', 'Name'
|
|
210
|
+
assert_select 'td.sortdown' do
|
|
211
|
+
assert_select 'a[href=/cablecar/users?sort=name]', 'Name'
|
|
212
|
+
assert_select 'a[title=Sort by this field]', 'Name'
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
assert_select 'tbody' do
|
|
217
|
+
assert_select 'tr', :count => 15
|
|
218
|
+
verify_name_user_row_data(11, 22)
|
|
219
|
+
end
|
|
220
|
+
verify_pagination_html(2)
|
|
221
|
+
|
|
222
|
+
Cablecar::UsersController.class_eval do
|
|
223
|
+
sortable_table Cablecar::User # put things back where you found them
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def verify_name_user_row_data(start, finish)
|
|
228
|
+
start.upto(finish) do |n|
|
|
229
|
+
assert_select "tr#cablecar_user_#{n}" do
|
|
230
|
+
assert_select 'td', :count => 3
|
|
231
|
+
assert_select 'td', {:minimum => 1}, "name#{n}" # test that the name value is there
|
|
232
|
+
assert_select 'td', {:minimum => 1}, "user#{n}"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def verify_sortable_table_html(count=10)
|
|
238
|
+
verify_sortable_table_header
|
|
239
|
+
assert_select 'tbody' do
|
|
240
|
+
assert_select 'tr', :count => count
|
|
241
|
+
30.downto(21) do |n|
|
|
242
|
+
assert_select "tr#cablecar_user_#{n}" do
|
|
243
|
+
assert_select 'td', :count => 4
|
|
244
|
+
assert_select 'td', {:minimum => 1}, n # test that the id value is there
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def verify_sortable_table_header(href='/cablecar/users?sort=id_reverse')
|
|
251
|
+
assert_select 'thead' do
|
|
252
|
+
assert_select 'tr' do
|
|
253
|
+
assert_select 'td', :count => 4
|
|
254
|
+
assert_select 'td.sortup', 'Id'
|
|
255
|
+
assert_select 'td.sortup' do
|
|
256
|
+
assert_select "a[href=#{href}]", 'Id'
|
|
257
|
+
assert_select 'a[title=Sort by this field]', 'Id'
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def verify_pagination_html(pages=3)
|
|
264
|
+
assert_select 'div.pagination' do
|
|
265
|
+
assert_select 'span', :count => 2
|
|
266
|
+
assert_select 'a', :count => pages
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def test_truth
|
|
271
|
+
true
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def test_should_search
|
|
275
|
+
get :index, :q => 'user3'
|
|
276
|
+
assert_equal 1, assigns(:objects).size
|
|
277
|
+
|
|
278
|
+
verify_sortable_table_header('/cablecar/users?q=user3&sort=id_reverse')
|
|
279
|
+
|
|
280
|
+
assert_select 'tbody' do
|
|
281
|
+
assert_select 'tr', :count => 1
|
|
282
|
+
assert_select "tr#cablecar_user_4" do
|
|
283
|
+
assert_select 'td', :count => 4
|
|
284
|
+
assert_select 'td', {:minimum => 1}, 4 # test that the id value is there
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def test_should_search_and_return_multiple_results
|
|
290
|
+
get :index, :q => 'user4'
|
|
291
|
+
assert_equal 2, assigns(:objects).size
|
|
292
|
+
|
|
293
|
+
verify_sortable_table_header('/cablecar/users?q=user4&sort=id_reverse')
|
|
294
|
+
assert_select 'tbody' do
|
|
295
|
+
assert_select 'tr', :count => 2
|
|
296
|
+
assert_select "tr#cablecar_user_5" do
|
|
297
|
+
assert_select 'td', :count => 4
|
|
298
|
+
assert_select 'td', {:minimum => 1}, 4 # test that the id value is there
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def create_a_dupe_user
|
|
305
|
+
c = Cablecar::ContactInfo.new(:name => "dupe_name4")
|
|
306
|
+
c.user = Cablecar::User.new(:username => "dupe_user4")
|
|
307
|
+
c.user.status = 'active'
|
|
308
|
+
c.save!
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def test_should_search_and_filter_results
|
|
312
|
+
create_a_dupe_user
|
|
313
|
+
# This tests the ability to search as well as apply an additional filter that is used in conjunction with the
|
|
314
|
+
# search conditions
|
|
315
|
+
get :index, :q => 'user4', :active => 'true', :filter_example => true
|
|
316
|
+
assert_equal 1, assigns(:objects).size
|
|
317
|
+
|
|
318
|
+
verify_sortable_table_header('/cablecar/users?q=user4&sort=id_reverse')
|
|
319
|
+
assert_select 'tbody' do
|
|
320
|
+
assert_select 'tr', :count => 1
|
|
321
|
+
assert_select "tr#cablecar_user_31" do
|
|
322
|
+
assert_select 'td', :count => 4
|
|
323
|
+
assert_select 'td', {:minimum => 1}, 4 # test that the id value is there
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def test_should_search_and_return_multiple_results
|
|
330
|
+
create_a_dupe_user
|
|
331
|
+
|
|
332
|
+
get :index, :q => 'user4'
|
|
333
|
+
assert_equal 2, assigns(:objects).size
|
|
334
|
+
|
|
335
|
+
verify_sortable_table_header('/cablecar/users?q=user4&sort=id_reverse')
|
|
336
|
+
assert_select 'tbody' do
|
|
337
|
+
assert_select 'tr', :count => 2
|
|
338
|
+
assert_select "tr#cablecar_user_5" do
|
|
339
|
+
assert_select 'td', :count => 4
|
|
340
|
+
assert_select 'td', {:minimum => 1}, 4 # test that the id value is there
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.sortableTable {
|
|
3
|
+
border: 1px solid #999;
|
|
4
|
+
background-image:url(../images/logo_opaque.png);
|
|
5
|
+
background-position: center;
|
|
6
|
+
background-repeat:no-repeat;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.sortableTable td {
|
|
10
|
+
font-size: .95em;
|
|
11
|
+
padding: 3px 6px;
|
|
12
|
+
color: #101010;
|
|
13
|
+
border-bottom: 1px solid #999;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.sortableTable tr.tableHeaderRow td {
|
|
17
|
+
color: #830519;
|
|
18
|
+
background: #c7e1f0;
|
|
19
|
+
border-top: 1px solid #830519;
|
|
20
|
+
border-bottom: 1px solid #830519;
|
|
21
|
+
font-weight: bold;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.sortableTable tr.tableHeaderRow td.sortup {
|
|
25
|
+
background-color: #faf1ed;
|
|
26
|
+
background-image:url(../images/arrow_down.gif);
|
|
27
|
+
background-position: top right;
|
|
28
|
+
background-repeat:no-repeat;
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.sortableTable tr.tableHeaderRow td.sortdown {
|
|
33
|
+
background-color: #faf1ed;
|
|
34
|
+
background-image:url(../images/arrow_up.gif);
|
|
35
|
+
background-position: top right;
|
|
36
|
+
background-repeat:no-repeat;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#search {
|
|
40
|
+
padding: 10px;
|
|
41
|
+
}
|
|
42
|
+
</style>
|
|
43
|
+
|
|
44
|
+
<% if search %>
|
|
45
|
+
<div id='search'>
|
|
46
|
+
<% form_tag({:action => @action}, {:method => 'get', :id => 'search_form'}) do %>
|
|
47
|
+
<%=text_field_tag("q", params['q'], :size => 10 ) %>
|
|
48
|
+
<%=submit_tag 'Search'%>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
51
|
+
<% end %>
|
|
52
|
+
|
|
53
|
+
<table width="100%" class='sortableTable' cellpadding="0" cellspacing="0">
|
|
54
|
+
<thead>
|
|
55
|
+
<%= table_header %>
|
|
56
|
+
</thead>
|
|
57
|
+
<tbody>
|
|
58
|
+
<%if @objects.size == 0%>
|
|
59
|
+
<tr><td colspan='<%= @headings.size %>'>No items in the list</td></tr>
|
|
60
|
+
<%else
|
|
61
|
+
@objects.each do |object|
|
|
62
|
+
klass = cycle('odd', 'even')
|
|
63
|
+
%>
|
|
64
|
+
<tr id='<%=dom_id(object)%>' class='<%= klass %>'>
|
|
65
|
+
<% @headings.each do |heading| %>
|
|
66
|
+
<td>
|
|
67
|
+
<%= object.instance_eval(heading[1]) %>
|
|
68
|
+
</td>
|
|
69
|
+
<% end %>
|
|
70
|
+
</tr>
|
|
71
|
+
<% end
|
|
72
|
+
end%>
|
|
73
|
+
</tbody>
|
|
74
|
+
</table>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= sortable_table %>
|
metadata
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sortable_table
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Michael Kovacs
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2010-01-12 00:00:00 -05:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies: []
|
|
15
|
+
|
|
16
|
+
description: Rails plugin to produce a sortable, paginated, searchable table for any model
|
|
17
|
+
email: ""
|
|
18
|
+
executables: []
|
|
19
|
+
|
|
20
|
+
extensions: []
|
|
21
|
+
|
|
22
|
+
extra_rdoc_files:
|
|
23
|
+
- README
|
|
24
|
+
- TODO
|
|
25
|
+
files:
|
|
26
|
+
- README
|
|
27
|
+
- Rakefile
|
|
28
|
+
- TODO
|
|
29
|
+
- VERSION
|
|
30
|
+
- example/controllers/cablecar/users_controller.rb
|
|
31
|
+
- example/models/cablecar/contact_info.rb
|
|
32
|
+
- example/models/cablecar/user.rb
|
|
33
|
+
- example/views/cablecar/users/index.html.erb
|
|
34
|
+
- images/arrow_down.gif
|
|
35
|
+
- images/arrow_up.gif
|
|
36
|
+
- images/logo_opaque.png
|
|
37
|
+
- init.rb
|
|
38
|
+
- install.rb
|
|
39
|
+
- lib/sortable.rb
|
|
40
|
+
- lib/sortable_helper.rb
|
|
41
|
+
- sortable_table.gemspec
|
|
42
|
+
- test/example_test_routing.rb
|
|
43
|
+
- test/test_helper.rb
|
|
44
|
+
- test/users_controller_test.rb
|
|
45
|
+
- views/sortable/_table.html.erb
|
|
46
|
+
- views/sortable/index.erb
|
|
47
|
+
has_rdoc: true
|
|
48
|
+
homepage: http://github.com/kovacs/sortable
|
|
49
|
+
licenses: []
|
|
50
|
+
|
|
51
|
+
post_install_message:
|
|
52
|
+
rdoc_options:
|
|
53
|
+
- --charset=UTF-8
|
|
54
|
+
require_paths:
|
|
55
|
+
- lib
|
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: "0"
|
|
61
|
+
version:
|
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: "0"
|
|
67
|
+
version:
|
|
68
|
+
requirements: []
|
|
69
|
+
|
|
70
|
+
rubyforge_project:
|
|
71
|
+
rubygems_version: 1.3.5
|
|
72
|
+
signing_key:
|
|
73
|
+
specification_version: 3
|
|
74
|
+
summary: Rails plugin to produce a sortable, paginated, searchable table for any model
|
|
75
|
+
test_files:
|
|
76
|
+
- test/example_test_routing.rb
|
|
77
|
+
- test/test_helper.rb
|
|
78
|
+
- test/users_controller_test.rb
|