acts_as_featured 0.1.2
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/Gemfile +8 -0
- data/MIT-LICENSE +21 -0
- data/README.markdown +41 -0
- data/acts_as_featured.gemspec +39 -0
- data/lib/acts_as_featured.rb +93 -0
- data/lib/acts_as_featured/version.rb +3 -0
- data/spec/acts_as_featured_spec.rb +107 -0
- data/spec/models.rb +44 -0
- data/spec/spec_helper.rb +5 -0
- metadata +133 -0
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2009 Brandan Lennox
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
Acts as Featured
|
2
|
+
================
|
3
|
+
|
4
|
+
Designates an attribute on this model to indicate "featuredness," where only one record within a given scope may be featured at a time.
|
5
|
+
|
6
|
+
Pass in the name of the attribute and an options hash:
|
7
|
+
|
8
|
+
* `:scope` - If given, designates the scope in which this model is featured. This would typically be a `belongs_to` association.
|
9
|
+
* `:create_scope` - If `true`, creates a named scope using the name of the attribute given here. If it's a symbol, creates a named scope using that symbol.
|
10
|
+
|
11
|
+
Examples:
|
12
|
+
|
13
|
+
class Project < ActiveRecord::Base
|
14
|
+
# no two Projects will ever have their @featured attributes set simultaneously
|
15
|
+
acts_as_featured :featured
|
16
|
+
end
|
17
|
+
|
18
|
+
class Photo < ActiveRecord::Base
|
19
|
+
# each account gets a favorite photo
|
20
|
+
belongs_to :account
|
21
|
+
acts_as_featured :favorite, :scope => :account
|
22
|
+
end
|
23
|
+
|
24
|
+
class Article < ActiveRecord::Base
|
25
|
+
# creates a named scope called Article.featured to return the featured article
|
26
|
+
acts_as_featured :main, :create_scope => :featured
|
27
|
+
end
|
28
|
+
|
29
|
+
Running Tests
|
30
|
+
-------------
|
31
|
+
|
32
|
+
It's RSpec:
|
33
|
+
|
34
|
+
rspec spec/acts_as_featured_spec.rb
|
35
|
+
|
36
|
+
Make sure you run `bundle install` to get the development dependencies first.
|
37
|
+
|
38
|
+
License
|
39
|
+
-------
|
40
|
+
|
41
|
+
See MIT-LICENSE.
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "acts_as_featured/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "acts_as_featured"
|
7
|
+
s.version = ActsAsFeatured::VERSION
|
8
|
+
s.authors = ["Brandan Lennox"]
|
9
|
+
s.email = ["brandan@bclennox.com"]
|
10
|
+
s.homepage = "https://github.com/bclennox/acts_as_featured"
|
11
|
+
s.date = "2012-01-28"
|
12
|
+
s.summary = %q{Designate a Rails model attribute as unique within a scope}
|
13
|
+
s.description = %q{Designates an attribute on this model to indicate "featuredness," where only one record within a given scope may be featured at a time.}
|
14
|
+
|
15
|
+
s.rubyforge_project = "acts_as_featured"
|
16
|
+
|
17
|
+
s.files = %w{
|
18
|
+
acts_as_featured.gemspec
|
19
|
+
Gemfile
|
20
|
+
MIT-LICENSE
|
21
|
+
README.markdown
|
22
|
+
|
23
|
+
lib/acts_as_featured.rb
|
24
|
+
lib/acts_as_featured/version.rb
|
25
|
+
}
|
26
|
+
|
27
|
+
s.test_files = %w{
|
28
|
+
spec/acts_as_featured_spec.rb
|
29
|
+
spec/models.rb
|
30
|
+
spec/spec_helper.rb
|
31
|
+
}
|
32
|
+
|
33
|
+
s.require_paths = ["lib"]
|
34
|
+
|
35
|
+
s.add_dependency "activerecord", ">= 3.0.0"
|
36
|
+
s.add_development_dependency "rspec"
|
37
|
+
s.add_development_dependency "sqlite3"
|
38
|
+
s.add_development_dependency "sqlite3-ruby"
|
39
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'acts_as_featured/version'
|
2
|
+
|
3
|
+
module ActsAsFeatured
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
|
8
|
+
# Designates an attribute on this model to indicate "featuredness," where
|
9
|
+
# only one record within a given scope may be featured at a time.
|
10
|
+
#
|
11
|
+
# Pass in the name of the attribute and an options hash:
|
12
|
+
#
|
13
|
+
# * <tt>:scope</tt> - If given, designates the scope in which this model is featured. This would typically be a <tt>belongs_to</tt> association.
|
14
|
+
# * <tt>:create_scope</tt> - If <tt>true</tt>, creates a named scope using the name of the attribute given here. If it's a symbol, creates a named scope using that symbol.
|
15
|
+
#
|
16
|
+
# class Project < ActiveRecord::Base
|
17
|
+
# # no two Projects will ever have their @featured attributes set simultaneously
|
18
|
+
# acts_as_featured :featured
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# class Photo < ActiveRecord::Base
|
22
|
+
# # each account gets a favorite photo
|
23
|
+
# belongs_to :account
|
24
|
+
# acts_as_featured :favorite, :scope => :account
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# class Article < ActiveRecord::Base
|
28
|
+
# # creates a named scope called Article.featured to return the featured article
|
29
|
+
# acts_as_featured :main, :create_scope => :featured
|
30
|
+
# end
|
31
|
+
def acts_as_featured(attribute, options = {})
|
32
|
+
cattr_accessor :featured_attribute
|
33
|
+
cattr_accessor :featured_attribute_scope
|
34
|
+
|
35
|
+
self.featured_attribute = attribute
|
36
|
+
self.featured_attribute_scope = options[:scope] || false
|
37
|
+
|
38
|
+
if scope_name = options[:create_scope]
|
39
|
+
scope_name = attribute if scope_name === true
|
40
|
+
scope scope_name, where(attribute => true).limit(1)
|
41
|
+
end
|
42
|
+
|
43
|
+
before_save :remove_featured_from_other_records
|
44
|
+
after_save :add_featured_to_first_record
|
45
|
+
before_destroy :add_featured_to_first_record_if_featured
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# <tt>before_save</tt> callback: If we're designating this record to be
|
52
|
+
# featured, we should clear that status on other records before saving this one.
|
53
|
+
def remove_featured_from_other_records
|
54
|
+
if scope && send(featured_attribute)
|
55
|
+
# I hope I find a better way to do this
|
56
|
+
scope.update_all(["#{featured_attribute} = ?", false], "id != #{id || 0}")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# <tt>after_save</tt> callback. If this save will result in no featured, just
|
61
|
+
# make the first record featured.
|
62
|
+
def add_featured_to_first_record
|
63
|
+
if scope && scope.count(:conditions => { featured_attribute => true }) == 0
|
64
|
+
scope.first.update_attribute(featured_attribute, true)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# <tt>before_destroy</tt> callback. If we destroy the featured, make the first
|
69
|
+
# unfeaturedmain the featured. If this was the last record, don't do anything.
|
70
|
+
def add_featured_to_first_record_if_featured
|
71
|
+
if scope && send(featured_attribute) && scope.count > 1
|
72
|
+
new_main = scope.find(:first, :conditions => { featured_attribute => false })
|
73
|
+
new_main.update_attribute(featured_attribute, true) unless new_main.nil?
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def featured_attribute
|
78
|
+
@featured_attribute ||= self.class.featured_attribute
|
79
|
+
end
|
80
|
+
|
81
|
+
# Either the model class or the scope given to acts_as_featured (probably a
|
82
|
+
# belongs_to association).
|
83
|
+
def scope
|
84
|
+
association = self.class.featured_attribute_scope
|
85
|
+
@scope ||= if association
|
86
|
+
send(association).try(self.class.to_s.underscore.pluralize)
|
87
|
+
else
|
88
|
+
self.class
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
ActiveRecord::Base.send :include, ActsAsFeatured
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
describe ActsAsFeatured do
|
4
|
+
def featured(model)
|
5
|
+
model.find_all_by_featured(true)
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "unscoped" do
|
9
|
+
before(:each) do
|
10
|
+
3.times { Thingy.create(:featured => false) }
|
11
|
+
end
|
12
|
+
|
13
|
+
after(:each) do
|
14
|
+
Thingy.delete_all
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should leave featured status alone when saving the currently featured thingy' do
|
18
|
+
originally_featured = Thingy.create(:featured => true)
|
19
|
+
featured(Thingy).first.should == originally_featured
|
20
|
+
originally_featured.save
|
21
|
+
featured(Thingy).first.should == originally_featured
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should remove featured status from the currently featured thingy when setting it on another thingy' do
|
25
|
+
originally_featured = Thingy.create(:featured => true)
|
26
|
+
will_be_featured = Thingy.create(:featured => false)
|
27
|
+
|
28
|
+
will_be_featured.featured = true
|
29
|
+
will_be_featured.save!
|
30
|
+
|
31
|
+
originally_featured.reload
|
32
|
+
will_be_featured.reload
|
33
|
+
|
34
|
+
featured(Thingy).should have(1).item
|
35
|
+
originally_featured.featured?.should be_false
|
36
|
+
will_be_featured.featured?.should be_true
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should set another thingy to be featured when removing featured status from the featured thingy' do
|
40
|
+
3.times { Thingy.create(:featured => false) }
|
41
|
+
originally_featured = Thingy.create(:featured => true)
|
42
|
+
|
43
|
+
originally_featured.featured = false
|
44
|
+
originally_featured.save!
|
45
|
+
originally_featured.reload
|
46
|
+
|
47
|
+
featured(Thingy).should have(1).item
|
48
|
+
originally_featured.featured?.should be_false
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should set another thingy to be featured when destroying the featured thingy' do
|
52
|
+
3.times { Thingy.create(:featured => false) }
|
53
|
+
originally_featured = Thingy.create(:featured => true)
|
54
|
+
|
55
|
+
originally_featured.destroy
|
56
|
+
|
57
|
+
featured(Thingy).should have(1).item
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "with a scope" do
|
62
|
+
after(:each) do
|
63
|
+
ThingyAggregator.delete_all
|
64
|
+
ScopedThingy.delete_all
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should constrain featured status to scope' do
|
68
|
+
aggregator1 = ThingyAggregator.create
|
69
|
+
aggregator2 = ThingyAggregator.create
|
70
|
+
|
71
|
+
3.times { aggregator1.scoped_thingies.create(:featured => false) }
|
72
|
+
3.times { aggregator2.scoped_thingies.create(:featured => false) }
|
73
|
+
f1 = aggregator1.scoped_thingies.create(:featured => true)
|
74
|
+
f2 = aggregator2.scoped_thingies.create(:featured => true)
|
75
|
+
|
76
|
+
featured(ScopedThingy).should have(2).items
|
77
|
+
|
78
|
+
f1.update_attribute(:featured, false)
|
79
|
+
|
80
|
+
featured(ScopedThingy).should have(2).items
|
81
|
+
aggregator2.scoped_thingies.find_by_featured(true).should == f2
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'should not explode when the scope is nil' do
|
85
|
+
expect {
|
86
|
+
ScopedThingy.create!(:featured => true)
|
87
|
+
featured(ScopedThingy).should have(1).item
|
88
|
+
}.not_to raise_error
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "named scope" do
|
93
|
+
it 'should add a default named scope' do
|
94
|
+
DefaultNamedScopeThingy.should respond_to(:featured)
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'should add a custom named scope' do
|
98
|
+
CustomNamedScopeThingy.should respond_to(:special)
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'should return the featured thingy from the named scope' do
|
102
|
+
3.times { DefaultNamedScopeThingy.create!(:featured => false) }
|
103
|
+
t = DefaultNamedScopeThingy.create!(:featured => true)
|
104
|
+
DefaultNamedScopeThingy.featured.first.should == t
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/spec/models.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'acts_as_featured')
|
2
|
+
|
3
|
+
# a basic model
|
4
|
+
class Thingy < ActiveRecord::Base
|
5
|
+
acts_as_featured :featured, :create_scope => true
|
6
|
+
end
|
7
|
+
|
8
|
+
# a model that belongs_to another model
|
9
|
+
class ScopedThingy < ActiveRecord::Base
|
10
|
+
belongs_to :thingy_aggregator
|
11
|
+
acts_as_featured :featured, :scope => :thingy_aggregator
|
12
|
+
end
|
13
|
+
class ThingyAggregator < ActiveRecord::Base
|
14
|
+
has_many :scoped_thingies
|
15
|
+
end
|
16
|
+
|
17
|
+
# a model with a default named scope
|
18
|
+
class DefaultNamedScopeThingy < ActiveRecord::Base
|
19
|
+
acts_as_featured :featured, :create_scope => true
|
20
|
+
end
|
21
|
+
|
22
|
+
# a model with a custom named scope
|
23
|
+
class CustomNamedScopeThingy < ActiveRecord::Base
|
24
|
+
acts_as_featured :featured, :create_scope => :special
|
25
|
+
end
|
26
|
+
|
27
|
+
ActiveRecord::Migration.verbose = false
|
28
|
+
ActiveRecord::Schema.define(:version => 1) do
|
29
|
+
[:thingies, :named_scope_thingies, :default_named_scope_thingies].each do |name|
|
30
|
+
create_table name, :force => true do |t|
|
31
|
+
t.boolean :featured
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
create_table :thingy_aggregators, :force => true do |t|
|
36
|
+
t.timestamps
|
37
|
+
end
|
38
|
+
|
39
|
+
create_table :scoped_thingies, :force => true do |t|
|
40
|
+
t.belongs_to :thingy_aggregator
|
41
|
+
t.boolean :featured
|
42
|
+
end
|
43
|
+
end
|
44
|
+
ActiveRecord::Migration.verbose = true
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acts_as_featured
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 31
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 2
|
10
|
+
version: 0.1.2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Brandan Lennox
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-01-28 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: activerecord
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 7
|
29
|
+
segments:
|
30
|
+
- 3
|
31
|
+
- 0
|
32
|
+
- 0
|
33
|
+
version: 3.0.0
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rspec
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 3
|
45
|
+
segments:
|
46
|
+
- 0
|
47
|
+
version: "0"
|
48
|
+
type: :development
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: sqlite3
|
52
|
+
prerelease: false
|
53
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
hash: 3
|
59
|
+
segments:
|
60
|
+
- 0
|
61
|
+
version: "0"
|
62
|
+
type: :development
|
63
|
+
version_requirements: *id003
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: sqlite3-ruby
|
66
|
+
prerelease: false
|
67
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
hash: 3
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
version: "0"
|
76
|
+
type: :development
|
77
|
+
version_requirements: *id004
|
78
|
+
description: Designates an attribute on this model to indicate "featuredness," where only one record within a given scope may be featured at a time.
|
79
|
+
email:
|
80
|
+
- brandan@bclennox.com
|
81
|
+
executables: []
|
82
|
+
|
83
|
+
extensions: []
|
84
|
+
|
85
|
+
extra_rdoc_files: []
|
86
|
+
|
87
|
+
files:
|
88
|
+
- acts_as_featured.gemspec
|
89
|
+
- Gemfile
|
90
|
+
- MIT-LICENSE
|
91
|
+
- README.markdown
|
92
|
+
- lib/acts_as_featured.rb
|
93
|
+
- lib/acts_as_featured/version.rb
|
94
|
+
- spec/acts_as_featured_spec.rb
|
95
|
+
- spec/models.rb
|
96
|
+
- spec/spec_helper.rb
|
97
|
+
homepage: https://github.com/bclennox/acts_as_featured
|
98
|
+
licenses: []
|
99
|
+
|
100
|
+
post_install_message:
|
101
|
+
rdoc_options: []
|
102
|
+
|
103
|
+
require_paths:
|
104
|
+
- lib
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
hash: 3
|
111
|
+
segments:
|
112
|
+
- 0
|
113
|
+
version: "0"
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
none: false
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
hash: 3
|
120
|
+
segments:
|
121
|
+
- 0
|
122
|
+
version: "0"
|
123
|
+
requirements: []
|
124
|
+
|
125
|
+
rubyforge_project: acts_as_featured
|
126
|
+
rubygems_version: 1.8.15
|
127
|
+
signing_key:
|
128
|
+
specification_version: 3
|
129
|
+
summary: Designate a Rails model attribute as unique within a scope
|
130
|
+
test_files:
|
131
|
+
- spec/acts_as_featured_spec.rb
|
132
|
+
- spec/models.rb
|
133
|
+
- spec/spec_helper.rb
|