aguids-positionable 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. data/.document +5 -0
  2. data/.gitignore +11 -0
  3. data/LICENSE +20 -0
  4. data/README.markdown +200 -0
  5. data/Rakefile +56 -0
  6. data/VERSION +1 -0
  7. data/app/controllers/positionable_controller.rb +32 -0
  8. data/app/helpers/positionable_helper.rb +70 -0
  9. data/config/routes.rb +3 -0
  10. data/init.rb +1 -0
  11. data/lib/positionable.rb +362 -0
  12. data/positionable.gemspec +154 -0
  13. data/test/rails_root/Rakefile +10 -0
  14. data/test/rails_root/app/controllers/application_controller.rb +10 -0
  15. data/test/rails_root/app/helpers/application_helper.rb +3 -0
  16. data/test/rails_root/app/models/conditional_item.rb +3 -0
  17. data/test/rails_root/app/models/different_position_column_item.rb +3 -0
  18. data/test/rails_root/app/models/hell_on_earth_item.rb +5 -0
  19. data/test/rails_root/app/models/multiple_list_item.rb +5 -0
  20. data/test/rails_root/app/models/multiple_scoped_item.rb +3 -0
  21. data/test/rails_root/app/models/scoped_item.rb +3 -0
  22. data/test/rails_root/app/models/simple_item.rb +4 -0
  23. data/test/rails_root/config/boot.rb +110 -0
  24. data/test/rails_root/config/database.yml +22 -0
  25. data/test/rails_root/config/environment.rb +14 -0
  26. data/test/rails_root/config/environments/development.rb +17 -0
  27. data/test/rails_root/config/environments/production.rb +28 -0
  28. data/test/rails_root/config/environments/test.rb +28 -0
  29. data/test/rails_root/config/initializers/backtrace_silencers.rb +7 -0
  30. data/test/rails_root/config/initializers/inflections.rb +10 -0
  31. data/test/rails_root/config/initializers/mime_types.rb +5 -0
  32. data/test/rails_root/config/initializers/new_rails_defaults.rb +19 -0
  33. data/test/rails_root/config/initializers/session_store.rb +15 -0
  34. data/test/rails_root/config/locales/en.yml +5 -0
  35. data/test/rails_root/config/routes.rb +45 -0
  36. data/test/rails_root/db/development.sqlite3 +0 -0
  37. data/test/rails_root/db/migrate/20090702170207_create_simple_items.rb +13 -0
  38. data/test/rails_root/db/migrate/20090703071830_create_scoped_items.rb +14 -0
  39. data/test/rails_root/db/migrate/20090706180046_create_different_position_column_items.rb +13 -0
  40. data/test/rails_root/db/migrate/20090706200318_create_conditional_items.rb +14 -0
  41. data/test/rails_root/db/migrate/20090708045618_create_multiple_scoped_items.rb +15 -0
  42. data/test/rails_root/db/migrate/20090708174947_create_multiple_list_items.rb +15 -0
  43. data/test/rails_root/db/migrate/20090708183550_create_hell_on_earth_items.rb +19 -0
  44. data/test/rails_root/db/schema.rb +68 -0
  45. data/test/rails_root/script/about +4 -0
  46. data/test/rails_root/script/console +3 -0
  47. data/test/rails_root/script/dbconsole +3 -0
  48. data/test/rails_root/script/destroy +3 -0
  49. data/test/rails_root/script/generate +3 -0
  50. data/test/rails_root/script/performance/benchmarker +3 -0
  51. data/test/rails_root/script/performance/profiler +3 -0
  52. data/test/rails_root/script/plugin +3 -0
  53. data/test/rails_root/script/runner +3 -0
  54. data/test/rails_root/script/server +3 -0
  55. data/test/rails_root/test/conditional_tests.rb +28 -0
  56. data/test/rails_root/test/functional/positionable_controller_test.rb +50 -0
  57. data/test/rails_root/test/performance/browsing_test.rb +9 -0
  58. data/test/rails_root/test/scoped_tests.rb +53 -0
  59. data/test/rails_root/test/simple_gap_tests.rb +17 -0
  60. data/test/rails_root/test/simple_gapless_tests.rb +52 -0
  61. data/test/rails_root/test/simple_tests.rb +185 -0
  62. data/test/rails_root/test/test_helper.rb +37 -0
  63. data/test/rails_root/test/unit/conditional_item_test.rb +36 -0
  64. data/test/rails_root/test/unit/different_position_column_item_test.rb +21 -0
  65. data/test/rails_root/test/unit/eval_support_test.rb +62 -0
  66. data/test/rails_root/test/unit/hell_on_earth_item_test.rb +71 -0
  67. data/test/rails_root/test/unit/helpers/positionable_helper_test.rb +69 -0
  68. data/test/rails_root/test/unit/multiple_list_item_test.rb +60 -0
  69. data/test/rails_root/test/unit/multiple_scoped_item_test.rb +34 -0
  70. data/test/rails_root/test/unit/scoped_item_test.rb +22 -0
  71. data/test/rails_root/test/unit/scoped_item_with_gaps_test.rb +25 -0
  72. data/test/rails_root/test/unit/simple_item_test.rb +21 -0
  73. data/test/rails_root/test/unit/simple_item_with_gaps_test.rb +24 -0
  74. metadata +172 -0
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+
7
+ # Test db file
8
+ test/rails_root/db/test.sqlite3
9
+
10
+ # Test log files
11
+ test/rails_root/log/*.log
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
@@ -0,0 +1,3 @@
1
+ ActionController::Routing::Routes.draw do |map|
2
+ map.connect ':resource/:id/:list/position/:action', :controller => 'positionable', :conditions => {:method => :put}
3
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'positionable'