aguids-positionable 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +11 -0
- data/LICENSE +20 -0
- data/README.markdown +200 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/app/controllers/positionable_controller.rb +32 -0
- data/app/helpers/positionable_helper.rb +70 -0
- data/config/routes.rb +3 -0
- data/init.rb +1 -0
- data/lib/positionable.rb +362 -0
- data/positionable.gemspec +154 -0
- data/test/rails_root/Rakefile +10 -0
- data/test/rails_root/app/controllers/application_controller.rb +10 -0
- data/test/rails_root/app/helpers/application_helper.rb +3 -0
- data/test/rails_root/app/models/conditional_item.rb +3 -0
- data/test/rails_root/app/models/different_position_column_item.rb +3 -0
- data/test/rails_root/app/models/hell_on_earth_item.rb +5 -0
- data/test/rails_root/app/models/multiple_list_item.rb +5 -0
- data/test/rails_root/app/models/multiple_scoped_item.rb +3 -0
- data/test/rails_root/app/models/scoped_item.rb +3 -0
- data/test/rails_root/app/models/simple_item.rb +4 -0
- data/test/rails_root/config/boot.rb +110 -0
- data/test/rails_root/config/database.yml +22 -0
- data/test/rails_root/config/environment.rb +14 -0
- data/test/rails_root/config/environments/development.rb +17 -0
- data/test/rails_root/config/environments/production.rb +28 -0
- data/test/rails_root/config/environments/test.rb +28 -0
- data/test/rails_root/config/initializers/backtrace_silencers.rb +7 -0
- data/test/rails_root/config/initializers/inflections.rb +10 -0
- data/test/rails_root/config/initializers/mime_types.rb +5 -0
- data/test/rails_root/config/initializers/new_rails_defaults.rb +19 -0
- data/test/rails_root/config/initializers/session_store.rb +15 -0
- data/test/rails_root/config/locales/en.yml +5 -0
- data/test/rails_root/config/routes.rb +45 -0
- data/test/rails_root/db/development.sqlite3 +0 -0
- data/test/rails_root/db/migrate/20090702170207_create_simple_items.rb +13 -0
- data/test/rails_root/db/migrate/20090703071830_create_scoped_items.rb +14 -0
- data/test/rails_root/db/migrate/20090706180046_create_different_position_column_items.rb +13 -0
- data/test/rails_root/db/migrate/20090706200318_create_conditional_items.rb +14 -0
- data/test/rails_root/db/migrate/20090708045618_create_multiple_scoped_items.rb +15 -0
- data/test/rails_root/db/migrate/20090708174947_create_multiple_list_items.rb +15 -0
- data/test/rails_root/db/migrate/20090708183550_create_hell_on_earth_items.rb +19 -0
- data/test/rails_root/db/schema.rb +68 -0
- data/test/rails_root/script/about +4 -0
- data/test/rails_root/script/console +3 -0
- data/test/rails_root/script/dbconsole +3 -0
- data/test/rails_root/script/destroy +3 -0
- data/test/rails_root/script/generate +3 -0
- data/test/rails_root/script/performance/benchmarker +3 -0
- data/test/rails_root/script/performance/profiler +3 -0
- data/test/rails_root/script/plugin +3 -0
- data/test/rails_root/script/runner +3 -0
- data/test/rails_root/script/server +3 -0
- data/test/rails_root/test/conditional_tests.rb +28 -0
- data/test/rails_root/test/functional/positionable_controller_test.rb +50 -0
- data/test/rails_root/test/performance/browsing_test.rb +9 -0
- data/test/rails_root/test/scoped_tests.rb +53 -0
- data/test/rails_root/test/simple_gap_tests.rb +17 -0
- data/test/rails_root/test/simple_gapless_tests.rb +52 -0
- data/test/rails_root/test/simple_tests.rb +185 -0
- data/test/rails_root/test/test_helper.rb +37 -0
- data/test/rails_root/test/unit/conditional_item_test.rb +36 -0
- data/test/rails_root/test/unit/different_position_column_item_test.rb +21 -0
- data/test/rails_root/test/unit/eval_support_test.rb +62 -0
- data/test/rails_root/test/unit/hell_on_earth_item_test.rb +71 -0
- data/test/rails_root/test/unit/helpers/positionable_helper_test.rb +69 -0
- data/test/rails_root/test/unit/multiple_list_item_test.rb +60 -0
- data/test/rails_root/test/unit/multiple_scoped_item_test.rb +34 -0
- data/test/rails_root/test/unit/scoped_item_test.rb +22 -0
- data/test/rails_root/test/unit/scoped_item_with_gaps_test.rb +25 -0
- data/test/rails_root/test/unit/simple_item_test.rb +21 -0
- data/test/rails_root/test/unit/simple_item_with_gaps_test.rb +24 -0
- metadata +172 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Felipe Doria
|
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.markdown
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
Positionable
|
2
|
+
============
|
3
|
+
|
4
|
+
Built on top of the rails [acts-as-list][] plugin bringing forth concepts from Sean Huber's [sortable][]. This gem brings positionable extension to controllers and helpers.
|
5
|
+
|
6
|
+
Install
|
7
|
+
-------
|
8
|
+
|
9
|
+
Specify it in your Rails config.
|
10
|
+
|
11
|
+
config.gem 'aguids-positionable', :lib => 'positionable', :source => 'http://gems.github.com'
|
12
|
+
|
13
|
+
Then install it.
|
14
|
+
|
15
|
+
rake gems:install
|
16
|
+
|
17
|
+
Usage
|
18
|
+
-----
|
19
|
+
|
20
|
+
The first step is to declare a model that acts-as-positionable:
|
21
|
+
|
22
|
+
class ListItem < ActiveRecord::Base
|
23
|
+
acts_as_positionable
|
24
|
+
end
|
25
|
+
|
26
|
+
Then we are set to use any of the familiar acts-as-list public methods on an instance of the model.
|
27
|
+
|
28
|
+
list_item.move_to_bottom
|
29
|
+
list_item.move_higher
|
30
|
+
|
31
|
+
The plain acts-as-positionable declaration although useful is somewhat lacking. To better suit your needs it accepts some useful options.
|
32
|
+
|
33
|
+
### Column
|
34
|
+
|
35
|
+
It is expected that the model possess a **position** column set to integer. If the position column is not available, say you are working with a legacy app, then you can define which column to use as the list's position with this option:
|
36
|
+
|
37
|
+
acts_as_positionable :column => 'my_different_attribute'
|
38
|
+
|
39
|
+
### Scope
|
40
|
+
|
41
|
+
Rarely we will want a list that cover all possible records of a model. Most of the times our lists makes sense only on behalf of another resource. Maybe our products are grouped under categories, or our list items are grouped under lists. Thus the scope option to group records onto their appropriate lists.
|
42
|
+
|
43
|
+
class List < ActiveRecord::Base
|
44
|
+
has_many :list_items, :order => 'position'
|
45
|
+
end
|
46
|
+
|
47
|
+
class ListItem < ActiveRecord::Base
|
48
|
+
belongs_to :list
|
49
|
+
acts_as_positionable :scope => :list_id
|
50
|
+
end
|
51
|
+
|
52
|
+
With this option set, list_items with the same list_id would be grouped under the same list, and list_items with a nil list_id would also me grouped under a list.
|
53
|
+
|
54
|
+
|
55
|
+
#### Record Updates
|
56
|
+
|
57
|
+
What if my record is updated and its scoped value changes? No need to worry. The record will be transparently removed from the previous list and inserted at the bottom of the new one.
|
58
|
+
|
59
|
+
#### Multiple Scopes
|
60
|
+
|
61
|
+
Our schema might be a little bit complex and out list items are grouped not only by list but also by status, a string that might be 'todo' or 'done'. The only difference here would be to declare the scopes as an array, in which case the scope option accepts as many scopes as we want, and there is no need for them to be an integer.
|
62
|
+
|
63
|
+
acts_as_positionable :scope => [:list_id, :status]
|
64
|
+
|
65
|
+
### Conditions
|
66
|
+
|
67
|
+
Some times we just want some specific records to be grouped under a list and this restriction doesn't maps well to the scoping concept. Say that we want to group in a list all our list_items that are flagged as _todo_ and it doesn't makes sense to us to have a list of our _done_ items.
|
68
|
+
|
69
|
+
acts_as_positionable :conditions => {:status => 'todo'}
|
70
|
+
|
71
|
+
With this declaration only items with _todo_ status would be grouped on lists, all other items would have their list position marked as *nil*.
|
72
|
+
|
73
|
+
The positionable conditions option accepts most of the activerecord find conditions syntax, thus the previous example could also be written as:
|
74
|
+
|
75
|
+
acts_as_positionable :conditions => ["status = ?", 'todo']
|
76
|
+
acts_as_positionable :conditions => "status = 'todo'"
|
77
|
+
|
78
|
+
#### Record Updates
|
79
|
+
|
80
|
+
What if my record is updated and it no longer meets the conditions set for the list? As those records that don't meet conditions don't belong in any list, if a record no longer meets the list's conditions after an update it will be transparently removed from the list. The opposite holds true for a record that doesn't meet the conditions before an update but does after the update.
|
81
|
+
|
82
|
+
### List Name
|
83
|
+
|
84
|
+
We might want to set the same record on two separate lists. To accomplish that all we need are two columns available to store the lists' position, and two declarations of acts-as-positionable.
|
85
|
+
|
86
|
+
acts_as_positionable
|
87
|
+
acts_as_positionable :list_name => :other, :column => 'other_position'
|
88
|
+
|
89
|
+
The first declaration will set the list named **:default**, using the **:position** column. With these declarations in place we can now use the positionable api in a different way:
|
90
|
+
|
91
|
+
list_item.move_up(:other) # This would only update the :other list
|
92
|
+
list_item.move_to_bottom(:default) # This would only update the :default list
|
93
|
+
|
94
|
+
When dealing with only one list, there is no need to pass the list name as the :default is assumed.
|
95
|
+
|
96
|
+
## Positionable Features
|
97
|
+
|
98
|
+
### Controller and Helper
|
99
|
+
|
100
|
+
In the best case scenario you won't need to write a single line of controller or helper code. There is no need to create routes whatsoever as the available route will take care of all the cases. All positionable controller actions are available under:
|
101
|
+
|
102
|
+
:resource/:id/:list_name/position/:action
|
103
|
+
|
104
|
+
But most of the time you won't need to address this route and use the available helpers for the move and insert methods:
|
105
|
+
|
106
|
+
move_up(record) # Would output record_class/record_id/default/position/move_up
|
107
|
+
|
108
|
+
You might not even need to touch this helper as there is even a higher level helper to address the common links or buttons for the move actions:
|
109
|
+
|
110
|
+
positionable_links(record)
|
111
|
+
positionable_buttons(record)
|
112
|
+
|
113
|
+
Both these helpers would output links/buttons for the move method if the record is on the list, or the insert methods otherwise.
|
114
|
+
|
115
|
+
### ActiveRecord Named Scopes
|
116
|
+
|
117
|
+
A positionable model has two activerecord named scopes for easy manipulation of the lists. They order the record either by ascending or by descending list position.
|
118
|
+
|
119
|
+
ListItem.ascending
|
120
|
+
ListItem.descending(:other)
|
121
|
+
|
122
|
+
They can be used with associations too:
|
123
|
+
|
124
|
+
list.list_items.ascending
|
125
|
+
list.list_item.descending
|
126
|
+
|
127
|
+
Another named scope is added but it only makes sense using if we set the conditions option on the acts-as-positionable declaration:
|
128
|
+
|
129
|
+
ListItem.conditions(:other)
|
130
|
+
|
131
|
+
The effect would be to narrow the records to only those that meet the conditions.
|
132
|
+
|
133
|
+
### Methods
|
134
|
+
|
135
|
+
List of the methods added with the acts-as-positionable declaration. All of them accept an optional list_name param.
|
136
|
+
|
137
|
+
# Insert the item at the given position (defaults to the top position)
|
138
|
+
insert_at(position)
|
139
|
+
|
140
|
+
insert_at_top
|
141
|
+
|
142
|
+
insert_at_bottom
|
143
|
+
|
144
|
+
# Swap positions with the next lower item, if one exists.
|
145
|
+
move_lower
|
146
|
+
move_down
|
147
|
+
|
148
|
+
# Swap positions with the next higher item, if one exists.
|
149
|
+
move_higher
|
150
|
+
move_up
|
151
|
+
|
152
|
+
# Move to the bottom of the list.
|
153
|
+
move_to_bottom
|
154
|
+
|
155
|
+
# Move to the top of the list.
|
156
|
+
move_to_top
|
157
|
+
|
158
|
+
# True if the record is the first in the list.
|
159
|
+
first?
|
160
|
+
|
161
|
+
# True if the record is the last in the list.
|
162
|
+
last?
|
163
|
+
|
164
|
+
# Returns the next higher item in the list.
|
165
|
+
higher_item
|
166
|
+
previous_item
|
167
|
+
|
168
|
+
# Returns the next lower item in the list.
|
169
|
+
lower_item
|
170
|
+
next_item
|
171
|
+
|
172
|
+
# True if the record is in the list.
|
173
|
+
in_list?
|
174
|
+
|
175
|
+
# Returns the record list position for the list.
|
176
|
+
list_position
|
177
|
+
|
178
|
+
# Removes the record from the list and shift other items accordingly.
|
179
|
+
remove_from_list
|
180
|
+
|
181
|
+
Credits
|
182
|
+
-------
|
183
|
+
|
184
|
+
Positionable is a mix of the Rails Core [acts-as-list][] plugin with the gap aware [fork][] by Ryan Bates and the features from [sortable][] by Sean Huber
|
185
|
+
|
186
|
+
Author
|
187
|
+
------
|
188
|
+
|
189
|
+
Felipe Doria
|
190
|
+
|
191
|
+
License
|
192
|
+
-------
|
193
|
+
|
194
|
+
Positionable is available under the MIT license.
|
195
|
+
|
196
|
+
|
197
|
+
[acts-as-list]: http://github.com/rails/acts_as_list/tree
|
198
|
+
[sortable]: http://github.com/shuber/sortable/tree
|
199
|
+
[fork]: http://github.com/ryanb/acts-as-list/tree
|
200
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "positionable"
|
8
|
+
gem.summary = %Q{acts-as-list extension stretched to controllers and helpers}
|
9
|
+
gem.email = "felipe.doria@gmail.com"
|
10
|
+
gem.homepage = "http://github.com/aguids/positionable"
|
11
|
+
gem.authors = ["Felipe Doria"]
|
12
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
13
|
+
end
|
14
|
+
|
15
|
+
rescue LoadError
|
16
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'rake/testtask'
|
20
|
+
Rake::TestTask.new(:test) do |test|
|
21
|
+
test.libs << 'lib' << 'test'
|
22
|
+
test.pattern = 'test/**/*_test.rb'
|
23
|
+
test.verbose = true
|
24
|
+
end
|
25
|
+
|
26
|
+
begin
|
27
|
+
require 'rcov/rcovtask'
|
28
|
+
Rcov::RcovTask.new do |test|
|
29
|
+
test.libs << 'test'
|
30
|
+
test.pattern = 'test/**/*_test.rb'
|
31
|
+
test.verbose = true
|
32
|
+
end
|
33
|
+
rescue LoadError
|
34
|
+
task :rcov do
|
35
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
task :default => :test
|
41
|
+
|
42
|
+
require 'rake/rdoctask'
|
43
|
+
Rake::RDocTask.new do |rdoc|
|
44
|
+
if File.exist?('VERSION.yml')
|
45
|
+
config = YAML.load(File.read('VERSION.yml'))
|
46
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
47
|
+
else
|
48
|
+
version = ""
|
49
|
+
end
|
50
|
+
|
51
|
+
rdoc.rdoc_dir = 'rdoc'
|
52
|
+
rdoc.title = "acts-as-positionable #{version}"
|
53
|
+
rdoc.rdoc_files.include('README*')
|
54
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
55
|
+
end
|
56
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.1
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class PositionableController < ApplicationController
|
2
|
+
before_filter :find_resource
|
3
|
+
|
4
|
+
public_api = %w( move_lower move_down move_higher move_up move_to_bottom
|
5
|
+
insert_at_bottom move_to_top insert_at_top remove_from_list ).freeze
|
6
|
+
|
7
|
+
public_api.each do |action|
|
8
|
+
define_method(action) do
|
9
|
+
@resource.send(action, params[:list].to_sym)
|
10
|
+
redirect_accordingly
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def insert_at
|
15
|
+
@resource.insert_at(params[:position].to_i, params[:list].to_sym)
|
16
|
+
redirect_accordingly
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
def find_resource
|
21
|
+
klass = params[:resource].camelize.constantize
|
22
|
+
@resource = klass.find(params[:id])
|
23
|
+
end
|
24
|
+
|
25
|
+
def redirect_accordingly
|
26
|
+
begin
|
27
|
+
redirect_to :back
|
28
|
+
rescue ActionController::RedirectBackError
|
29
|
+
redirect_to '/'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module PositionableHelper
|
2
|
+
|
3
|
+
%w( move_lower move_down move_higher move_up move_to_bottom insert_at_bottom
|
4
|
+
move_to_top insert_at_top insert_at remove_from_list).each do |action|
|
5
|
+
path_method = %Q(
|
6
|
+
def #{action}_path(resource, list = :default, options={})
|
7
|
+
url_for({:controller => 'positionable', :action => '#{action}',
|
8
|
+
:resource => resource.class.to_s.underscore, :id => resource.id, :list => list}.merge(options))
|
9
|
+
end
|
10
|
+
)
|
11
|
+
url_method = %Q(
|
12
|
+
def #{action}_url(resource, list = :default, options={})
|
13
|
+
#{action}_path(resource, list, options.merge({:only_path => false}))
|
14
|
+
end
|
15
|
+
)
|
16
|
+
class_eval(path_method)
|
17
|
+
class_eval(url_method)
|
18
|
+
end
|
19
|
+
|
20
|
+
def positionable_links(resource, list = :default)
|
21
|
+
resource.in_list?(list) ? positionable_move(resource,list,:link) : positionable_insert(resource,list,:link)
|
22
|
+
end
|
23
|
+
|
24
|
+
def positionable_buttons(resource, list = :default)
|
25
|
+
resource.in_list?(list) ? positionable_move(resource,list,:button) : positionable_insert(resource,list,:button)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def positionable_move(resource, list, interface)
|
30
|
+
tag = "<ul class=\"positionable_#{interface}s\">"
|
31
|
+
if resource.first?(list)
|
32
|
+
tag << send("positionable_disabled_#{interface}", 'Move to top')
|
33
|
+
tag << send("positionable_disabled_#{interface}", 'Move up')
|
34
|
+
else
|
35
|
+
tag << send("positionable_working_#{interface}", 'move_to_top', resource, list)
|
36
|
+
tag << send("positionable_working_#{interface}", 'move_up', resource, list)
|
37
|
+
end
|
38
|
+
if resource.last?(list)
|
39
|
+
tag << send("positionable_disabled_#{interface}", 'Move down')
|
40
|
+
tag << send("positionable_disabled_#{interface}", 'Move to bottom')
|
41
|
+
else
|
42
|
+
tag << send("positionable_working_#{interface}", 'move_down', resource, list)
|
43
|
+
tag << send("positionable_working_#{interface}", 'move_to_bottom', resource, list)
|
44
|
+
end
|
45
|
+
tag << '</ul>'
|
46
|
+
end
|
47
|
+
|
48
|
+
def positionable_insert(resource, list, interface)
|
49
|
+
tag = "<ul class=\"positionable_#{interface}s\">"
|
50
|
+
tag << send("positionable_working_#{interface}", 'insert_at_top', resource, list)
|
51
|
+
tag << send("positionable_working_#{interface}", 'insert_at_bottom', resource, list)
|
52
|
+
tag << '</ul>'
|
53
|
+
end
|
54
|
+
|
55
|
+
def positionable_disabled_link(message)
|
56
|
+
"<li class=\"positionable_disabled\">#{message}</li>"
|
57
|
+
end
|
58
|
+
|
59
|
+
def positionable_working_link(action, resource, list)
|
60
|
+
'<li>' << link_to(action.titleize, send("#{action}_path",resource,list), :method => :put) << '</li>'
|
61
|
+
end
|
62
|
+
|
63
|
+
def positionable_disabled_button(message)
|
64
|
+
"<li>" << button_to(message, '/', :disabled => true) << "</li>"
|
65
|
+
end
|
66
|
+
|
67
|
+
def positionable_working_button(action, resource, list)
|
68
|
+
"<li>" << button_to(action.titleize, send("#{action}_path",resource,list), :method => :put) << "</li>"
|
69
|
+
end
|
70
|
+
end
|
data/config/routes.rb
ADDED
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'positionable'
|