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