handles_sortable_columns 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Alex Fortuna
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,118 @@
1
+
2
+ Sortable Table Columns
3
+ ======================
4
+
5
+
6
+ Introduction
7
+ ------------
8
+
9
+ A simple yet flexible Rails gem/plugin to quickly add sortable table columns to your controller and views.
10
+
11
+
12
+ Setup
13
+ -----
14
+
15
+ $ gem sources --add http://rubygems.org
16
+ $ gem install handles_sortable_columns
17
+
18
+ In your app's `config/environment.rb` do a:
19
+
20
+ config.gem "handles_sortable_columns"
21
+
22
+
23
+ Basic Usage
24
+ -----------
25
+
26
+ Activate the feature in your controller class:
27
+
28
+ class MyController < ApplicationController
29
+ handles_sortable_columns
30
+ ...
31
+
32
+ In a view, mark up sortable columns by using the <tt>sortable_column</tt> helper:
33
+
34
+ <%= sortable_column "Product" %>
35
+ <%= sortable_column "Price" %>
36
+
37
+ In controller action, fetch and use the order clause according to current state of sortable columns:
38
+
39
+ def index
40
+ order = sortable_column_order
41
+ @records = Article.all(:order => order)
42
+ end
43
+
44
+ That's it for basic usage. Production usage may require passing additional parameters to listed methods.
45
+
46
+
47
+ Production Usage
48
+ ----------------
49
+
50
+ Please take time to read the gem's full [RDoc documentation](http://rdoc.info/projects/dadooda/handles_sortable_columns). This README has a limited coverage.
51
+
52
+
53
+ ### Configuration ###
54
+
55
+ Change names of GET parameters used for sorting and pagination:
56
+
57
+ class MyController < ApplicationController
58
+ handles_sortable_columns do |conf|
59
+ conf.sort_param = "s"
60
+ conf.page_param = "p"
61
+ end
62
+ ...
63
+
64
+ Change CSS class of all sortable column `<a>` tags:
65
+
66
+ handles_sortable_columns do |conf|
67
+ conf.class = "SortableLink"
68
+ conf.indicator_class = {:asc => "AscSortableLink", :desc => "DescSortableLink"}
69
+ end
70
+
71
+ Change how text-based sort indicator looks like:
72
+
73
+ handles_sortable_columns do |conf|
74
+ conf.indicator_text = {:asc => "[asc]", :desc => "[desc]"}
75
+ end
76
+
77
+ Disable text-based sort indicator completely:
78
+
79
+ handles_sortable_columns do |conf|
80
+ conf.indicator_text = {}
81
+ end
82
+
83
+
84
+ ### Helper Options ###
85
+
86
+ Explicitly specify column name:
87
+
88
+ <%= sortable_column "Highest Price", :column => "max_price" %>
89
+
90
+ Specify CSS class for this particular link:
91
+
92
+ <%= sortable_column "Name", :class => "SortableLink" %>
93
+
94
+ Specify sort direction on first click:
95
+
96
+ <%= sortable_column "Created At", :direction => :asc %>
97
+
98
+
99
+ ### Fetching Sort Order ###
100
+
101
+ To fetch sort order **securely**, with **column name validation**, **default values** and **multiple sort criteria**, use the block form of `sortable_column_order`:
102
+
103
+ order = sortable_column_order do |column, direction|
104
+ case column
105
+ when "name"
106
+ "#{column} #{direction}"
107
+ when "created_at", "updated_at"
108
+ "#{column} #{direction}, name ASC"
109
+ else
110
+ "name ASC"
111
+ end
112
+ end
113
+
114
+
115
+ Feedback
116
+ --------
117
+
118
+ Send bug reports, suggestions and criticisms through [project's page on GitHub](http://github.com/dadooda/handles_sortable_columns).
data/Rakefile ADDED
@@ -0,0 +1,51 @@
1
+ require "rake/rdoctask"
2
+
3
+ GEM_NAME = "handles_sortable_columns"
4
+
5
+ begin
6
+ require "jeweler"
7
+ Jeweler::Tasks.new do |gem|
8
+ gem.name = GEM_NAME
9
+ gem.summary = "Sortable Table Columns"
10
+ gem.description = gem.summary
11
+ gem.email = "alex.r@askit.org"
12
+ gem.homepage = "http://github.com/dadooda/handles_sortable_titles"
13
+ gem.authors = ["Alex Fortuna"]
14
+ gem.files = FileList[
15
+ "[A-Z]*",
16
+ "*.gemspec",
17
+ "lib/**/*.rb",
18
+ "init.rb",
19
+ ] - ["README.html"]
20
+ gem.extra_rdoc_files = ["README.md"]
21
+ end
22
+ rescue LoadError
23
+ STDERR.puts "This gem requires Jeweler to be built"
24
+ end
25
+
26
+ desc "Rebuild gemspec and package"
27
+ task :rebuild => [:gemspec, :build]
28
+
29
+ desc "Push (publish) gem to RubyGems (aka Gemcutter)"
30
+ task :push => :rebuild do
31
+ # Yet found no way to ask Jeweler forge a complete version string for us.
32
+ vh = YAML.load(File.read("VERSION.yml"))
33
+ version = [vh[:major], vh[:minor], vh[:patch]].join(".")
34
+ pkgfile = File.join("pkg", [GEM_NAME, "-", version, ".gem"].to_s)
35
+ system("gem", "push", pkgfile)
36
+ end
37
+
38
+ desc "Compile README preview"
39
+ task :readme do
40
+ require "kramdown"
41
+
42
+ doc = Kramdown::Document.new(File.read "README.md")
43
+
44
+ fn = "README.html"
45
+ puts "Writing '#{fn}'..."
46
+ File.open(fn, "w") do |f|
47
+ f.write(File.read "dev/head.html")
48
+ f.write(doc.to_html)
49
+ end
50
+ puts ": ok"
51
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 0
@@ -0,0 +1,47 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{handles_sortable_columns}
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 = ["Alex Fortuna"]
12
+ s.date = %q{2010-08-26}
13
+ s.description = %q{Sortable Table Columns}
14
+ s.email = %q{alex.r@askit.org}
15
+ s.extra_rdoc_files = [
16
+ "README.md"
17
+ ]
18
+ s.files = [
19
+ "MIT-LICENSE",
20
+ "README.md",
21
+ "Rakefile",
22
+ "VERSION.yml",
23
+ "handles_sortable_columns.gemspec",
24
+ "init.rb",
25
+ "lib/action_controller/base/handles_sortable_columns.rb",
26
+ "lib/handles/sortable_columns.rb",
27
+ "lib/handles_sortable_columns.rb"
28
+ ]
29
+ s.homepage = %q{http://github.com/dadooda/handles_sortable_titles}
30
+ s.rdoc_options = ["--charset=UTF-8"]
31
+ s.require_paths = ["lib"]
32
+ s.rubygems_version = %q{1.3.5}
33
+ s.summary = %q{Sortable Table Columns}
34
+ s.test_files = [
35
+ "spec/handles_sortable_columns_spec.rb"
36
+ ]
37
+
38
+ if s.respond_to? :specification_version then
39
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
40
+ s.specification_version = 3
41
+
42
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
43
+ else
44
+ end
45
+ else
46
+ end
47
+ end
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ # Rails plugin init.
2
+ require File.join(File.dirname(__FILE__), "lib/handles_sortable_columns")
@@ -0,0 +1,3 @@
1
+ class ActionController::Base #:nodoc:
2
+ include Handles::SortableColumns
3
+ end
@@ -0,0 +1,261 @@
1
+ module Handles #:nodoc:
2
+ # == Overview
3
+ #
4
+ # A sortable columns feature for your controller and views.
5
+ #
6
+ # == Basic Usage
7
+ #
8
+ # Activate the feature in your controller class:
9
+ #
10
+ # class MyController < ApplicationController
11
+ # handles_sortable_columns
12
+ # ...
13
+ #
14
+ # In a view, mark up sortable columns by using the <tt>sortable_column</tt> helper:
15
+ #
16
+ # <%= sortable_column "Product" %>
17
+ # <%= sortable_column "Price" % >
18
+ #
19
+ # In controller action, fetch and use the order clause according to current state of sortable columns:
20
+ #
21
+ # def index
22
+ # order = sortable_column_order
23
+ # @records = Article.all(:order => order)
24
+ # end
25
+ #
26
+ # That's it for basic usage. Production usage may require passing additional parameters to listed methods.
27
+ #
28
+ # See also:
29
+ # * <tt>MetaClassMethods#handles_sortable_columns</tt>
30
+ # * <tt>HelperMethods#sortable_column</tt>
31
+ # * <tt>InstanceMethods#sortable_column_order</tt>
32
+ module SortableColumns
33
+ def self.included(owner)
34
+ owner.extend MetaClassMethods
35
+ end
36
+
37
+ # Sortable columns configuration object. Passed to block when you do a:
38
+ #
39
+ # handles_sortable_column do |conf|
40
+ # ...
41
+ # end
42
+ class Config
43
+ # CSS class for link (regardless of sorted state). Default:
44
+ #
45
+ # nil
46
+ attr_accessor :class
47
+
48
+ # GET parameter name for page number. Default:
49
+ #
50
+ # page
51
+ attr_accessor :page_param
52
+
53
+ # GET parameter name for sort column and direction. Default:
54
+ #
55
+ # sort
56
+ attr_accessor :sort_param
57
+
58
+ # Sort indicator text. If any of values are empty, indicator is not displayed. Default:
59
+ #
60
+ # {:asc => "&nbsp;&darr;&nbsp;", :desc => "&nbsp;&uarr;&nbsp;"}
61
+ attr_accessor :indicator_text
62
+
63
+ # Sort indicator class. Default:
64
+ #
65
+ # {:asc => "SortedAsc", :desc => "SortedDesc"}
66
+ attr_accessor :indicator_class
67
+
68
+ def initialize(attrs = {})
69
+ defaults = {
70
+ :page_param => "page",
71
+ :sort_param => "sort",
72
+ :indicator_text => {:asc => "&nbsp;&darr;&nbsp;", :desc => "&nbsp;&uarr;&nbsp;"},
73
+ :indicator_class => {:asc => "SortedAsc", :desc => "SortedDesc"},
74
+ }
75
+
76
+ defaults.merge(attrs).each {|k, v| send("#{k}=", v)}
77
+ end
78
+
79
+ def [](key)
80
+ send(key)
81
+ end
82
+
83
+ def []=(key, value)
84
+ send("#{key}=", value)
85
+ end
86
+ end # Config
87
+
88
+ module MetaClassMethods
89
+ # Activate and optionally configure the sortable columns.
90
+ #
91
+ # class MyController < ApplicationController
92
+ # handles_sortable_columns
93
+ # end
94
+ #
95
+ # With configuration:
96
+ #
97
+ # class MyController < ApplicationController
98
+ # handles_sortable_columns do |conf|
99
+ # conf.sort_param = "s"
100
+ # conf.page_param = "p"
101
+ # conf.indicator_text = {}
102
+ # ...
103
+ # end
104
+ # end
105
+ #
106
+ # <tt>conf</tt> is a <tt>Config</tt> object.
107
+ def handles_sortable_columns(&block)
108
+ # Multiple activation protection.
109
+ if not self < InstanceMethods
110
+ extend ClassMethods
111
+ include InstanceMethods
112
+ helper HelperMethods
113
+ end
114
+
115
+ # Configuration is processed at every activation.
116
+ yield(sortable_columns_config) if block
117
+ end
118
+ end # MetaClassMethods
119
+
120
+ module ClassMethods
121
+ # Internal/advanced use only. Access/initialize the sortable columns config.
122
+ def sortable_columns_config
123
+ # NOTE: This is controller's class instance variable.
124
+ @sortable_columns_config ||= ::Handles::SortableColumns::Config.new
125
+ end
126
+
127
+ # Internal/advanced use only. Convert title to sortable column name.
128
+ #
129
+ # sortable_column_name_from_title("ProductName") # => "product_name"
130
+ def sortable_column_name_from_title(title)
131
+ title.gsub(/(\s)(\S)/) {$2.upcase}.underscore
132
+ end
133
+
134
+ # Internal/advanced use only. Parse sortable column sort param into a Hash with predefined keys.
135
+ #
136
+ # parse_sortable_column_sort_param("name") # => {:column => "name", :direction => :asc}
137
+ # parse_sortable_column_sort_param("-name") # => {:column => "name", :direction => :desc}
138
+ # parse_sortable_column_sort_param("") # => {:column => nil, :direction => nil}
139
+ def parse_sortable_column_sort_param(sort)
140
+ out = {:column => nil, :direction => nil}
141
+ if sort.to_s.strip.match /\A((?:-|))([^-]+)\z/
142
+ out[:direction] = $1.empty?? :asc : :desc
143
+ out[:column] = $2.strip
144
+ end
145
+ out
146
+ end
147
+ end # ClassMethods
148
+
149
+ module InstanceMethods
150
+ protected
151
+
152
+ # Compile SQL order clause according to current state of sortable columns.
153
+ #
154
+ # Basic (kickstart) usage:
155
+ #
156
+ # order = sortable_column_order
157
+ #
158
+ # <b>WARNING!</b> Basic usage is <b>not recommended</b> for production since it is potentially
159
+ # vulnerable to SQL injection!
160
+ #
161
+ # Production usage with multiple sort criteria, column name validation and defaults:
162
+ #
163
+ # order = sortable_column_order do |column, direction|
164
+ # case column
165
+ # when "name"
166
+ # "#{column} #{direction}"
167
+ # when "created_at", "updated_at"
168
+ # "#{column} #{direction}, name ASC"
169
+ # else
170
+ # "name ASC"
171
+ # end
172
+ # end
173
+ #
174
+ # Apply order:
175
+ #
176
+ # @records = Article.all(:order => order) # Rails 2.x.
177
+ # @records = Article.order(order) # Rails 3.
178
+ def sortable_column_order(&block)
179
+ conf = {}
180
+ conf[k = :sort_param] = self.class.sortable_columns_config[k]
181
+
182
+ # Parse sort param.
183
+ pp = self.class.parse_sortable_column_sort_param(params[conf[:sort_param]])
184
+
185
+ order = if block
186
+ yield(pp[:column], pp[:direction])
187
+ else
188
+ # No block -- do a straight mapping.
189
+ if pp[:column]
190
+ [pp[:column], pp[:direction]].join(" ")
191
+ end
192
+ end
193
+
194
+ # Can be nil.
195
+ order
196
+ end
197
+ end # InstanceMethods
198
+
199
+ module HelperMethods
200
+ # Render a sortable column link.
201
+ #
202
+ # Options:
203
+ # * <tt>:column</tt> -- Column name. E.g. <tt>"created_at"</tt>.
204
+ # * <tt>:direction</tt> -- Sort direction on first click. <tt>:asc</tt> or <tt>:desc</tt>. Default is <tt>:asc</tt>.
205
+ # * <tt>:class</tt> -- CSS class for link (regardless of sorted state).
206
+ #
207
+ # Examples:
208
+ #
209
+ # <%= sortable_column "Product" %>
210
+ # <%= sortable_column "Highest Price", :column_name => "max_price" %>
211
+ # <%= sortable_column "Name", :class => "SortableLink" %>
212
+ # <%= sortable_column "Created At", :direction => :asc %>
213
+ def sortable_column(title, options = {})
214
+ options = options.dup
215
+ o = {}
216
+ conf = {}
217
+ conf[k = :sort_param] = controller.class.sortable_columns_config[k]
218
+ conf[k = :page_param] = controller.class.sortable_columns_config[k]
219
+ conf[k = :indicator_text] = controller.class.sortable_columns_config[k]
220
+ conf[k = :indicator_class] = controller.class.sortable_columns_config[k]
221
+
222
+ #HELP sortable_column
223
+ o[k = :column] = options.delete(k) || controller.class.sortable_column_name_from_title(title)
224
+ o[k = :direction] = options.delete(k).to_s.downcase =~ /\Adesc\z/ ? :desc : :asc
225
+ o[k = :class] = options.delete(k) || controller.class.sortable_columns_config[k]
226
+ #HELP /sortable_column
227
+
228
+ raise "Unknown option(s): #{options.inspect}" if not options.empty?
229
+
230
+ # Parse sort param.
231
+ pp = controller.class.parse_sortable_column_sort_param(params[conf[:sort_param]])
232
+
233
+ css_class = []
234
+ if (s = o[:class]).present?
235
+ css_class << s
236
+ end
237
+
238
+ # Build link itself.
239
+ pcs = []
240
+
241
+ # Already sorted?
242
+ if pp[:column] == o[:column].to_s
243
+ if (s = conf[:indicator_class][pp[:direction]]).present?
244
+ css_class << s
245
+ end
246
+
247
+ pcs << link_to(title, params.merge({conf[:sort_param] => [("-" if pp[:direction] == :asc), o[:column]].join, conf[:page_param] => 1}), {:class => css_class.present?? css_class.join(" ") : nil}) # Opposite sort order when clicked.
248
+
249
+ if (s = conf[:indicator_text][pp[:direction]]).present?
250
+ pcs << s
251
+ end
252
+ else
253
+ # Not sorted.
254
+ pcs << link_to(title, params.merge({conf[:sort_param] => [("-" if o[:direction] != :asc), o[:column]].join, conf[:page_param] => 1}), {:class => css_class.present?? css_class.join(" ") : nil})
255
+ end
256
+
257
+ pcs.join
258
+ end
259
+ end # HelperMethods
260
+ end # SortableColumns
261
+ end # Handles
@@ -0,0 +1,4 @@
1
+ # Rails gem init.
2
+ Dir[File.join(File.dirname(__FILE__), "**/*.rb")].each do |fn|
3
+ require fn
4
+ end
@@ -0,0 +1,42 @@
1
+ describe "ClassMethods" do
2
+ module WrapSortableColumnsClassMethods
3
+ class MyController < ActionController::Base
4
+ handles_sortable_columns
5
+ end
6
+ end
7
+
8
+ describe "#sortable_column_name_from_title" do
9
+ it "generally works" do
10
+ tests = [
11
+ ["Product", "product"],
12
+ ["product", "product"],
13
+ ["created_at", "created_at"],
14
+ ["created at", "created_at"],
15
+ ["CreatedAt", "created_at"],
16
+ ["Created At", "created_at"],
17
+ ]
18
+
19
+ tests.each do |input, expected|
20
+ WrapSortableColumnsClassMethods::MyController.sortable_column_name_from_title(input).should == expected
21
+ end
22
+ end
23
+ end # #sortable_column_name_from_title
24
+
25
+ describe "#parse_sortable_column_sort_param" do
26
+ it "generally works" do
27
+ tests = [
28
+ ["name", {:column => "name", :direction => :asc}],
29
+ ["-name", {:column => "name", :direction => :desc}],
30
+ [" -name ", {:column => "name", :direction => :desc}],
31
+ ["", {:column => nil, :direction => nil}],
32
+ ["-", {:column => nil, :direction => nil}],
33
+ ["- name", {:column => "name", :direction => :desc}],
34
+ ["--kaka", {:column => nil, :direction => nil}],
35
+ ]
36
+
37
+ tests.each do |input, expected|
38
+ WrapSortableColumnsClassMethods::MyController.parse_sortable_column_sort_param(input).should == expected
39
+ end
40
+ end
41
+ end # #parse_sortable_column_sort_param
42
+ end # ClassMethods
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: handles_sortable_columns
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Alex Fortuna
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-08-26 00:00:00 +04:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Sortable Table Columns
17
+ email: alex.r@askit.org
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.md
24
+ files:
25
+ - MIT-LICENSE
26
+ - README.md
27
+ - Rakefile
28
+ - VERSION.yml
29
+ - handles_sortable_columns.gemspec
30
+ - init.rb
31
+ - lib/action_controller/base/handles_sortable_columns.rb
32
+ - lib/handles/sortable_columns.rb
33
+ - lib/handles_sortable_columns.rb
34
+ has_rdoc: true
35
+ homepage: http://github.com/dadooda/handles_sortable_titles
36
+ licenses: []
37
+
38
+ post_install_message:
39
+ rdoc_options:
40
+ - --charset=UTF-8
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ version:
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ requirements: []
56
+
57
+ rubyforge_project:
58
+ rubygems_version: 1.3.5
59
+ signing_key:
60
+ specification_version: 3
61
+ summary: Sortable Table Columns
62
+ test_files:
63
+ - spec/handles_sortable_columns_spec.rb