has_features 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/.gitignore +1 -0
- data/README +18 -0
- data/Rakefile +12 -0
- data/has_features.gemspec +31 -0
- data/init.rb +2 -0
- data/lib/has_features.rb +2 -0
- data/lib/has_features/active_record/has/features.rb +280 -0
- data/lib/has_features/version.rb +7 -0
- data/test/list_test.rb +523 -0
- data/test/schema.rb +59 -0
- metadata +103 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
test/test.db
|
data/README
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
HasFeatures
|
2
|
+
==========
|
3
|
+
|
4
|
+
This is a minor variation on acts_as_list, with a terrible name. If the position is nil then the object is not featured.
|
5
|
+
|
6
|
+
|
7
|
+
Example
|
8
|
+
=======
|
9
|
+
|
10
|
+
class Authors < ActiveRecord::Base
|
11
|
+
has_features
|
12
|
+
end
|
13
|
+
|
14
|
+
Authors.featured
|
15
|
+
|
16
|
+
|
17
|
+
blatantly borrowed from acts_as_list (https://github.com/swanandp/acts_as_list) which is (c) David Heinemeier Hansson
|
18
|
+
Copyright (c) 2012 Seth Faxon, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
desc 'Default: run has_features unit tests.'
|
5
|
+
task :default => :test
|
6
|
+
|
7
|
+
desc 'Test the has_features gem.'
|
8
|
+
Rake::TestTask.new(:test) do |t|
|
9
|
+
t.libs << 'lib'
|
10
|
+
t.pattern = 'test/**/*_test.rb'
|
11
|
+
t.verbose = true
|
12
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path('../lib', __FILE__)
|
3
|
+
require 'has_features/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
|
7
|
+
# Description Meta...
|
8
|
+
s.name = 'has_features'
|
9
|
+
s.version = ActiveRecord::Has::Features::VERSION
|
10
|
+
s.platform = Gem::Platform::RUBY
|
11
|
+
s.authors = ['Seth Faxon']
|
12
|
+
s.email = ['seth.faxon@gmail.com']
|
13
|
+
s.homepage = 'http://github.com/sfaxon/has_features'
|
14
|
+
s.summary = %q{A gem allowing a active_record model to have an ordered list of featured items.}
|
15
|
+
s.description = %q{Based on acts_as_list, but allows nil elements to be not a part of the featuerd list.}
|
16
|
+
s.rubyforge_project = 'has_features'
|
17
|
+
|
18
|
+
|
19
|
+
# Load Paths...
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
23
|
+
s.require_paths = ['lib']
|
24
|
+
|
25
|
+
|
26
|
+
# Dependencies (installed via 'bundle install')...
|
27
|
+
s.add_development_dependency("bundler", ["~> 1.0.0"])
|
28
|
+
s.add_development_dependency("activerecord", [">= 3.0.0"])
|
29
|
+
s.add_development_dependency("rdoc")
|
30
|
+
s.add_development_dependency("sqlite3")
|
31
|
+
end
|
data/init.rb
ADDED
data/lib/has_features.rb
ADDED
@@ -0,0 +1,280 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Has #:nodoc:
|
3
|
+
module Features #:nodoc:
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
# This +has+ extension provides the capabilities for sorting and reordering a number of objects in a list.
|
9
|
+
# The class that has this specified needs to have a +featured_position+ column defined as an integer on
|
10
|
+
# the mapped database table.
|
11
|
+
#
|
12
|
+
# Todo list example:
|
13
|
+
#
|
14
|
+
# class TodoList < ActiveRecord::Base
|
15
|
+
# has_many :todo_items, :order => "featured_position"
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# class Author < ActiveRecord::Base
|
19
|
+
# belongs_to :todo_list
|
20
|
+
# has_features :scope => :organization
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# author.featured = true
|
24
|
+
module ClassMethods
|
25
|
+
# Configuration options are:
|
26
|
+
#
|
27
|
+
# * +column+ - specifies the column name to use for keeping the featured_position integer (default: +featured_position+)
|
28
|
+
# * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
|
29
|
+
# (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
|
30
|
+
# to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
|
31
|
+
# Example: <tt>has_features :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
|
32
|
+
def has_features(options = {})
|
33
|
+
configuration = { :column => "featured_position", :scope => "1 = 1" }
|
34
|
+
configuration.update(options) if options.is_a?(Hash)
|
35
|
+
|
36
|
+
configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
|
37
|
+
|
38
|
+
if configuration[:scope].is_a?(Symbol)
|
39
|
+
scope_condition_method = %(
|
40
|
+
def scope_condition
|
41
|
+
self.class.send(:sanitize_sql_hash_for_conditions, { :#{configuration[:scope].to_s} => send(:#{configuration[:scope].to_s}) })
|
42
|
+
end
|
43
|
+
)
|
44
|
+
elsif configuration[:scope].is_a?(Array)
|
45
|
+
scope_condition_method = %(
|
46
|
+
def scope_condition
|
47
|
+
attrs = %w(#{configuration[:scope].join(" ")}).inject({}) do |memo,column|
|
48
|
+
memo[column.intern] = send(column.intern); memo
|
49
|
+
end
|
50
|
+
self.class.send(:sanitize_sql_hash_for_conditions, attrs)
|
51
|
+
end
|
52
|
+
)
|
53
|
+
else
|
54
|
+
scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
|
55
|
+
end
|
56
|
+
|
57
|
+
class_eval <<-EOV
|
58
|
+
include ActiveRecord::Has::Features::InstanceMethods
|
59
|
+
|
60
|
+
def featured_position_column
|
61
|
+
'#{configuration[:column]}'
|
62
|
+
end
|
63
|
+
|
64
|
+
def has_features_class
|
65
|
+
::#{self.name}
|
66
|
+
end
|
67
|
+
|
68
|
+
scope :featured, order("#{configuration[:column]} asc").where("#{configuration[:column]} is not null")
|
69
|
+
|
70
|
+
#{scope_condition_method}
|
71
|
+
|
72
|
+
before_destroy :decrement_positions_on_lower_featured_items
|
73
|
+
EOV
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# All the methods available to a record that has had <tt>has_features</tt> specified. Each method works
|
78
|
+
# by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
|
79
|
+
# lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
|
80
|
+
# the first in the list of all chapters.
|
81
|
+
module InstanceMethods
|
82
|
+
# Insert the item at the given position (defaults to the top position of 1).
|
83
|
+
def feature_at(position = 1)
|
84
|
+
feature_at_position(position)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Swap positions with the next lower item, if one exists.
|
88
|
+
def move_lower
|
89
|
+
return unless lower_item
|
90
|
+
|
91
|
+
has_features_class.transaction do
|
92
|
+
lower_item.decrement_position
|
93
|
+
increment_position
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Swap positions with the next higher item, if one exists.
|
98
|
+
def move_higher
|
99
|
+
return unless higher_item
|
100
|
+
|
101
|
+
has_features_class.transaction do
|
102
|
+
higher_item.increment_position
|
103
|
+
decrement_position
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Move to the bottom of the list. If the item is already in the list, the items below it have their
|
108
|
+
# position adjusted accordingly.
|
109
|
+
def move_to_bottom
|
110
|
+
return unless in_list?
|
111
|
+
has_features_class.transaction do
|
112
|
+
decrement_positions_on_lower_featured_items
|
113
|
+
assume_bottom_position
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Move to the top of the list. If the item is already in the list, the items above it have their
|
118
|
+
# position adjusted accordingly.
|
119
|
+
def move_to_top
|
120
|
+
return unless in_list?
|
121
|
+
has_features_class.transaction do
|
122
|
+
increment_positions_on_higher_items
|
123
|
+
assume_top_position
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Removes the item from the list.
|
128
|
+
def unfeature
|
129
|
+
if in_list?
|
130
|
+
decrement_positions_on_lower_featured_items
|
131
|
+
update_attribute featured_position_column, nil
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Increase the position of this item without adjusting the rest of the list.
|
136
|
+
def increment_position
|
137
|
+
return unless in_list?
|
138
|
+
update_attribute featured_position_column, self.send(featured_position_column).to_i + 1
|
139
|
+
end
|
140
|
+
|
141
|
+
# Decrease the position of this item without adjusting the rest of the list.
|
142
|
+
def decrement_position
|
143
|
+
return unless in_list?
|
144
|
+
update_attribute featured_position_column, self.send(featured_position_column).to_i - 1
|
145
|
+
end
|
146
|
+
|
147
|
+
# Return +true+ if this object is the first in the list.
|
148
|
+
def first?
|
149
|
+
return false unless in_list?
|
150
|
+
self.send(featured_position_column) == 1
|
151
|
+
end
|
152
|
+
|
153
|
+
# Return +true+ if this object is the last in the list.
|
154
|
+
def last?
|
155
|
+
return false unless in_list?
|
156
|
+
self.send(featured_position_column) == bottom_position_in_list
|
157
|
+
end
|
158
|
+
|
159
|
+
# Return the next higher item in the list.
|
160
|
+
def higher_item
|
161
|
+
return nil unless in_list?
|
162
|
+
has_features_class.find(:first, :conditions =>
|
163
|
+
"#{scope_condition} AND #{featured_position_column} = #{(send(featured_position_column).to_i - 1).to_s}"
|
164
|
+
)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Return the next lower item in the list.
|
168
|
+
def lower_item
|
169
|
+
return nil unless in_list?
|
170
|
+
has_features_class.find(:first, :conditions =>
|
171
|
+
"#{scope_condition} AND #{featured_position_column} = #{(send(featured_position_column).to_i + 1).to_s}"
|
172
|
+
)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Test if this record is in a list
|
176
|
+
def in_list?
|
177
|
+
!send(featured_position_column).nil?
|
178
|
+
end
|
179
|
+
|
180
|
+
def featured=(val)
|
181
|
+
if true == val || "true" == val
|
182
|
+
add_to_list_bottom
|
183
|
+
save
|
184
|
+
else
|
185
|
+
unfeature
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def featured?
|
190
|
+
!self.send(featured_position_column).nil?
|
191
|
+
end
|
192
|
+
alias_method :featured, :featured?
|
193
|
+
|
194
|
+
def feature
|
195
|
+
add_to_list_bottom
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
def add_to_list_top
|
200
|
+
increment_positions_on_all_items
|
201
|
+
end
|
202
|
+
|
203
|
+
def add_to_list_bottom
|
204
|
+
self[featured_position_column] = bottom_position_in_list.to_i + 1
|
205
|
+
# update_attribute(featured_position_column, bottom_position_in_list.to_i + 1)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Overwrite this method to define the scope of the list changes
|
209
|
+
def scope_condition() "1" end
|
210
|
+
|
211
|
+
# Returns the bottom position number in the list.
|
212
|
+
# bottom_position_in_list # => 2
|
213
|
+
def bottom_position_in_list(except = nil)
|
214
|
+
item = bottom_item(except)
|
215
|
+
item ? item.send(featured_position_column) : 0
|
216
|
+
end
|
217
|
+
|
218
|
+
# Returns the bottom item
|
219
|
+
def bottom_item(except = nil)
|
220
|
+
conditions = scope_condition
|
221
|
+
conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
|
222
|
+
has_features_class.find(:first, :conditions => conditions, :order => "#{featured_position_column} DESC")
|
223
|
+
end
|
224
|
+
|
225
|
+
# Forces item to assume the bottom position in the list.
|
226
|
+
def assume_bottom_position
|
227
|
+
update_attribute(featured_position_column, bottom_position_in_list(self).to_i + 1)
|
228
|
+
end
|
229
|
+
|
230
|
+
# Forces item to assume the top position in the list.
|
231
|
+
def assume_top_position
|
232
|
+
update_attribute(featured_position_column, 1)
|
233
|
+
end
|
234
|
+
|
235
|
+
# This has the effect of moving all the higher items up one.
|
236
|
+
def decrement_positions_on_higher_items(position)
|
237
|
+
has_features_class.update_all(
|
238
|
+
"#{featured_position_column} = (#{featured_position_column} - 1)", "#{scope_condition} AND #{featured_position_column} <= #{position}"
|
239
|
+
)
|
240
|
+
end
|
241
|
+
|
242
|
+
# This has the effect of moving all the lower items up one.
|
243
|
+
def decrement_positions_on_lower_featured_items
|
244
|
+
return unless in_list?
|
245
|
+
has_features_class.update_all(
|
246
|
+
"#{featured_position_column} = (#{featured_position_column} - 1)", "#{scope_condition} AND #{featured_position_column} > #{send(featured_position_column).to_i}"
|
247
|
+
)
|
248
|
+
end
|
249
|
+
|
250
|
+
# This has the effect of moving all the higher items down one.
|
251
|
+
def increment_positions_on_higher_items
|
252
|
+
return unless in_list?
|
253
|
+
has_features_class.update_all(
|
254
|
+
"#{featured_position_column} = (#{featured_position_column} + 1)", "#{scope_condition} AND #{featured_position_column} < #{send(featured_position_column).to_i}"
|
255
|
+
)
|
256
|
+
end
|
257
|
+
|
258
|
+
# This has the effect of moving all the lower items down one.
|
259
|
+
def increment_positions_on_lower_items(position)
|
260
|
+
has_features_class.update_all(
|
261
|
+
"#{featured_position_column} = (#{featured_position_column} + 1)", "#{scope_condition} AND #{featured_position_column} >= #{position}"
|
262
|
+
)
|
263
|
+
end
|
264
|
+
|
265
|
+
# Increments position (<tt>featured_position_column</tt>) of all items in the list.
|
266
|
+
def increment_positions_on_all_items
|
267
|
+
has_features_class.update_all(
|
268
|
+
"#{featured_position_column} = (#{featured_position_column} + 1)", "#{scope_condition}"
|
269
|
+
)
|
270
|
+
end
|
271
|
+
|
272
|
+
def feature_at_position(position)
|
273
|
+
unfeature
|
274
|
+
increment_positions_on_lower_items(position)
|
275
|
+
self.update_attribute(featured_position_column, position)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
data/test/list_test.rb
ADDED
@@ -0,0 +1,523 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
gem 'activerecord', '>= 3.0.0'
|
5
|
+
require 'active_record'
|
6
|
+
require 'ruby-debug'
|
7
|
+
|
8
|
+
require File.join(File.dirname(__FILE__), '../lib/has_features')
|
9
|
+
require File.join(File.dirname(__FILE__), 'schema')
|
10
|
+
|
11
|
+
class FeaturedTest < Test::Unit::TestCase
|
12
|
+
|
13
|
+
def setup
|
14
|
+
setup_db
|
15
|
+
(1..4).each { |counter| FeaturedMixin.create! :pos => counter, :parent_id => 5 }
|
16
|
+
end
|
17
|
+
|
18
|
+
def teardown
|
19
|
+
teardown_db
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_reordering
|
23
|
+
assert_equal [1, 2, 3, 4], FeaturedMixin.where(:parent_id => 5).order('pos').map(&:id)
|
24
|
+
|
25
|
+
FeaturedMixin.find(2).move_lower
|
26
|
+
assert_equal [1, 3, 2, 4], FeaturedMixin.where(:parent_id => 5).order('pos').map(&:id)
|
27
|
+
|
28
|
+
FeaturedMixin.find(2).move_higher
|
29
|
+
assert_equal [1, 2, 3, 4], FeaturedMixin.where(:parent_id => 5).order('pos').map(&:id)
|
30
|
+
|
31
|
+
FeaturedMixin.find(1).move_to_bottom
|
32
|
+
assert_equal [2, 3, 4, 1], FeaturedMixin.where(:parent_id => 5).order('pos').map(&:id)
|
33
|
+
|
34
|
+
FeaturedMixin.find(1).move_to_top
|
35
|
+
assert_equal [1, 2, 3, 4], FeaturedMixin.where(:parent_id => 5).order('pos').map(&:id)
|
36
|
+
|
37
|
+
FeaturedMixin.find(2).move_to_bottom
|
38
|
+
assert_equal [1, 3, 4, 2], FeaturedMixin.where(:parent_id => 5).order('pos').map(&:id)
|
39
|
+
|
40
|
+
FeaturedMixin.find(4).move_to_top
|
41
|
+
assert_equal [4, 1, 3, 2], FeaturedMixin.where(:parent_id => 5).order('pos').map(&:id)
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_move_to_bottom_with_next_to_last_item
|
45
|
+
assert_equal [1, 2, 3, 4], FeaturedMixin.where(:parent_id => 5).order('pos').map(&:id)
|
46
|
+
FeaturedMixin.find(3).move_to_bottom
|
47
|
+
assert_equal [1, 2, 4, 3], FeaturedMixin.where(:parent_id => 5).order('pos').map(&:id)
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_next_prev
|
51
|
+
assert_equal FeaturedMixin.find(2), FeaturedMixin.find(1).lower_item
|
52
|
+
assert_nil FeaturedMixin.find(1).higher_item
|
53
|
+
assert_equal FeaturedMixin.find(3), FeaturedMixin.find(4).higher_item
|
54
|
+
assert_nil FeaturedMixin.find(4).lower_item
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_injection
|
58
|
+
item = FeaturedMixin.new(:parent_id => 1)
|
59
|
+
assert_equal '"mixins"."parent_id" = 1', item.scope_condition
|
60
|
+
assert_equal "pos", item.featured_position_column
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_insert
|
64
|
+
new = FeaturedMixin.create(:parent_id => 20)
|
65
|
+
assert_equal nil, new.pos
|
66
|
+
assert !new.first?
|
67
|
+
assert !new.last?
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_featuring
|
71
|
+
new = FeaturedMixin.create(:parent_id => 20)
|
72
|
+
new.featured = true
|
73
|
+
assert_equal 1, new.pos
|
74
|
+
assert new.featured?
|
75
|
+
assert new.featured
|
76
|
+
assert new.first?
|
77
|
+
assert new.last?
|
78
|
+
|
79
|
+
new = FeaturedMixin.create(:parent_id => 20)
|
80
|
+
new.featured = true
|
81
|
+
assert_equal 2, new.pos
|
82
|
+
assert !new.first?
|
83
|
+
assert new.last?
|
84
|
+
|
85
|
+
new = FeaturedMixin.create(:parent_id => 20)
|
86
|
+
new.featured = true
|
87
|
+
assert_equal 3, new.pos
|
88
|
+
assert !new.first?
|
89
|
+
assert new.last?
|
90
|
+
|
91
|
+
new = FeaturedMixin.create(:parent_id => 0)
|
92
|
+
new.featured = true
|
93
|
+
assert_equal 1, new.pos
|
94
|
+
assert new.first?
|
95
|
+
assert new.last?
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_unfeaturing
|
99
|
+
new = FeaturedMixin.create(:parent_id => 20)
|
100
|
+
new.featured = true
|
101
|
+
assert_equal 1, new.pos
|
102
|
+
new.featured = false
|
103
|
+
assert_nil new.pos
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_feature_at
|
107
|
+
new = FeaturedMixin.create(:parent_id => 20)
|
108
|
+
new.featured = true
|
109
|
+
assert_equal 1, new.pos
|
110
|
+
|
111
|
+
new = FeaturedMixin.create(:parent_id => 20)
|
112
|
+
new.featured = true
|
113
|
+
assert_equal 2, new.pos
|
114
|
+
|
115
|
+
new = FeaturedMixin.create(:parent_id => 20)
|
116
|
+
new.featured = true
|
117
|
+
assert_equal 3, new.pos
|
118
|
+
|
119
|
+
new4 = FeaturedMixin.create(:parent_id => 20)
|
120
|
+
new4.featured = true
|
121
|
+
assert_equal 4, new4.pos
|
122
|
+
|
123
|
+
new4.feature_at(3)
|
124
|
+
assert_equal 3, new4.pos
|
125
|
+
|
126
|
+
new.reload
|
127
|
+
assert_equal 4, new.pos
|
128
|
+
|
129
|
+
new.feature_at(2)
|
130
|
+
assert_equal 2, new.pos
|
131
|
+
|
132
|
+
new4.reload
|
133
|
+
assert_equal 4, new4.pos
|
134
|
+
|
135
|
+
new5 = FeaturedMixin.create(:parent_id => 20)
|
136
|
+
new5.featured = true
|
137
|
+
assert_equal 5, new5.pos
|
138
|
+
|
139
|
+
new5.feature_at(1)
|
140
|
+
assert_equal 1, new5.pos
|
141
|
+
|
142
|
+
new4.reload
|
143
|
+
assert_equal 5, new4.pos
|
144
|
+
end
|
145
|
+
|
146
|
+
def test_delete_middle
|
147
|
+
assert_equal [1, 2, 3, 4], FeaturedMixin.where(:parent_id => 5).order('pos').map(&:id)
|
148
|
+
|
149
|
+
FeaturedMixin.find(2).destroy
|
150
|
+
|
151
|
+
assert_equal [1, 3, 4], FeaturedMixin.where(:parent_id => 5).order('pos').map(&:id)
|
152
|
+
|
153
|
+
assert_equal 1, FeaturedMixin.find(1).pos
|
154
|
+
assert_equal 2, FeaturedMixin.find(3).pos
|
155
|
+
assert_equal 3, FeaturedMixin.find(4).pos
|
156
|
+
|
157
|
+
FeaturedMixin.find(1).destroy
|
158
|
+
|
159
|
+
assert_equal [3, 4], FeaturedMixin.where(:parent_id => 5).order('pos').map(&:id)
|
160
|
+
|
161
|
+
assert_equal 1, FeaturedMixin.find(3).pos
|
162
|
+
assert_equal 2, FeaturedMixin.find(4).pos
|
163
|
+
end
|
164
|
+
|
165
|
+
def test_with_string_based_scope
|
166
|
+
new = FeaturedWithStringScopeMixin.create(:parent_id => 500)
|
167
|
+
new.featured = true
|
168
|
+
assert_equal 1, new.pos
|
169
|
+
assert new.first?
|
170
|
+
assert new.last?
|
171
|
+
end
|
172
|
+
|
173
|
+
def test_nil_scope
|
174
|
+
new1, new2, new3 = FeaturedMixin.create, FeaturedMixin.create, FeaturedMixin.create
|
175
|
+
new1.featured = true
|
176
|
+
new2.featured = true
|
177
|
+
new3.featured = true
|
178
|
+
new2.move_higher
|
179
|
+
assert_equal [new2, new1, new3], FeaturedMixin.where('parent_id IS NULL').order('pos')
|
180
|
+
end
|
181
|
+
|
182
|
+
def test_unfeature_should_then_fail_in_list?
|
183
|
+
assert_equal true, FeaturedMixin.find(1).in_list?
|
184
|
+
FeaturedMixin.find(1).unfeature
|
185
|
+
assert_equal false, FeaturedMixin.find(1).in_list?
|
186
|
+
end
|
187
|
+
|
188
|
+
def test_unfeature_should_set_position_to_nil
|
189
|
+
assert_equal [1, 2, 3, 4], FeaturedMixin.where(:parent_id => 5).order('pos').map(&:id)
|
190
|
+
|
191
|
+
FeaturedMixin.find(2).unfeature
|
192
|
+
|
193
|
+
assert_equal [2, 1, 3, 4], FeaturedMixin.where(:parent_id => 5).order('pos').map(&:id)
|
194
|
+
|
195
|
+
assert_equal 1, FeaturedMixin.find(1).pos
|
196
|
+
assert_equal nil, FeaturedMixin.find(2).pos
|
197
|
+
assert_equal 2, FeaturedMixin.find(3).pos
|
198
|
+
assert_equal 3, FeaturedMixin.find(4).pos
|
199
|
+
end
|
200
|
+
|
201
|
+
def test_remove_before_destroy_does_not_shift_lower_items_twice
|
202
|
+
assert_equal [1, 2, 3, 4], FeaturedMixin.where(:parent_id => 5).order('pos').map(&:id)
|
203
|
+
|
204
|
+
FeaturedMixin.find(2).unfeature
|
205
|
+
FeaturedMixin.find(2).destroy
|
206
|
+
|
207
|
+
assert_equal [1, 3, 4], FeaturedMixin.where(:parent_id => 5).order('pos').map(&:id)
|
208
|
+
|
209
|
+
assert_equal 1, FeaturedMixin.find(1).pos
|
210
|
+
assert_equal 2, FeaturedMixin.find(3).pos
|
211
|
+
assert_equal 3, FeaturedMixin.find(4).pos
|
212
|
+
end
|
213
|
+
|
214
|
+
def test_before_destroy_callbacks_do_not_update_position_to_nil_before_deleting_the_record
|
215
|
+
assert_equal [1, 2, 3, 4], FeaturedMixin.where(:parent_id => 5).order('pos').map(&:id)
|
216
|
+
|
217
|
+
# We need to trigger all the before_destroy callbacks without actually
|
218
|
+
# destroying the record so we can see the affect the callbacks have on
|
219
|
+
# the record.
|
220
|
+
list = FeaturedMixin.find(2)
|
221
|
+
if list.respond_to?(:run_callbacks)
|
222
|
+
list.run_callbacks(:destroy)
|
223
|
+
else
|
224
|
+
list.send(:callback, :before_destroy)
|
225
|
+
end
|
226
|
+
|
227
|
+
assert_equal [1, 2, 3, 4], FeaturedMixin.where(:parent_id => 5).order('pos').map(&:id)
|
228
|
+
|
229
|
+
assert_equal 1, FeaturedMixin.find(1).pos
|
230
|
+
assert_equal 2, FeaturedMixin.find(2).pos
|
231
|
+
assert_equal 2, FeaturedMixin.find(3).pos
|
232
|
+
assert_equal 3, FeaturedMixin.find(4).pos
|
233
|
+
end
|
234
|
+
|
235
|
+
end
|
236
|
+
|
237
|
+
class FeaturedSubTest < Test::Unit::TestCase
|
238
|
+
|
239
|
+
def setup
|
240
|
+
setup_db
|
241
|
+
(1..4).each { |i| ((i % 2 == 1) ? FeaturedMixinSub1 : FeaturedMixinSub2).create! :pos => i, :parent_id => 5000 }
|
242
|
+
end
|
243
|
+
|
244
|
+
def teardown
|
245
|
+
teardown_db
|
246
|
+
end
|
247
|
+
|
248
|
+
def test_reordering
|
249
|
+
assert_equal [1, 2, 3, 4], FeaturedMixin.where(:parent_id => 5000).order('pos').map(&:id)
|
250
|
+
|
251
|
+
FeaturedMixin.find(2).move_lower
|
252
|
+
assert_equal [1, 3, 2, 4], FeaturedMixin.where(:parent_id => 5000).order('pos').map(&:id)
|
253
|
+
|
254
|
+
FeaturedMixin.find(2).move_higher
|
255
|
+
assert_equal [1, 2, 3, 4], FeaturedMixin.where(:parent_id => 5000).order('pos').map(&:id)
|
256
|
+
|
257
|
+
FeaturedMixin.find(1).move_to_bottom
|
258
|
+
assert_equal [2, 3, 4, 1], FeaturedMixin.where(:parent_id => 5000).order('pos').map(&:id)
|
259
|
+
|
260
|
+
FeaturedMixin.find(1).move_to_top
|
261
|
+
assert_equal [1, 2, 3, 4], FeaturedMixin.where(:parent_id => 5000).order('pos').map(&:id)
|
262
|
+
|
263
|
+
FeaturedMixin.find(2).move_to_bottom
|
264
|
+
assert_equal [1, 3, 4, 2], FeaturedMixin.where(:parent_id => 5000).order('pos').map(&:id)
|
265
|
+
|
266
|
+
FeaturedMixin.find(4).move_to_top
|
267
|
+
assert_equal [4, 1, 3, 2], FeaturedMixin.where(:parent_id => 5000).order('pos').map(&:id)
|
268
|
+
end
|
269
|
+
|
270
|
+
def test_move_to_bottom_with_next_to_last_item
|
271
|
+
assert_equal [1, 2, 3, 4], FeaturedMixin.where(:parent_id => 5000).order('pos').map(&:id)
|
272
|
+
FeaturedMixin.find(3).move_to_bottom
|
273
|
+
assert_equal [1, 2, 4, 3], FeaturedMixin.where(:parent_id => 5000).order('pos').map(&:id)
|
274
|
+
end
|
275
|
+
|
276
|
+
def test_next_prev
|
277
|
+
assert_equal FeaturedMixin.find(2), FeaturedMixin.find(1).lower_item
|
278
|
+
assert_nil FeaturedMixin.find(1).higher_item
|
279
|
+
assert_equal FeaturedMixin.find(3), FeaturedMixin.find(4).higher_item
|
280
|
+
assert_nil FeaturedMixin.find(4).lower_item
|
281
|
+
end
|
282
|
+
|
283
|
+
def test_injection
|
284
|
+
item = FeaturedMixin.new("parent_id"=>1)
|
285
|
+
assert_equal '"mixins"."parent_id" = 1', item.scope_condition
|
286
|
+
assert_equal "pos", item.featured_position_column
|
287
|
+
end
|
288
|
+
|
289
|
+
def test_feature_at
|
290
|
+
new = FeaturedMixin.create("parent_id" => 20)
|
291
|
+
new.featured = true
|
292
|
+
assert_equal 1, new.pos
|
293
|
+
|
294
|
+
new = FeaturedMixinSub1.create("parent_id" => 20)
|
295
|
+
new.featured = true
|
296
|
+
assert_equal 2, new.pos
|
297
|
+
|
298
|
+
new = FeaturedMixinSub2.create("parent_id" => 20)
|
299
|
+
new.featured = true
|
300
|
+
assert_equal 3, new.pos
|
301
|
+
|
302
|
+
new4 = FeaturedMixin.create("parent_id" => 20)
|
303
|
+
new4.featured = true
|
304
|
+
assert_equal 4, new4.pos
|
305
|
+
|
306
|
+
new4.feature_at(3)
|
307
|
+
assert_equal 3, new4.pos
|
308
|
+
|
309
|
+
new.reload
|
310
|
+
assert_equal 4, new.pos
|
311
|
+
|
312
|
+
new.feature_at(2)
|
313
|
+
assert_equal 2, new.pos
|
314
|
+
|
315
|
+
new4.reload
|
316
|
+
assert_equal 4, new4.pos
|
317
|
+
|
318
|
+
new5 = FeaturedMixinSub1.create("parent_id" => 20)
|
319
|
+
new5.featured = true
|
320
|
+
assert_equal 5, new5.pos
|
321
|
+
|
322
|
+
new5.feature_at(1)
|
323
|
+
assert_equal 1, new5.pos
|
324
|
+
|
325
|
+
new4.reload
|
326
|
+
assert_equal 5, new4.pos
|
327
|
+
end
|
328
|
+
|
329
|
+
def test_delete_middle
|
330
|
+
assert_equal [1, 2, 3, 4], FeaturedMixin.where(:parent_id => 5000).order('pos').map(&:id)
|
331
|
+
|
332
|
+
FeaturedMixin.find(2).destroy
|
333
|
+
|
334
|
+
assert_equal [1, 3, 4], FeaturedMixin.where(:parent_id => 5000).order('pos').map(&:id)
|
335
|
+
|
336
|
+
assert_equal 1, FeaturedMixin.find(1).pos
|
337
|
+
assert_equal 2, FeaturedMixin.find(3).pos
|
338
|
+
assert_equal 3, FeaturedMixin.find(4).pos
|
339
|
+
|
340
|
+
FeaturedMixin.find(1).destroy
|
341
|
+
|
342
|
+
assert_equal [3, 4], FeaturedMixin.where(:parent_id => 5000).order('pos').map(&:id)
|
343
|
+
|
344
|
+
assert_equal 1, FeaturedMixin.find(3).pos
|
345
|
+
assert_equal 2, FeaturedMixin.find(4).pos
|
346
|
+
end
|
347
|
+
|
348
|
+
end
|
349
|
+
|
350
|
+
class ArrayScopeFeaturedTest < Test::Unit::TestCase
|
351
|
+
|
352
|
+
def setup
|
353
|
+
setup_db
|
354
|
+
(1..4).each { |counter| ArrayScopeFeaturedMixin.create! :pos => counter, :parent_id => 5, :parent_type => 'ParentClass' }
|
355
|
+
end
|
356
|
+
|
357
|
+
def teardown
|
358
|
+
teardown_db
|
359
|
+
end
|
360
|
+
|
361
|
+
def test_reordering
|
362
|
+
assert_equal [1, 2, 3, 4], ArrayScopeFeaturedMixin.where(:parent_id => 5, :parent_type => 'ParentClass').order('pos').map(&:id)
|
363
|
+
|
364
|
+
ArrayScopeFeaturedMixin.find(2).move_lower
|
365
|
+
assert_equal [1, 3, 2, 4], ArrayScopeFeaturedMixin.where(:parent_id => 5, :parent_type => 'ParentClass').order('pos').map(&:id)
|
366
|
+
|
367
|
+
ArrayScopeFeaturedMixin.find(2).move_higher
|
368
|
+
assert_equal [1, 2, 3, 4], ArrayScopeFeaturedMixin.where(:parent_id => 5, :parent_type => 'ParentClass').order('pos').map(&:id)
|
369
|
+
|
370
|
+
ArrayScopeFeaturedMixin.find(1).move_to_bottom
|
371
|
+
assert_equal [2, 3, 4, 1], ArrayScopeFeaturedMixin.where(:parent_id => 5, :parent_type => 'ParentClass').order('pos').map(&:id)
|
372
|
+
|
373
|
+
ArrayScopeFeaturedMixin.find(1).move_to_top
|
374
|
+
assert_equal [1, 2, 3, 4], ArrayScopeFeaturedMixin.where(:parent_id => 5, :parent_type => 'ParentClass').order('pos').map(&:id)
|
375
|
+
|
376
|
+
ArrayScopeFeaturedMixin.find(2).move_to_bottom
|
377
|
+
assert_equal [1, 3, 4, 2], ArrayScopeFeaturedMixin.where(:parent_id => 5, :parent_type => 'ParentClass').order('pos').map(&:id)
|
378
|
+
|
379
|
+
ArrayScopeFeaturedMixin.find(4).move_to_top
|
380
|
+
assert_equal [4, 1, 3, 2], ArrayScopeFeaturedMixin.where(:parent_id => 5, :parent_type => 'ParentClass').order('pos').map(&:id)
|
381
|
+
end
|
382
|
+
|
383
|
+
def test_move_to_bottom_with_next_to_last_item
|
384
|
+
assert_equal [1, 2, 3, 4], ArrayScopeFeaturedMixin.where(:parent_id => 5, :parent_type => 'ParentClass').order('pos').map(&:id)
|
385
|
+
ArrayScopeFeaturedMixin.find(3).move_to_bottom
|
386
|
+
assert_equal [1, 2, 4, 3], ArrayScopeFeaturedMixin.where(:parent_id => 5, :parent_type => 'ParentClass').order('pos').map(&:id)
|
387
|
+
end
|
388
|
+
|
389
|
+
def test_next_prev
|
390
|
+
assert_equal ArrayScopeFeaturedMixin.find(2), ArrayScopeFeaturedMixin.find(1).lower_item
|
391
|
+
assert_nil ArrayScopeFeaturedMixin.find(1).higher_item
|
392
|
+
assert_equal ArrayScopeFeaturedMixin.find(3), ArrayScopeFeaturedMixin.find(4).higher_item
|
393
|
+
assert_nil ArrayScopeFeaturedMixin.find(4).lower_item
|
394
|
+
end
|
395
|
+
|
396
|
+
def test_injection
|
397
|
+
item = ArrayScopeFeaturedMixin.new(:parent_id => 1, :parent_type => 'ParentClass')
|
398
|
+
assert_equal '"mixins"."parent_id" = 1 AND "mixins"."parent_type" = \'ParentClass\'', item.scope_condition
|
399
|
+
assert_equal "pos", item.featured_position_column
|
400
|
+
end
|
401
|
+
|
402
|
+
def test_insert
|
403
|
+
ArrayScopeFeaturedMixin.destroy_all
|
404
|
+
new = ArrayScopeFeaturedMixin.create(:parent_id => 20, :parent_type => 'ParentClass')
|
405
|
+
new.featured = true
|
406
|
+
assert_equal 1, new.pos
|
407
|
+
assert new.first?
|
408
|
+
assert new.last?
|
409
|
+
|
410
|
+
new = ArrayScopeFeaturedMixin.create(:parent_id => 20, :parent_type => 'ParentClass')
|
411
|
+
new.featured = true
|
412
|
+
assert_equal 2, new.pos
|
413
|
+
assert !new.first?
|
414
|
+
assert new.last?
|
415
|
+
|
416
|
+
new = ArrayScopeFeaturedMixin.create(:parent_id => 20, :parent_type => 'ParentClass')
|
417
|
+
new.featured = true
|
418
|
+
assert_equal 3, new.pos
|
419
|
+
assert !new.first?
|
420
|
+
assert new.last?
|
421
|
+
|
422
|
+
new = ArrayScopeFeaturedMixin.create(:parent_id => 0, :parent_type => 'ParentClass')
|
423
|
+
new.featured = true
|
424
|
+
assert_equal 1, new.pos
|
425
|
+
assert new.first?
|
426
|
+
assert new.last?
|
427
|
+
end
|
428
|
+
|
429
|
+
def test_feature_at
|
430
|
+
new = ArrayScopeFeaturedMixin.create(:parent_id => 20, :parent_type => 'ParentClass')
|
431
|
+
new.featured = true
|
432
|
+
assert new.featured?
|
433
|
+
assert new.featured
|
434
|
+
assert_equal 1, new.pos
|
435
|
+
|
436
|
+
new = ArrayScopeFeaturedMixin.create(:parent_id => 20, :parent_type => 'ParentClass')
|
437
|
+
new.featured = true
|
438
|
+
assert_equal 2, new.pos
|
439
|
+
|
440
|
+
new = ArrayScopeFeaturedMixin.create(:parent_id => 20, :parent_type => 'ParentClass')
|
441
|
+
new.featured = true
|
442
|
+
assert_equal 3, new.pos
|
443
|
+
|
444
|
+
new4 = ArrayScopeFeaturedMixin.create(:parent_id => 20, :parent_type => 'ParentClass')
|
445
|
+
new4.featured = true
|
446
|
+
assert_equal 4, new4.pos
|
447
|
+
|
448
|
+
new4.feature_at(3)
|
449
|
+
assert_equal 3, new4.pos
|
450
|
+
|
451
|
+
new.reload
|
452
|
+
assert_equal 4, new.pos
|
453
|
+
|
454
|
+
new.feature_at(2)
|
455
|
+
assert_equal 2, new.pos
|
456
|
+
|
457
|
+
new4.reload
|
458
|
+
assert_equal 4, new4.pos
|
459
|
+
|
460
|
+
new5 = ArrayScopeFeaturedMixin.create(:parent_id => 20, :parent_type => 'ParentClass')
|
461
|
+
new5.featured = true
|
462
|
+
assert_equal 5, new5.pos
|
463
|
+
|
464
|
+
new5.feature_at(1)
|
465
|
+
assert_equal 1, new5.pos
|
466
|
+
|
467
|
+
new4.reload
|
468
|
+
assert_equal 5, new4.pos
|
469
|
+
end
|
470
|
+
|
471
|
+
def test_delete_middle
|
472
|
+
assert_equal [1, 2, 3, 4], ArrayScopeFeaturedMixin.where(:parent_id => 5, :parent_type => 'ParentClass').order('pos').map(&:id)
|
473
|
+
|
474
|
+
ArrayScopeFeaturedMixin.find(2).destroy
|
475
|
+
|
476
|
+
assert_equal [1, 3, 4], ArrayScopeFeaturedMixin.where(:parent_id => 5, :parent_type => 'ParentClass').order('pos').map(&:id)
|
477
|
+
|
478
|
+
assert_equal 1, ArrayScopeFeaturedMixin.find(1).pos
|
479
|
+
assert_equal 2, ArrayScopeFeaturedMixin.find(3).pos
|
480
|
+
assert_equal 3, ArrayScopeFeaturedMixin.find(4).pos
|
481
|
+
|
482
|
+
ArrayScopeFeaturedMixin.find(1).destroy
|
483
|
+
|
484
|
+
assert_equal [3, 4], ArrayScopeFeaturedMixin.where(:parent_id => 5, :parent_type => 'ParentClass').order('pos').map(&:id)
|
485
|
+
|
486
|
+
assert_equal 1, ArrayScopeFeaturedMixin.find(3).pos
|
487
|
+
assert_equal 2, ArrayScopeFeaturedMixin.find(4).pos
|
488
|
+
end
|
489
|
+
|
490
|
+
def test_unfeature_should_then_fail_in_list?
|
491
|
+
assert_equal true, ArrayScopeFeaturedMixin.find(1).in_list?
|
492
|
+
ArrayScopeFeaturedMixin.find(1).unfeature
|
493
|
+
assert_equal false, ArrayScopeFeaturedMixin.find(1).in_list?
|
494
|
+
end
|
495
|
+
|
496
|
+
def test_unfeature_should_set_position_to_nil
|
497
|
+
assert_equal [1, 2, 3, 4], ArrayScopeFeaturedMixin.where(:parent_id => 5, :parent_type => 'ParentClass').order('pos').map(&:id)
|
498
|
+
|
499
|
+
ArrayScopeFeaturedMixin.find(2).unfeature
|
500
|
+
|
501
|
+
assert_equal [2, 1, 3, 4], ArrayScopeFeaturedMixin.where(:parent_id => 5, :parent_type => 'ParentClass').order('pos').map(&:id)
|
502
|
+
|
503
|
+
assert_equal 1, ArrayScopeFeaturedMixin.find(1).pos
|
504
|
+
assert_equal nil, ArrayScopeFeaturedMixin.find(2).pos
|
505
|
+
assert_equal 2, ArrayScopeFeaturedMixin.find(3).pos
|
506
|
+
assert_equal 3, ArrayScopeFeaturedMixin.find(4).pos
|
507
|
+
end
|
508
|
+
|
509
|
+
def test_remove_before_destroy_does_not_shift_lower_items_twice
|
510
|
+
assert_equal [1, 2, 3, 4], ArrayScopeFeaturedMixin.where(:parent_id => 5, :parent_type => 'ParentClass').order('pos').map(&:id)
|
511
|
+
|
512
|
+
ArrayScopeFeaturedMixin.find(2).unfeature
|
513
|
+
ArrayScopeFeaturedMixin.find(2).destroy
|
514
|
+
|
515
|
+
assert_equal [1, 3, 4], ArrayScopeFeaturedMixin.where(:parent_id => 5, :parent_type => 'ParentClass').order('pos').map(&:id)
|
516
|
+
|
517
|
+
assert_equal 1, ArrayScopeFeaturedMixin.find(1).pos
|
518
|
+
assert_equal 2, ArrayScopeFeaturedMixin.find(3).pos
|
519
|
+
assert_equal 3, ArrayScopeFeaturedMixin.find(4).pos
|
520
|
+
end
|
521
|
+
|
522
|
+
end
|
523
|
+
|
data/test/schema.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'sqlite3'
|
3
|
+
|
4
|
+
ActiveRecord::Base.establish_connection(
|
5
|
+
:adapter => defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby' ? 'jdbcsqlite3' : 'sqlite3',
|
6
|
+
:database => File.join(File.dirname(__FILE__), 'test.db')
|
7
|
+
)
|
8
|
+
|
9
|
+
class CreateSchema < ActiveRecord::Migration
|
10
|
+
def self.up
|
11
|
+
create_table :mixins, :force => true do |t|
|
12
|
+
t.integer :pos
|
13
|
+
t.integer :parent_id
|
14
|
+
t.string :parent_type
|
15
|
+
t.timestamp
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def setup_db
|
21
|
+
CreateSchema.suppress_messages do
|
22
|
+
CreateSchema.migrate(:up)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
setup_db
|
27
|
+
|
28
|
+
def teardown_db
|
29
|
+
ActiveRecord::Base.connection.tables.each do |table|
|
30
|
+
ActiveRecord::Base.connection.drop_table(table)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Mixin < ActiveRecord::Base
|
35
|
+
end
|
36
|
+
|
37
|
+
class FeaturedMixin < Mixin
|
38
|
+
self.table_name = 'mixins'
|
39
|
+
|
40
|
+
has_features :column => "pos", :scope => :parent
|
41
|
+
end
|
42
|
+
|
43
|
+
class FeaturedMixinSub1 < FeaturedMixin
|
44
|
+
end
|
45
|
+
|
46
|
+
class FeaturedMixinSub2 < FeaturedMixin
|
47
|
+
end
|
48
|
+
|
49
|
+
class FeaturedWithStringScopeMixin < ActiveRecord::Base
|
50
|
+
self.table_name = 'mixins'
|
51
|
+
|
52
|
+
has_features :column => "pos", :scope => 'parent_id = #{parent_id}'
|
53
|
+
end
|
54
|
+
|
55
|
+
class ArrayScopeFeaturedMixin < Mixin
|
56
|
+
self.table_name = 'mixins'
|
57
|
+
|
58
|
+
has_features :column => "pos", :scope => [:parent_id, :parent_type]
|
59
|
+
end
|
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: has_features
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Seth Faxon
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-02-08 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: &70252332686700 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.0.0
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70252332686700
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: activerecord
|
27
|
+
requirement: &70252332683660 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 3.0.0
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70252332683660
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rdoc
|
38
|
+
requirement: &70252332682960 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70252332682960
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: sqlite3
|
49
|
+
requirement: &70252332682000 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70252332682000
|
58
|
+
description: Based on acts_as_list, but allows nil elements to be not a part of the
|
59
|
+
featuerd list.
|
60
|
+
email:
|
61
|
+
- seth.faxon@gmail.com
|
62
|
+
executables: []
|
63
|
+
extensions: []
|
64
|
+
extra_rdoc_files: []
|
65
|
+
files:
|
66
|
+
- .gitignore
|
67
|
+
- README
|
68
|
+
- Rakefile
|
69
|
+
- has_features.gemspec
|
70
|
+
- init.rb
|
71
|
+
- lib/has_features.rb
|
72
|
+
- lib/has_features/active_record/has/features.rb
|
73
|
+
- lib/has_features/version.rb
|
74
|
+
- test/list_test.rb
|
75
|
+
- test/schema.rb
|
76
|
+
homepage: http://github.com/sfaxon/has_features
|
77
|
+
licenses: []
|
78
|
+
post_install_message:
|
79
|
+
rdoc_options: []
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
requirements: []
|
95
|
+
rubyforge_project: has_features
|
96
|
+
rubygems_version: 1.8.8
|
97
|
+
signing_key:
|
98
|
+
specification_version: 3
|
99
|
+
summary: A gem allowing a active_record model to have an ordered list of featured
|
100
|
+
items.
|
101
|
+
test_files:
|
102
|
+
- test/list_test.rb
|
103
|
+
- test/schema.rb
|