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 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