mbleigh-acts-as-taggable-on 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +9 -0
- data/MIT-LICENSE +20 -0
- data/README +154 -0
- data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +8 -0
- data/generators/acts_as_taggable_on_migration/templates/add_users_migration.rb +11 -0
- data/generators/acts_as_taggable_on_migration/templates/migration.rb +27 -0
- data/init.rb +1 -0
- data/lib/acts-as-taggable-on.rb +6 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +293 -0
- data/lib/acts_as_taggable_on/acts_as_tagger.rb +48 -0
- data/lib/acts_as_taggable_on/tag.rb +23 -0
- data/lib/acts_as_taggable_on/tag_list.rb +93 -0
- data/lib/acts_as_taggable_on/tagging.rb +6 -0
- data/lib/acts_as_taggable_on/tags_helper.rb +11 -0
- data/rails/init.rb +6 -0
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +80 -0
- data/spec/acts_as_taggable_on/tag_list_spec.rb +41 -0
- data/spec/acts_as_taggable_on/tag_spec.rb +25 -0
- data/spec/acts_as_taggable_on/taggable_spec.rb +118 -0
- data/spec/acts_as_taggable_on/tagger_spec.rb +22 -0
- data/spec/acts_as_taggable_on/tagging_spec.rb +7 -0
- data/spec/schema.rb +24 -0
- data/spec/spec_helper.rb +22 -0
- data/uninstall.rb +1 -0
- metadata +80 -0
data/CHANGELOG
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2007 Michael Bleigh and Intridea Inc.
|
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
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
ActsAsTaggableOn
|
2
|
+
================
|
3
|
+
|
4
|
+
This plugin was originally based on Acts as Taggable on Steroids by Jonathan Viney.
|
5
|
+
It has evolved substantially since that point, but all credit goes to him for the
|
6
|
+
initial tagging functionality that so many people have used.
|
7
|
+
|
8
|
+
For instance, in a social network, a user might have tags that are called skills,
|
9
|
+
interests, sports, and more. There is no real way to differentiate between tags and
|
10
|
+
so an implementation of this type is not possible with acts as taggable on steroids.
|
11
|
+
|
12
|
+
Enter Acts as Taggable On. Rather than tying functionality to a specific keyword
|
13
|
+
(namely "tags"), acts as taggable on allows you to specify an arbitrary number of
|
14
|
+
tag "contexts" that can be used locally or in combination in the same way steroids
|
15
|
+
was used.
|
16
|
+
|
17
|
+
Installation
|
18
|
+
============
|
19
|
+
|
20
|
+
Plugin
|
21
|
+
------
|
22
|
+
|
23
|
+
Acts As Taggable On is available both as a gem and as a traditional plugin. For the
|
24
|
+
traditional plugin you can install like so (Rails 2.1 or later):
|
25
|
+
|
26
|
+
script/plugin install git://github.com/mbleigh/acts-as-taggable-on.git
|
27
|
+
|
28
|
+
For earlier versions:
|
29
|
+
|
30
|
+
git clone git://github.com/mbleigh/acts-as-taggable-on.git vendor/plugins/acts-as-taggable-on
|
31
|
+
|
32
|
+
GemPlugin
|
33
|
+
---------
|
34
|
+
|
35
|
+
Acts As Taggable On is also available as a gem plugin using Rails 2.1's gem dependencies.
|
36
|
+
To install the gem, add this to your config/environment.rb:
|
37
|
+
|
38
|
+
config.gem "mbleigh-acts-as-taggable-on", :source => "http://gems.github.com"
|
39
|
+
|
40
|
+
After that, you can run "rake gems:install" to install the gem if you don't already have it.
|
41
|
+
See http://ryandaigle.com/articles/2008/4/1/what-s-new-in-edge-rails-gem-dependencies for
|
42
|
+
additional details about gem dependencies in Rails.
|
43
|
+
|
44
|
+
Testing
|
45
|
+
=======
|
46
|
+
|
47
|
+
Acts As Taggable On uses RSpec for its test coverage. If you already have RSpec on your
|
48
|
+
application, the specs will run while using:
|
49
|
+
|
50
|
+
rake spec:plugins
|
51
|
+
|
52
|
+
Example
|
53
|
+
=======
|
54
|
+
|
55
|
+
class User < ActiveRecord::Base
|
56
|
+
acts_as_taggable_on :tags, :skills, :interests
|
57
|
+
end
|
58
|
+
|
59
|
+
@user = User.new(:name => "Bobby")
|
60
|
+
@user.tag_list = "awesome, slick, hefty" # this should be familiar
|
61
|
+
@user.skill_list = "joking, clowning, boxing" # but you can do it for any context!
|
62
|
+
@user.skill_list # => ["joking","clowning","boxing"] as TagList
|
63
|
+
@user.save
|
64
|
+
|
65
|
+
@user.tags # => [<Tag name:"awesome">,<Tag name:"slick">,<Tag name:"hefty">]
|
66
|
+
@user.skills # => [<Tag name:"joking">,<Tag name:"clowning">,<Tag name:"boxing">]
|
67
|
+
|
68
|
+
User.find_tagged_with("awesome", :on => :tags) # => [@user]
|
69
|
+
User.find_tagged_with("awesome", :on => :skills) # => []
|
70
|
+
|
71
|
+
@frankie = User.create(:name => "Frankie", :skill_list => "joking, flying, eating")
|
72
|
+
User.skill_counts # => [<Tag name="joking" count=2>,<Tag name="clowning" count=1>...]
|
73
|
+
@frankie.skill_counts
|
74
|
+
|
75
|
+
Relationships
|
76
|
+
====================
|
77
|
+
|
78
|
+
You can find objects of the same type based on similar tags on certain contexts.
|
79
|
+
Also, objects will be returned in descending order based on the total number of
|
80
|
+
matched tags.
|
81
|
+
|
82
|
+
@bobby = User.find_by_name("Bobby")
|
83
|
+
@bobby.skill_list # => ["jogging", "diving"]
|
84
|
+
|
85
|
+
@frankie = User.find_by_name("Frankie")
|
86
|
+
@frankie.skill_list # => ["hacking"]
|
87
|
+
|
88
|
+
@tom = User.find_by_name("Tom")
|
89
|
+
@tom.skill_list # => ["hacking", "jogging", "diving"]
|
90
|
+
|
91
|
+
@tom.find_related_skills # => [<User name="Bobby">,<User name="Frankie">]
|
92
|
+
@bobby.find_related_skills # => [<User name="Tom">]
|
93
|
+
@frankie.find_related_skills # => [<User name="Tom">]
|
94
|
+
|
95
|
+
|
96
|
+
Dynamic Tag Contexts
|
97
|
+
====================
|
98
|
+
|
99
|
+
In addition to the generated tag contexts in the definition, it is also possible
|
100
|
+
to allow for dynamic tag contexts (this could be user generated tag contexts!)
|
101
|
+
|
102
|
+
@user = User.new(:name => "Bobby")
|
103
|
+
@user.set_tag_list_on(:customs, "same, as, tag, list")
|
104
|
+
@user.tag_list_on(:customs) # => ["same","as","tag","list"]
|
105
|
+
@user.save
|
106
|
+
@user.tags_on(:customs) # => [<Tag name='same'>,...]
|
107
|
+
@user.tag_counts_on(:customs)
|
108
|
+
User.find_tagged_with("same", :on => :customs) # => [@user]
|
109
|
+
|
110
|
+
Tag Ownership
|
111
|
+
=============
|
112
|
+
|
113
|
+
Tags can have owners:
|
114
|
+
|
115
|
+
class User < ActiveRecord::Base
|
116
|
+
acts_as_tagger
|
117
|
+
end
|
118
|
+
|
119
|
+
class Photo < ActiveRecord::Base
|
120
|
+
acts_as_taggable_on :locations
|
121
|
+
end
|
122
|
+
|
123
|
+
@some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations)
|
124
|
+
@some_user.owned_taggings
|
125
|
+
@some_user.owned_tags
|
126
|
+
@some_photo.locations_from(@some_user)
|
127
|
+
|
128
|
+
Caveats, Uncharted Waters
|
129
|
+
=========================
|
130
|
+
|
131
|
+
This plugin is still under active development. Tag caching has not
|
132
|
+
been thoroughly (or even casually) tested and may not work as expected.
|
133
|
+
|
134
|
+
Contributors
|
135
|
+
============
|
136
|
+
|
137
|
+
* Michael Bleigh - Original Author
|
138
|
+
* Brendan Lim - Related objects
|
139
|
+
* Pradeep Elankumaran - Taggers
|
140
|
+
|
141
|
+
Patch Contributors
|
142
|
+
------------------
|
143
|
+
|
144
|
+
* Peter Cooper - named_scope fix
|
145
|
+
* slainer68 - STI fix
|
146
|
+
|
147
|
+
Resources
|
148
|
+
=========
|
149
|
+
|
150
|
+
* Acts As Community - http://www.actsascommunity.com/projects/acts-as-taggable-on
|
151
|
+
* GitHub - http://github.com/mbleigh/acts-as-taggable-on
|
152
|
+
* Lighthouse - http://mbleigh.lighthouseapp.com/projects/10116-acts-as-taggable-on
|
153
|
+
|
154
|
+
Copyright (c) 2007 Michael Bleigh (http://mbleigh.com/) and Intridea Inc. (http://intridea.com/), released under the MIT license
|
@@ -0,0 +1,8 @@
|
|
1
|
+
class ActsAsTaggableOnMigrationGenerator < Rails::Generator::Base
|
2
|
+
def manifest
|
3
|
+
record do |m|
|
4
|
+
m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => "acts_as_taggable_on_migration"
|
5
|
+
m.migration_template 'add_users_migration.rb', 'db/migrate', :migration_file_name => "add_users_to_acts_as_taggable_on_migration"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class AddUsersToActsAsTaggableOnMigration < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
add_column :taggings, :tagger_id, :integer
|
4
|
+
add_column :taggings, :tagger_type, :string
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.down
|
8
|
+
remove_column :taggings, :tagger_type
|
9
|
+
remove_column :taggings, :tagger_id
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class ActsAsTaggableOnMigration < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :tags do |t|
|
4
|
+
t.column :name, :string
|
5
|
+
end
|
6
|
+
|
7
|
+
create_table :taggings do |t|
|
8
|
+
t.column :tag_id, :integer
|
9
|
+
t.column :taggable_id, :integer
|
10
|
+
|
11
|
+
# You should make sure that the column created is
|
12
|
+
# long enough to store the required class names.
|
13
|
+
t.column :taggable_type, :string
|
14
|
+
t.column :context, :string
|
15
|
+
|
16
|
+
t.column :created_at, :datetime
|
17
|
+
end
|
18
|
+
|
19
|
+
add_index :taggings, :tag_id
|
20
|
+
add_index :taggings, [:taggable_id, :taggable_type, :context]
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.down
|
24
|
+
drop_table :taggings
|
25
|
+
drop_table :tags
|
26
|
+
end
|
27
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/rails/init"
|
@@ -0,0 +1,293 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Acts
|
3
|
+
module TaggableOn
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def acts_as_taggable
|
10
|
+
acts_as_taggable_on :tags
|
11
|
+
end
|
12
|
+
|
13
|
+
def acts_as_taggable_on(*args)
|
14
|
+
puts "Registering #{args.inspect} with #{self.inspect}"
|
15
|
+
for tag_type in args
|
16
|
+
tag_type = tag_type.to_s
|
17
|
+
self.class_eval do
|
18
|
+
has_many "#{tag_type.singularize}_taggings".to_sym, :as => :taggable, :dependent => :destroy,
|
19
|
+
:include => :tag, :conditions => ["context = ?",tag_type], :class_name => "Tagging"
|
20
|
+
has_many "#{tag_type}".to_sym, :through => "#{tag_type.singularize}_taggings".to_sym, :source => :tag
|
21
|
+
end
|
22
|
+
|
23
|
+
self.class_eval <<-RUBY
|
24
|
+
def self.caching_#{tag_type.singularize}_list?
|
25
|
+
caching_tag_list_on?("#{tag_type}")
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.#{tag_type.singularize}_counts(options={})
|
29
|
+
tag_counts_on('#{tag_type}',options)
|
30
|
+
end
|
31
|
+
|
32
|
+
def #{tag_type.singularize}_list
|
33
|
+
tag_list_on('#{tag_type}')
|
34
|
+
end
|
35
|
+
|
36
|
+
def #{tag_type.singularize}_list=(new_tags)
|
37
|
+
set_tag_list_on('#{tag_type}',new_tags)
|
38
|
+
end
|
39
|
+
|
40
|
+
def #{tag_type.singularize}_counts(options = {})
|
41
|
+
tag_counts_on('#{tag_type}',options)
|
42
|
+
end
|
43
|
+
|
44
|
+
def #{tag_type}_from(owner)
|
45
|
+
tag_list_on('#{tag_type}', owner)
|
46
|
+
end
|
47
|
+
|
48
|
+
def find_related_#{tag_type}(options = {})
|
49
|
+
related_tags_on('#{tag_type}',options)
|
50
|
+
end
|
51
|
+
alias_method :find_related_on_#{tag_type}, :find_related_#{tag_type}
|
52
|
+
RUBY
|
53
|
+
end
|
54
|
+
|
55
|
+
if respond_to?(:tag_types)
|
56
|
+
puts "Appending #{args.inspect} onto #{tag_types.inspect}"
|
57
|
+
write_inheritable_attribute(:tag_types, tag_types + args)
|
58
|
+
else
|
59
|
+
self.class_eval do
|
60
|
+
write_inheritable_attribute(:tag_types, args)
|
61
|
+
class_inheritable_reader :tag_types
|
62
|
+
|
63
|
+
has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
|
64
|
+
has_many :base_tags, :class_name => "Tag", :through => :taggings, :source => :tag
|
65
|
+
|
66
|
+
attr_writer :custom_contexts
|
67
|
+
|
68
|
+
before_save :save_cached_tag_list
|
69
|
+
after_save :save_tags
|
70
|
+
end
|
71
|
+
|
72
|
+
include ActiveRecord::Acts::TaggableOn::InstanceMethods
|
73
|
+
extend ActiveRecord::Acts::TaggableOn::SingletonMethods
|
74
|
+
alias_method_chain :reload, :tag_list
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def is_taggable?
|
79
|
+
false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
module SingletonMethods
|
84
|
+
# Pass either a tag string, or an array of strings or tags
|
85
|
+
#
|
86
|
+
# Options:
|
87
|
+
# :exclude - Find models that are not tagged with the given tags
|
88
|
+
# :match_all - Find models that match all of the given tags, not just one
|
89
|
+
# :conditions - A piece of SQL conditions to add to the query
|
90
|
+
# :on - scopes the find to a context
|
91
|
+
def find_tagged_with(*args)
|
92
|
+
options = find_options_for_find_tagged_with(*args)
|
93
|
+
options.blank? ? [] : find(:all,options)
|
94
|
+
end
|
95
|
+
|
96
|
+
def caching_tag_list_on?(context)
|
97
|
+
column_names.include?("cached_#{context.to_s.singularize}_list")
|
98
|
+
end
|
99
|
+
|
100
|
+
def tag_counts_on(context, options = {})
|
101
|
+
Tag.find(:all, find_options_for_tag_counts(options.merge({:on => context.to_s})))
|
102
|
+
end
|
103
|
+
|
104
|
+
def find_options_for_find_tagged_with(tags, options = {})
|
105
|
+
tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
|
106
|
+
|
107
|
+
return {} if tags.empty?
|
108
|
+
|
109
|
+
conditions = []
|
110
|
+
conditions << sanitize_sql(options.delete(:conditions)) if options[:conditions]
|
111
|
+
|
112
|
+
unless (on = options.delete(:on)).nil?
|
113
|
+
conditions << sanitize_sql(["context = ?",on.to_s])
|
114
|
+
end
|
115
|
+
|
116
|
+
taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"
|
117
|
+
|
118
|
+
if options.delete(:exclude)
|
119
|
+
tags_conditions = tags.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
|
120
|
+
conditions << sanitize_sql(["#{table_name}.id NOT IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} LEFT OUTER JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id WHERE (#{tags_conditions}) AND #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})", tags])
|
121
|
+
else
|
122
|
+
conditions << tags.map { |t| sanitize_sql(["#{tags_alias}.name LIKE ?", t]) }.join(" OR ")
|
123
|
+
|
124
|
+
if options.delete(:match_all)
|
125
|
+
group = "#{taggings_alias}.taggable_id HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
{ :select => "DISTINCT #{table_name}.*",
|
130
|
+
:joins => "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias} ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key} AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)} " +
|
131
|
+
"LEFT OUTER JOIN #{Tag.table_name} #{tags_alias} ON #{tags_alias}.id = #{taggings_alias}.tag_id",
|
132
|
+
:conditions => conditions.join(" AND "),
|
133
|
+
:group => group
|
134
|
+
}.update(options)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Calculate the tag counts for all tags.
|
138
|
+
#
|
139
|
+
# Options:
|
140
|
+
# :start_at - Restrict the tags to those created after a certain time
|
141
|
+
# :end_at - Restrict the tags to those created before a certain time
|
142
|
+
# :conditions - A piece of SQL conditions to add to the query
|
143
|
+
# :limit - The maximum number of tags to return
|
144
|
+
# :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
|
145
|
+
# :at_least - Exclude tags with a frequency less than the given value
|
146
|
+
# :at_most - Exclude tags with a frequency greater than the given value
|
147
|
+
# :on - Scope the find to only include a certain context
|
148
|
+
def find_options_for_tag_counts(options = {})
|
149
|
+
options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on
|
150
|
+
|
151
|
+
scope = scope(:find)
|
152
|
+
start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
153
|
+
end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
154
|
+
|
155
|
+
type_and_context = "#{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)}"
|
156
|
+
|
157
|
+
conditions = [
|
158
|
+
type_and_context,
|
159
|
+
options[:conditions],
|
160
|
+
start_at,
|
161
|
+
end_at
|
162
|
+
]
|
163
|
+
|
164
|
+
conditions = conditions.compact.join(' AND ')
|
165
|
+
conditions = merge_conditions(conditions, scope[:conditions]) if scope
|
166
|
+
|
167
|
+
joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
|
168
|
+
joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
|
169
|
+
joins << "LEFT OUTER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
|
170
|
+
joins << scope[:joins] if scope && scope[:joins]
|
171
|
+
|
172
|
+
at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
|
173
|
+
at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
|
174
|
+
having = [at_least, at_most].compact.join(' AND ')
|
175
|
+
group_by = "#{Tag.table_name}.id, #{Tag.table_name}.name HAVING COUNT(*) > 0"
|
176
|
+
group_by << " AND #{having}" unless having.blank?
|
177
|
+
|
178
|
+
{ :select => "#{Tag.table_name}.id, #{Tag.table_name}.name, COUNT(*) AS count",
|
179
|
+
:joins => joins.join(" "),
|
180
|
+
:conditions => conditions,
|
181
|
+
:group => group_by
|
182
|
+
}.update(options)
|
183
|
+
end
|
184
|
+
|
185
|
+
def is_taggable?
|
186
|
+
true
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
module InstanceMethods
|
191
|
+
|
192
|
+
def tag_types
|
193
|
+
self.class.tag_types
|
194
|
+
end
|
195
|
+
|
196
|
+
def custom_contexts
|
197
|
+
@custom_contexts ||= []
|
198
|
+
end
|
199
|
+
|
200
|
+
def is_taggable?
|
201
|
+
self.class.is_taggable?
|
202
|
+
end
|
203
|
+
|
204
|
+
def add_custom_context(value)
|
205
|
+
custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
|
206
|
+
end
|
207
|
+
|
208
|
+
def tag_list_on(context, owner=nil)
|
209
|
+
var_name = context.to_s.singularize + "_list"
|
210
|
+
return instance_variable_get("@#{var_name}") unless instance_variable_get("@#{var_name}").nil?
|
211
|
+
|
212
|
+
if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context, owner)).nil?
|
213
|
+
instance_variable_set("@#{var_name}", TagList.from(self["cached_#{var_name}"]))
|
214
|
+
else
|
215
|
+
instance_variable_set("@#{var_name}", TagList.new(*tags_on(context, owner).map(&:name)))
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def tags_on(context, owner=nil)
|
220
|
+
if owner
|
221
|
+
opts = {:conditions => ["context = ? AND tagger_id = ? AND tagger_type = ?",
|
222
|
+
context.to_s, owner.id, owner.class.to_s]}
|
223
|
+
else
|
224
|
+
opts = {:conditions => ["context = ?", context.to_s]}
|
225
|
+
end
|
226
|
+
base_tags.find(:all, opts)
|
227
|
+
end
|
228
|
+
|
229
|
+
def cached_tag_list_on(context)
|
230
|
+
self["cached_#{context.to_s.singularize}_list"]
|
231
|
+
end
|
232
|
+
|
233
|
+
def set_tag_list_on(context,new_list, tagger=nil)
|
234
|
+
instance_variable_set("@#{context.to_s.singularize}_list", TagList.from_owner(tagger, new_list))
|
235
|
+
add_custom_context(context)
|
236
|
+
end
|
237
|
+
|
238
|
+
def tag_counts_on(context,options={})
|
239
|
+
self.class.tag_counts_on(context,{:conditions => ["#{Tag.table_name}.name IN (?)", tag_list_on(context)]}.reverse_merge!(options))
|
240
|
+
end
|
241
|
+
|
242
|
+
def related_tags_on(context, options={})
|
243
|
+
tags_to_find = self.tags_on(context).collect {|t| t.name}
|
244
|
+
search_conditions = {
|
245
|
+
:select => "#{self.class.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
|
246
|
+
:from => "#{self.class.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
|
247
|
+
:conditions => ["#{self.class.table_name}.id != #{self.id} AND #{self.class.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{self.class.to_s}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND #{Tag.table_name}.name IN (?)",tags_to_find],
|
248
|
+
:group => "#{self.class.table_name}.id",
|
249
|
+
:order => "count DESC"
|
250
|
+
}.update(options)
|
251
|
+
|
252
|
+
self.class.find(:all, search_conditions)
|
253
|
+
end
|
254
|
+
|
255
|
+
def save_cached_tag_list
|
256
|
+
self.class.tag_types.map(&:to_s).each do |tag_type|
|
257
|
+
if self.class.send("caching_#{tag_type.singularize}_list?")
|
258
|
+
self["cached_#{tag_type.singularize}_list"] = send("#{tag_type.singularize}_list").to_s
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def save_tags
|
264
|
+
(custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
|
265
|
+
next unless instance_variable_get("@#{tag_type.singularize}_list")
|
266
|
+
owner = instance_variable_get("@#{tag_type.singularize}_list").owner
|
267
|
+
new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type).map(&:name)
|
268
|
+
old_tags = tags_on(tag_type).reject { |tag| instance_variable_get("@#{tag_type.singularize}_list").include?(tag.name) }
|
269
|
+
|
270
|
+
self.class.transaction do
|
271
|
+
base_tags.delete(*old_tags) if old_tags.any?
|
272
|
+
new_tag_names.each do |new_tag_name|
|
273
|
+
new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
|
274
|
+
Tagging.create(:tag_id => new_tag.id, :context => tag_type,
|
275
|
+
:taggable => self, :tagger => owner)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
true
|
281
|
+
end
|
282
|
+
|
283
|
+
def reload_with_tag_list(*args)
|
284
|
+
self.class.tag_types.each do |tag_type|
|
285
|
+
self.instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
|
286
|
+
end
|
287
|
+
|
288
|
+
reload_without_tag_list(*args)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Acts
|
3
|
+
module Tagger
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def acts_as_tagger(opts={})
|
10
|
+
has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
|
11
|
+
:include => :tag, :class_name => "Tagging")
|
12
|
+
has_many :owned_tags, :through => :owned_taggings, :source => :tag
|
13
|
+
include ActiveRecord::Acts::Tagger::InstanceMethods
|
14
|
+
extend ActiveRecord::Acts::Tagger::SingletonMethods
|
15
|
+
end
|
16
|
+
|
17
|
+
def is_tagger?
|
18
|
+
false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module InstanceMethods
|
23
|
+
def self.included(base)
|
24
|
+
end
|
25
|
+
|
26
|
+
def tag(taggable, opts={})
|
27
|
+
return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
|
28
|
+
raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
|
29
|
+
raise "You need to specify some tags using :with" unless opts.has_key?(:with)
|
30
|
+
raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless taggable.tag_types.include?(opts[:on])
|
31
|
+
taggable.set_tag_list_on(opts[:on].to_s, opts[:with], self)
|
32
|
+
taggable.save
|
33
|
+
end
|
34
|
+
|
35
|
+
def is_tagger?
|
36
|
+
self.class.is_tagger?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module SingletonMethods
|
41
|
+
def is_tagger?
|
42
|
+
true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class Tag < ActiveRecord::Base
|
2
|
+
has_many :taggings
|
3
|
+
|
4
|
+
validates_presence_of :name
|
5
|
+
validates_uniqueness_of :name
|
6
|
+
|
7
|
+
# LIKE is used for cross-database case-insensitivity
|
8
|
+
def self.find_or_create_with_like_by_name(name)
|
9
|
+
find(:first, :conditions => ["name LIKE ?", name]) || create(:name => name)
|
10
|
+
end
|
11
|
+
|
12
|
+
def ==(object)
|
13
|
+
super || (object.is_a?(Tag) && name == object.name)
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
name
|
18
|
+
end
|
19
|
+
|
20
|
+
def count
|
21
|
+
read_attribute(:count).to_i
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
class TagList < Array
|
2
|
+
cattr_accessor :delimiter
|
3
|
+
self.delimiter = ','
|
4
|
+
|
5
|
+
def initialize(*args)
|
6
|
+
add(*args)
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_accessor :owner
|
10
|
+
|
11
|
+
# Add tags to the tag_list. Duplicate or blank tags will be ignored.
|
12
|
+
#
|
13
|
+
# tag_list.add("Fun", "Happy")
|
14
|
+
#
|
15
|
+
# Use the <tt>:parse</tt> option to add an unparsed tag string.
|
16
|
+
#
|
17
|
+
# tag_list.add("Fun, Happy", :parse => true)
|
18
|
+
def add(*names)
|
19
|
+
extract_and_apply_options!(names)
|
20
|
+
concat(names)
|
21
|
+
clean!
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
# Remove specific tags from the tag_list.
|
26
|
+
#
|
27
|
+
# tag_list.remove("Sad", "Lonely")
|
28
|
+
#
|
29
|
+
# Like #add, the <tt>:parse</tt> option can be used to remove multiple tags in a string.
|
30
|
+
#
|
31
|
+
# tag_list.remove("Sad, Lonely", :parse => true)
|
32
|
+
def remove(*names)
|
33
|
+
extract_and_apply_options!(names)
|
34
|
+
delete_if { |name| names.include?(name) }
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
# Transform the tag_list into a tag string suitable for edting in a form.
|
39
|
+
# The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
|
40
|
+
#
|
41
|
+
# tag_list = TagList.new("Round", "Square,Cube")
|
42
|
+
# tag_list.to_s # 'Round, "Square,Cube"'
|
43
|
+
def to_s
|
44
|
+
clean!
|
45
|
+
|
46
|
+
map do |name|
|
47
|
+
name.include?(delimiter) ? "\"#{name}\"" : name
|
48
|
+
end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
# Remove whitespace, duplicates, and blanks.
|
53
|
+
def clean!
|
54
|
+
reject!(&:blank?)
|
55
|
+
map!(&:strip)
|
56
|
+
uniq!
|
57
|
+
end
|
58
|
+
|
59
|
+
def extract_and_apply_options!(args)
|
60
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
61
|
+
options.assert_valid_keys :parse
|
62
|
+
|
63
|
+
if options[:parse]
|
64
|
+
args.map! { |a| self.class.from(a) }
|
65
|
+
end
|
66
|
+
|
67
|
+
args.flatten!
|
68
|
+
end
|
69
|
+
|
70
|
+
class << self
|
71
|
+
# Returns a new TagList using the given tag string.
|
72
|
+
#
|
73
|
+
# tag_list = TagList.from("One , Two, Three")
|
74
|
+
# tag_list # ["One", "Two", "Three"]
|
75
|
+
def from(string)
|
76
|
+
returning new do |tag_list|
|
77
|
+
string = string.to_s.dup
|
78
|
+
|
79
|
+
# Parse the quoted tags
|
80
|
+
string.gsub!(/"(.*?)"\s*#{delimiter}?\s*/) { tag_list << $1; "" }
|
81
|
+
string.gsub!(/'(.*?)'\s*#{delimiter}?\s*/) { tag_list << $1; "" }
|
82
|
+
|
83
|
+
tag_list.add(string.split(delimiter))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def from_owner(owner, *tags)
|
88
|
+
returning from(*tags) do |taglist|
|
89
|
+
taglist.owner = owner
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module TagsHelper
|
2
|
+
# See the README for an example using tag_cloud.
|
3
|
+
def tag_cloud(tags, classes)
|
4
|
+
max_count = tags.sort_by(&:count).last.count.to_f
|
5
|
+
|
6
|
+
tags.each do |tag|
|
7
|
+
index = ((tag.count / max_count) * (classes.size - 1)).round
|
8
|
+
yield tag, classes[index]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe "acts_as_taggable_on" do
|
4
|
+
context "Taggable Method Generation" do
|
5
|
+
before(:each) do
|
6
|
+
@taggable = TaggableModel.new(:name => "Bob Jones")
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should create a class attribute for tag types" do
|
10
|
+
@taggable.class.should respond_to(:tag_types)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should generate an association for each tag type" do
|
14
|
+
@taggable.should respond_to(:tags, :skills, :languages)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should generate a cached column checker for each tag type" do
|
18
|
+
TaggableModel.should respond_to(:caching_tag_list?, :caching_skill_list?, :caching_language_list?)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should add tagged_with and tag_counts to singleton" do
|
22
|
+
TaggableModel.should respond_to(:find_tagged_with, :tag_counts)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should add saving of tag lists and cached tag lists to the instance" do
|
26
|
+
@taggable.should respond_to(:save_cached_tag_list)
|
27
|
+
@taggable.should respond_to(:save_tags)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should generate a tag_list accessor/setter for each tag type" do
|
31
|
+
@taggable.should respond_to(:tag_list, :skill_list, :language_list)
|
32
|
+
@taggable.should respond_to(:tag_list=, :skill_list=, :language_list=)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context "inheritance" do
|
37
|
+
before do
|
38
|
+
@taggable = TaggableModel.new(:name => "taggable")
|
39
|
+
@inherited_same = InheritingTaggableModel.new(:name => "inherited same")
|
40
|
+
@inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should pass on tag contexts to STI-inherited models" do
|
44
|
+
@inherited_same.should respond_to(:tag_list, :skill_list, :language_list)
|
45
|
+
@inherited_different.should respond_to(:tag_list, :skill_list, :language_list)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should have tag contexts added in altered STI models" do
|
49
|
+
@inherited_different.should respond_to(:part_list)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "reloading" do
|
54
|
+
it "should save a model instantiated by Model.find" do
|
55
|
+
taggable = TaggableModel.create!(:name => "Taggable")
|
56
|
+
found_taggable = TaggableModel.find(taggable.id)
|
57
|
+
found_taggable.save
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context "related" do
|
62
|
+
it "should find related objects based on tag names on context" do
|
63
|
+
taggable1 = TaggableModel.create!(:name => "Taggable 1")
|
64
|
+
taggable2 = TaggableModel.create!(:name => "Taggable 2")
|
65
|
+
taggable3 = TaggableModel.create!(:name => "Taggable 3")
|
66
|
+
|
67
|
+
taggable1.tag_list = "one, two"
|
68
|
+
taggable1.save
|
69
|
+
|
70
|
+
taggable2.tag_list = "three, four"
|
71
|
+
taggable2.save
|
72
|
+
|
73
|
+
taggable3.tag_list = "one, four"
|
74
|
+
taggable3.save
|
75
|
+
|
76
|
+
taggable1.find_related_tags.should include(taggable3)
|
77
|
+
taggable1.find_related_tags.should_not include(taggable2)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe TagList do
|
4
|
+
before(:each) do
|
5
|
+
@tag_list = TagList.new("awesome","radical")
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should be an array" do
|
9
|
+
@tag_list.is_a?(Array).should be_true
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should be able to be add a new tag word" do
|
13
|
+
@tag_list.add("cool")
|
14
|
+
@tag_list.include?("cool").should be_true
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should be able to add delimited lists of words" do
|
18
|
+
@tag_list.add("cool, wicked", :parse => true)
|
19
|
+
@tag_list.include?("cool").should be_true
|
20
|
+
@tag_list.include?("wicked").should be_true
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should be able to remove words" do
|
24
|
+
@tag_list.remove("awesome")
|
25
|
+
@tag_list.include?("awesome").should be_false
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should be able to remove delimited lists of words" do
|
29
|
+
@tag_list.remove("awesome, radical", :parse => true)
|
30
|
+
@tag_list.should be_empty
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should give a delimited list of words when converted to string" do
|
34
|
+
@tag_list.to_s.should == "awesome, radical"
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should quote escape tags with commas in them" do
|
38
|
+
@tag_list.add("cool","rad,bodacious")
|
39
|
+
@tag_list.to_s.should == "awesome, radical, cool, \"rad,bodacious\""
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe Tag do
|
4
|
+
before(:each) do
|
5
|
+
@tag = Tag.new
|
6
|
+
@user = TaggableModel.create(:name => "Pablo")
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should require a name" do
|
10
|
+
@tag.should have(1).errors_on(:name)
|
11
|
+
@tag.name = "something"
|
12
|
+
@tag.should have(0).errors_on(:name)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should equal a tag with the same name" do
|
16
|
+
@tag.name = "awesome"
|
17
|
+
new_tag = Tag.new(:name => "awesome")
|
18
|
+
new_tag.should == @tag
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should return its name when to_s is called" do
|
22
|
+
@tag.name = "cool"
|
23
|
+
@tag.to_s.should == "cool"
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe "Taggable" do
|
4
|
+
before(:each) do
|
5
|
+
@taggable = TaggableModel.new(:name => "Bob Jones")
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should be able to create tags" do
|
9
|
+
@taggable.skill_list = "ruby, rails, css"
|
10
|
+
@taggable.instance_variable_get("@skill_list").instance_of?(TagList).should be_true
|
11
|
+
@taggable.save
|
12
|
+
|
13
|
+
Tag.find(:all).size.should == 3
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should differentiate between contexts" do
|
17
|
+
@taggable.skill_list = "ruby, rails, css"
|
18
|
+
@taggable.tag_list = "ruby, bob, charlie"
|
19
|
+
@taggable.save
|
20
|
+
@taggable.reload
|
21
|
+
@taggable.skill_list.include?("ruby").should be_true
|
22
|
+
@taggable.skill_list.include?("bob").should be_false
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should be able to remove tags through list alone" do
|
26
|
+
@taggable.skill_list = "ruby, rails, css"
|
27
|
+
@taggable.save
|
28
|
+
@taggable.reload
|
29
|
+
@taggable.should have(3).skills
|
30
|
+
@taggable.skill_list = "ruby, rails"
|
31
|
+
@taggable.save
|
32
|
+
@taggable.reload
|
33
|
+
@taggable.should have(2).skills
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should be able to find by tag" do
|
37
|
+
@taggable.skill_list = "ruby, rails, css"
|
38
|
+
@taggable.save
|
39
|
+
TaggableModel.find_tagged_with("ruby").first.should == @taggable
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should be able to find by tag with context" do
|
43
|
+
@taggable.skill_list = "ruby, rails, css"
|
44
|
+
@taggable.tag_list = "bob, charlie"
|
45
|
+
@taggable.save
|
46
|
+
TaggableModel.find_tagged_with("ruby").first.should == @taggable
|
47
|
+
TaggableModel.find_tagged_with("bob", :on => :skills).first.should_not == @taggable
|
48
|
+
TaggableModel.find_tagged_with("bob", :on => :tags).first.should == @taggable
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should not care about case" do
|
52
|
+
bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby")
|
53
|
+
frank = TaggableModel.create(:name => "Frank", :tag_list => "Ruby")
|
54
|
+
|
55
|
+
Tag.find(:all).size.should == 1
|
56
|
+
TaggableModel.find_tagged_with("ruby").should == TaggableModel.find_tagged_with("Ruby")
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should be able to get tag counts on model as a whole" do
|
60
|
+
bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
|
61
|
+
frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
|
62
|
+
charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
|
63
|
+
TaggableModel.tag_counts.should_not be_empty
|
64
|
+
TaggableModel.skill_counts.should_not be_empty
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should be able to get tag counts on an association" do
|
68
|
+
bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
|
69
|
+
frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
|
70
|
+
charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
|
71
|
+
bob.tag_counts.first.count.should == 2
|
72
|
+
charlie.skill_counts.first.count.should == 1
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should be able to set a custom tag context list" do
|
76
|
+
bob = TaggableModel.create(:name => "Bob")
|
77
|
+
bob.set_tag_list_on(:rotors, "spinning, jumping")
|
78
|
+
bob.tag_list_on(:rotors).should == ["spinning","jumping"]
|
79
|
+
bob.save
|
80
|
+
bob.reload
|
81
|
+
bob.tags_on(:rotors).should_not be_empty
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should be able to find tagged on a custom tag context" do
|
85
|
+
bob = TaggableModel.create(:name => "Bob")
|
86
|
+
bob.set_tag_list_on(:rotors, "spinning, jumping")
|
87
|
+
bob.tag_list_on(:rotors).should == ["spinning","jumping"]
|
88
|
+
bob.save
|
89
|
+
TaggableModel.find_tagged_with("spinning", :on => :rotors).should_not be_empty
|
90
|
+
end
|
91
|
+
|
92
|
+
context "inheritance" do
|
93
|
+
before do
|
94
|
+
@taggable = TaggableModel.new(:name => "taggable")
|
95
|
+
@inherited_same = InheritingTaggableModel.new(:name => "inherited same")
|
96
|
+
@inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should be able to save tags for inherited models" do
|
100
|
+
@inherited_same.tag_list = "bob, kelso"
|
101
|
+
@inherited_same.save
|
102
|
+
InheritingTaggableModel.find_tagged_with("bob").first.should == @inherited_same
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should find STI tagged models on the superclass" do
|
106
|
+
@inherited_same.tag_list = "bob, kelso"
|
107
|
+
@inherited_same.save
|
108
|
+
TaggableModel.find_tagged_with("bob").first.should == @inherited_same
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should be able to add on contexts only to some subclasses" do
|
112
|
+
@inherited_different.part_list = "fork, spoon"
|
113
|
+
@inherited_different.save
|
114
|
+
InheritingTaggableModel.find_tagged_with("fork", :on => :parts).should be_empty
|
115
|
+
AlteredInheritingTaggableModel.find_tagged_with("fork", :on => :parts).first.should == @inherited_different
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe "Tagger" do
|
4
|
+
before(:each) do
|
5
|
+
@user = TaggableUser.new
|
6
|
+
@taggable = TaggableModel.new(:name => "Bob Jones")
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should have taggings" do
|
10
|
+
@user.tag(@taggable, :with=>'ruby,scheme', :on=>:tags)
|
11
|
+
@user.owned_taggings.size == 2
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should have tags" do
|
15
|
+
@user.tag(@taggable, :with=>'ruby,scheme', :on=>:tags)
|
16
|
+
@user.owned_tags.size == 2
|
17
|
+
end
|
18
|
+
|
19
|
+
it "is tagger" do
|
20
|
+
@user.is_tagger?.should(be_true)
|
21
|
+
end
|
22
|
+
end
|
data/spec/schema.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
ActiveRecord::Schema.define :version => 0 do
|
2
|
+
create_table :tags, :force => true do |t|
|
3
|
+
t.column :name, :string
|
4
|
+
end
|
5
|
+
|
6
|
+
create_table :taggings, :force => true do |t|
|
7
|
+
t.column :tag_id, :integer
|
8
|
+
t.column :taggable_id, :integer
|
9
|
+
t.column :taggable_type, :string
|
10
|
+
t.column :context, :string
|
11
|
+
t.column :created_at, :datetime
|
12
|
+
t.column :tagger_id, :integer
|
13
|
+
t.column :tagger_type, :string
|
14
|
+
end
|
15
|
+
|
16
|
+
create_table :taggable_models, :force => true do |t|
|
17
|
+
t.column :name, :string
|
18
|
+
t.column :type, :string
|
19
|
+
#t.column :cached_tag_list, :string
|
20
|
+
end
|
21
|
+
create_table :taggable_users, :force => true do |t|
|
22
|
+
t.column :name, :string
|
23
|
+
end
|
24
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../../../../spec/spec_helper'
|
2
|
+
|
3
|
+
plugin_spec_dir = File.dirname(__FILE__)
|
4
|
+
ActiveRecord::Base.logger = Logger.new(plugin_spec_dir + "/debug.log")
|
5
|
+
|
6
|
+
load(File.dirname(__FILE__) + '/schema.rb')
|
7
|
+
|
8
|
+
class TaggableModel < ActiveRecord::Base
|
9
|
+
acts_as_taggable_on :tags, :languages
|
10
|
+
acts_as_taggable_on :skills
|
11
|
+
end
|
12
|
+
|
13
|
+
class InheritingTaggableModel < TaggableModel
|
14
|
+
end
|
15
|
+
|
16
|
+
class AlteredInheritingTaggableModel < TaggableModel
|
17
|
+
acts_as_taggable_on :parts
|
18
|
+
end
|
19
|
+
|
20
|
+
class TaggableUser < ActiveRecord::Base
|
21
|
+
acts_as_tagger
|
22
|
+
end
|
data/uninstall.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Uninstall hook code here
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mbleigh-acts-as-taggable-on
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michael Bleigh
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-06-10 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Acts As Taggable On provides the ability to have multiple tag contexts on a single model in ActiveRecord. It also has support for tag clouds, related items, taggers, and more.
|
17
|
+
email: michael@intridea.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- CHANGELOG
|
26
|
+
- MIT-LICENSE
|
27
|
+
- README
|
28
|
+
- generators/acts_as_taggable_on_migration
|
29
|
+
- generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb
|
30
|
+
- generators/acts_as_taggable_on_migration/templates
|
31
|
+
- generators/acts_as_taggable_on_migration/templates/add_users_migration.rb
|
32
|
+
- generators/acts_as_taggable_on_migration/templates/migration.rb
|
33
|
+
- init.rb
|
34
|
+
- lib/acts-as-taggable-on.rb
|
35
|
+
- lib/acts_as_taggable_on/acts_as_taggable_on.rb
|
36
|
+
- lib/acts_as_taggable_on/acts_as_tagger.rb
|
37
|
+
- lib/acts_as_taggable_on/tag.rb
|
38
|
+
- lib/acts_as_taggable_on/tag_list.rb
|
39
|
+
- lib/acts_as_taggable_on/tagging.rb
|
40
|
+
- lib/acts_as_taggable_on/tags_helper.rb
|
41
|
+
- rails/init.rb
|
42
|
+
- spec/acts_as_taggable_on
|
43
|
+
- spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb
|
44
|
+
- spec/acts_as_taggable_on/tag_list_spec.rb
|
45
|
+
- spec/acts_as_taggable_on/tag_spec.rb
|
46
|
+
- spec/acts_as_taggable_on/taggable_spec.rb
|
47
|
+
- spec/acts_as_taggable_on/tagger_spec.rb
|
48
|
+
- spec/acts_as_taggable_on/tagging_spec.rb
|
49
|
+
- spec/debug.log
|
50
|
+
- spec/schema.rb
|
51
|
+
- spec/spec_helper.rb
|
52
|
+
- uninstall.rb
|
53
|
+
has_rdoc: false
|
54
|
+
homepage: http://www.actsascommunity.com/projects/acts-as-taggable-on
|
55
|
+
post_install_message:
|
56
|
+
rdoc_options: []
|
57
|
+
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: "0"
|
71
|
+
version:
|
72
|
+
requirements: []
|
73
|
+
|
74
|
+
rubyforge_project:
|
75
|
+
rubygems_version: 1.0.1
|
76
|
+
signing_key:
|
77
|
+
specification_version: 2
|
78
|
+
summary: Tagging for ActiveRecord with custom contexts and advanced features.
|
79
|
+
test_files: []
|
80
|
+
|