sortable_table 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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.
@@ -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,7 @@
1
+ class Cablecar::User < ActiveRecord::Base
2
+ set_table_name 'cablecar_users'
3
+
4
+ def name
5
+ self.contact_info.name
6
+ end
7
+ end
@@ -0,0 +1 @@
1
+ <%= sortable_table :paginate => !@hide_pagination%>
Binary file
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
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -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
+
@@ -0,0 +1,16 @@
1
+ module ActionController
2
+ module Routing
3
+ class RouteSet
4
+ def draw
5
+ clear!
6
+ map = Mapper.new(self)
7
+ map.namespace :cablecar do |strong|
8
+ strong.resources 'users'
9
+ end
10
+
11
+ yield map
12
+ install_helpers
13
+ end
14
+ end
15
+ end
16
+ end
@@ -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&amp;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&amp;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&amp;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&amp;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