acts-as-taggable-on 2.2.0 → 2.3.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 +3 -1
- data/README.rdoc +59 -7
- data/acts-as-taggable-on.gemspec +3 -3
- data/lib/acts-as-taggable-on/version.rb +1 -1
- data/lib/acts-as-taggable-on.rb +30 -3
- data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +7 -19
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +86 -21
- data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +37 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +33 -12
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +1 -1
- data/lib/acts_as_taggable_on/tag.rb +5 -8
- data/lib/acts_as_taggable_on/tag_list.rb +13 -12
- data/lib/acts_as_taggable_on/taggable.rb +102 -0
- data/lib/acts_as_taggable_on/{acts_as_tagger.rb → tagger.rb} +3 -3
- data/lib/acts_as_taggable_on/tagging.rb +12 -0
- data/lib/acts_as_taggable_on/tags_helper.rb +2 -2
- data/lib/acts_as_taggable_on/utils.rb +4 -0
- data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +3 -1
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +58 -0
- data/spec/acts_as_taggable_on/tag_list_spec.rb +84 -65
- data/spec/acts_as_taggable_on/tag_spec.rb +32 -10
- data/spec/acts_as_taggable_on/taggable_spec.rb +147 -14
- data/spec/acts_as_taggable_on/tagger_spec.rb +23 -16
- data/spec/acts_as_taggable_on/tagging_spec.rb +2 -5
- data/spec/acts_as_taggable_on/tags_helper_spec.rb +16 -0
- data/spec/models.rb +5 -0
- data/spec/schema.rb +5 -0
- data/spec/spec_helper.rb +3 -1
- metadata +26 -25
- data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +0 -54
data/.gitignore
CHANGED
data/README.rdoc
CHANGED
|
@@ -16,17 +16,15 @@ was used.
|
|
|
16
16
|
|
|
17
17
|
== Installation
|
|
18
18
|
|
|
19
|
-
=== Rails 2.
|
|
19
|
+
=== Rails 2.x
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
gem 'acts-as-taggable-on', '2.1.0'
|
|
21
|
+
Not supported any more! It is time for update guys.
|
|
24
22
|
|
|
25
23
|
=== Rails 3.x
|
|
26
24
|
|
|
27
25
|
To use it, add it to your Gemfile:
|
|
28
26
|
|
|
29
|
-
gem 'acts-as-taggable-on'
|
|
27
|
+
gem 'acts-as-taggable-on', '~> 2.2.2'
|
|
30
28
|
|
|
31
29
|
==== Post Installation
|
|
32
30
|
|
|
@@ -61,6 +59,25 @@ directory, you can run the specs for RoR 3.x with:
|
|
|
61
59
|
User.skill_counts # => [<Tag name="joking" count=2>,<Tag name="clowning" count=1>...]
|
|
62
60
|
@frankie.skill_counts
|
|
63
61
|
|
|
62
|
+
To preserve the order in which tags are created use acts_as_ordered_taggable:
|
|
63
|
+
|
|
64
|
+
class User < ActiveRecord::Base
|
|
65
|
+
# Alias for <tt>acts_as_ordered_taggable_on :tags</tt>:
|
|
66
|
+
acts_as_ordered_taggable
|
|
67
|
+
acts_as_ordered_taggable_on :skills, :interests
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@user = User.new(:name => "Bobby")
|
|
71
|
+
@user.tag_list = "east, south"
|
|
72
|
+
@user.save
|
|
73
|
+
|
|
74
|
+
@user.tag_list = "north, east, south, west"
|
|
75
|
+
@user.save
|
|
76
|
+
|
|
77
|
+
@user.reload
|
|
78
|
+
@user.tag_list # => ["north", "east", "south", "west"]
|
|
79
|
+
|
|
80
|
+
|
|
64
81
|
=== Finding Tagged Objects
|
|
65
82
|
|
|
66
83
|
Acts As Taggable On utilizes named_scopes to create an association for tags.
|
|
@@ -76,15 +93,20 @@ compatibility with the will_paginate gem:
|
|
|
76
93
|
User.tagged_with("awesome").by_date.paginate(:page => params[:page], :per_page => 20)
|
|
77
94
|
|
|
78
95
|
# Find a user with matching all tags, not just one
|
|
79
|
-
User.tagged_with(["awesome", "cool"], :match_all =>
|
|
96
|
+
User.tagged_with(["awesome", "cool"], :match_all => true)
|
|
80
97
|
|
|
81
98
|
# Find a user with any of the tags:
|
|
82
99
|
User.tagged_with(["awesome", "cool"], :any => true)
|
|
83
100
|
|
|
101
|
+
# Find a user that not tags with awesome or cool:
|
|
102
|
+
User.tagged_with(["awesome", "cool"], :exclude => true)
|
|
103
|
+
|
|
84
104
|
# Find a user with any of tags based on context:
|
|
85
105
|
User.tagged_with(['awesome, cool'], :on => :tags, :any => true).tagged_with(['smart', 'shy'], :on => :skills, :any => true)
|
|
86
106
|
|
|
87
|
-
|
|
107
|
+
You can also use :wild => true option along with :any or :exclude option. It will looking for %awesome% and %cool% in sql.
|
|
108
|
+
|
|
109
|
+
Tip: User.tagged_with([]) or '' will return [], but not all records.
|
|
88
110
|
|
|
89
111
|
=== Relationships
|
|
90
112
|
|
|
@@ -136,6 +158,21 @@ Tags can have owners:
|
|
|
136
158
|
@some_photo.locations_from(@some_user) # => ["paris", "normandy"]
|
|
137
159
|
@some_photo.owner_tags_on(@some_user, :locations) # => [#<ActsAsTaggableOn::Tag id: 1, name: "paris">...]
|
|
138
160
|
@some_photo.owner_tags_on(nil, :locations) # => Ownerships equivalent to saying @some_photo.locations
|
|
161
|
+
@some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations, :skip_save => true) #won't save @some_photo object
|
|
162
|
+
|
|
163
|
+
=== Dirty objects
|
|
164
|
+
|
|
165
|
+
@bobby = User.find_by_name("Bobby")
|
|
166
|
+
@bobby.skill_list # => ["jogging", "diving"]
|
|
167
|
+
|
|
168
|
+
@boddy.skill_list_changed? #=> false
|
|
169
|
+
@boddy.changes #=> {}
|
|
170
|
+
|
|
171
|
+
@bobby.skill_list = "swimming"
|
|
172
|
+
@bobby.changes.should == {"skill_list"=>["jogging, diving", ["swimming"]]}
|
|
173
|
+
@boddy.skill_list_changed? #=> true
|
|
174
|
+
|
|
175
|
+
@bobby.skill_list_change.should == ["jogging, diving", ["swimming"]]
|
|
139
176
|
|
|
140
177
|
=== Tag cloud calculations
|
|
141
178
|
|
|
@@ -177,6 +214,21 @@ CSS:
|
|
|
177
214
|
.css3 { font-size: 1.4em; }
|
|
178
215
|
.css4 { font-size: 1.6em; }
|
|
179
216
|
|
|
217
|
+
== Configuration
|
|
218
|
+
|
|
219
|
+
If you would like to remove unused tag objects after removing taggings, add
|
|
220
|
+
|
|
221
|
+
ActsAsTaggableOn.remove_unused_tags = true
|
|
222
|
+
|
|
223
|
+
If you want force tags to be saved downcased:
|
|
224
|
+
|
|
225
|
+
ActsAsTaggableOn.force_lowercase = true
|
|
226
|
+
|
|
227
|
+
If you want tags to be saved parametrized (you can redefine to_param as well):
|
|
228
|
+
|
|
229
|
+
ActsAsTaggableOn.force_parameterize = true
|
|
230
|
+
|
|
231
|
+
|
|
180
232
|
== Contributors
|
|
181
233
|
|
|
182
234
|
We have a long list of valued contributors. {Check them all}[https://github.com/mbleigh/acts-as-taggable-on/contributors]
|
data/acts-as-taggable-on.gemspec
CHANGED
|
@@ -4,14 +4,14 @@ require 'acts-as-taggable-on/version'
|
|
|
4
4
|
Gem::Specification.new do |gem|
|
|
5
5
|
gem.name = %q{acts-as-taggable-on}
|
|
6
6
|
gem.authors = ["Michael Bleigh"]
|
|
7
|
-
gem.date = %q{
|
|
7
|
+
gem.date = %q{2012-01-06}
|
|
8
8
|
gem.description = %q{With ActsAsTaggableOn, you can tag a single model on several contexts, such as skills, interests, and awards. It also provides other advanced functionality.}
|
|
9
9
|
gem.summary = "Advanced tagging for Rails."
|
|
10
10
|
gem.email = %q{michael@intridea.com}
|
|
11
11
|
gem.homepage = ''
|
|
12
12
|
|
|
13
|
-
gem.add_runtime_dependency 'rails', '~> 3.
|
|
14
|
-
gem.add_development_dependency 'rspec', '~> 2.
|
|
13
|
+
gem.add_runtime_dependency 'rails', '~> 3.0'
|
|
14
|
+
gem.add_development_dependency 'rspec', '~> 2.6'
|
|
15
15
|
gem.add_development_dependency 'ammeter', '~> 0.1.3'
|
|
16
16
|
gem.add_development_dependency 'sqlite3'
|
|
17
17
|
gem.add_development_dependency 'mysql2', '~> 0.3.7'
|
data/lib/acts-as-taggable-on.rb
CHANGED
|
@@ -2,18 +2,44 @@ require "active_record"
|
|
|
2
2
|
require "active_record/version"
|
|
3
3
|
require "action_view"
|
|
4
4
|
|
|
5
|
+
require "digest/sha1"
|
|
6
|
+
|
|
5
7
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
|
6
8
|
|
|
9
|
+
module ActsAsTaggableOn
|
|
10
|
+
mattr_accessor :delimiter
|
|
11
|
+
@@delimiter = ','
|
|
12
|
+
|
|
13
|
+
mattr_accessor :force_lowercase
|
|
14
|
+
@@force_lowercase = false
|
|
15
|
+
|
|
16
|
+
mattr_accessor :force_parameterize
|
|
17
|
+
@@force_parameterize = false
|
|
18
|
+
|
|
19
|
+
mattr_accessor :remove_unused_tags
|
|
20
|
+
self.remove_unused_tags = false
|
|
21
|
+
|
|
22
|
+
def self.glue
|
|
23
|
+
@@delimiter.ends_with?(" ") ? @@delimiter : "#{@@delimiter} "
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.setup
|
|
27
|
+
yield self
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
|
|
7
32
|
require "acts_as_taggable_on/utils"
|
|
8
33
|
|
|
9
|
-
require "acts_as_taggable_on/
|
|
34
|
+
require "acts_as_taggable_on/taggable"
|
|
10
35
|
require "acts_as_taggable_on/acts_as_taggable_on/core"
|
|
11
36
|
require "acts_as_taggable_on/acts_as_taggable_on/collection"
|
|
12
37
|
require "acts_as_taggable_on/acts_as_taggable_on/cache"
|
|
13
38
|
require "acts_as_taggable_on/acts_as_taggable_on/ownership"
|
|
14
39
|
require "acts_as_taggable_on/acts_as_taggable_on/related"
|
|
40
|
+
require "acts_as_taggable_on/acts_as_taggable_on/dirty"
|
|
15
41
|
|
|
16
|
-
require "acts_as_taggable_on/
|
|
42
|
+
require "acts_as_taggable_on/tagger"
|
|
17
43
|
require "acts_as_taggable_on/tag"
|
|
18
44
|
require "acts_as_taggable_on/tag_list"
|
|
19
45
|
require "acts_as_taggable_on/tags_helper"
|
|
@@ -29,4 +55,5 @@ end
|
|
|
29
55
|
|
|
30
56
|
if defined?(ActionView::Base)
|
|
31
57
|
ActionView::Base.send :include, ActsAsTaggableOn::TagsHelper
|
|
32
|
-
end
|
|
58
|
+
end
|
|
59
|
+
|
|
@@ -53,11 +53,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
53
53
|
def all_tag_counts(options = {})
|
|
54
54
|
options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
|
|
55
55
|
|
|
56
|
-
scope =
|
|
57
|
-
{}
|
|
58
|
-
else
|
|
59
|
-
scope(:find) || {}
|
|
60
|
-
end
|
|
56
|
+
scope = {}
|
|
61
57
|
|
|
62
58
|
## Generate conditions:
|
|
63
59
|
options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
|
|
@@ -92,8 +88,6 @@ module ActsAsTaggableOn::Taggable
|
|
|
92
88
|
tag_joins = [
|
|
93
89
|
].compact
|
|
94
90
|
|
|
95
|
-
[tagging_joins, tag_joins].each(&:reverse!) if ActiveRecord::VERSION::MAJOR < 3
|
|
96
|
-
|
|
97
91
|
## Generate scope:
|
|
98
92
|
tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count")
|
|
99
93
|
tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
|
|
@@ -112,18 +106,12 @@ module ActsAsTaggableOn::Taggable
|
|
|
112
106
|
|
|
113
107
|
group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
|
|
114
108
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
else
|
|
122
|
-
# Having is not available in 2.3.x:
|
|
123
|
-
group_by = "#{group_columns} HAVING COUNT(*) > 0"
|
|
124
|
-
group_by << " AND #{having}" unless having.blank?
|
|
125
|
-
tagging_scope = tagging_scope.group(group_by)
|
|
126
|
-
end
|
|
109
|
+
# Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
|
|
110
|
+
scoped_select = "#{table_name}.#{primary_key}"
|
|
111
|
+
tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{select(scoped_select).to_sql})").
|
|
112
|
+
group(group_columns).
|
|
113
|
+
having(having)
|
|
114
|
+
|
|
127
115
|
|
|
128
116
|
tag_scope = tag_scope.joins("JOIN (#{tagging_scope.to_sql}) AS #{ActsAsTaggableOn::Tagging.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id")
|
|
129
117
|
tag_scope
|
|
@@ -18,11 +18,22 @@ module ActsAsTaggableOn::Taggable
|
|
|
18
18
|
tag_type = tags_type.to_s.singularize
|
|
19
19
|
context_taggings = "#{tag_type}_taggings".to_sym
|
|
20
20
|
context_tags = tags_type.to_sym
|
|
21
|
-
|
|
21
|
+
taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" : nil)
|
|
22
|
+
|
|
22
23
|
class_eval do
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
# when preserving tag order, include order option so that for a 'tags' context
|
|
25
|
+
# the associations tag_taggings & tags are always returned in created order
|
|
26
|
+
has_many context_taggings, :as => :taggable,
|
|
27
|
+
:dependent => :destroy,
|
|
28
|
+
:include => :tag,
|
|
29
|
+
:class_name => "ActsAsTaggableOn::Tagging",
|
|
30
|
+
:conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_type],
|
|
31
|
+
:order => taggings_order
|
|
32
|
+
|
|
33
|
+
has_many context_tags, :through => context_taggings,
|
|
34
|
+
:source => :tag,
|
|
35
|
+
:class_name => "ActsAsTaggableOn::Tag",
|
|
36
|
+
:order => taggings_order
|
|
26
37
|
end
|
|
27
38
|
|
|
28
39
|
class_eval %(
|
|
@@ -41,11 +52,11 @@ module ActsAsTaggableOn::Taggable
|
|
|
41
52
|
end
|
|
42
53
|
end
|
|
43
54
|
|
|
44
|
-
def
|
|
45
|
-
super(*
|
|
55
|
+
def taggable_on(preserve_tag_order, *tag_types)
|
|
56
|
+
super(preserve_tag_order, *tag_types)
|
|
46
57
|
initialize_acts_as_taggable_on_core
|
|
47
58
|
end
|
|
48
|
-
|
|
59
|
+
|
|
49
60
|
# all column names are necessary for PostgreSQL group clause
|
|
50
61
|
def grouped_column_names_for(object)
|
|
51
62
|
object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
|
|
@@ -81,20 +92,31 @@ module ActsAsTaggableOn::Taggable
|
|
|
81
92
|
alias_base_name = undecorated_table_name.gsub('.','_')
|
|
82
93
|
|
|
83
94
|
if options.delete(:exclude)
|
|
84
|
-
|
|
95
|
+
if options.delete(:wild)
|
|
96
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ? ESCAPE '!'", "%#{escape_like(t)}%"]) }.join(" OR ")
|
|
97
|
+
else
|
|
98
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
|
|
99
|
+
end
|
|
100
|
+
|
|
85
101
|
conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{ActsAsTaggableOn::Tagging.table_name}.taggable_id FROM #{ActsAsTaggableOn::Tagging.table_name} JOIN #{ActsAsTaggableOn::Tag.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
|
|
86
102
|
|
|
87
103
|
elsif options.delete(:any)
|
|
88
104
|
# get tags, drop out if nothing returned (we need at least one)
|
|
89
|
-
|
|
105
|
+
if options.delete(:wild)
|
|
106
|
+
tags = ActsAsTaggableOn::Tag.named_like_any(tag_list)
|
|
107
|
+
else
|
|
108
|
+
tags = ActsAsTaggableOn::Tag.named_any(tag_list)
|
|
109
|
+
end
|
|
110
|
+
|
|
90
111
|
return scoped(:conditions => "1 = 0") unless tags.length > 0
|
|
91
112
|
|
|
92
113
|
# setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
|
|
93
114
|
# avoid ambiguous column name
|
|
94
115
|
taggings_context = context ? "_#{context}" : ''
|
|
95
116
|
|
|
96
|
-
|
|
97
|
-
|
|
117
|
+
taggings_alias = adjust_taggings_alias(
|
|
118
|
+
"#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:name).join('_'))}"
|
|
119
|
+
)
|
|
98
120
|
|
|
99
121
|
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
|
100
122
|
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
|
@@ -112,9 +134,8 @@ module ActsAsTaggableOn::Taggable
|
|
|
112
134
|
return empty_result unless tags.length == tag_list.length
|
|
113
135
|
|
|
114
136
|
tags.each do |tag|
|
|
115
|
-
prefix = "#{tag.safe_name}_#{rand(1024)}"
|
|
116
137
|
|
|
117
|
-
taggings_alias = "#{alias_base_name}_taggings_#{
|
|
138
|
+
taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}")
|
|
118
139
|
|
|
119
140
|
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
|
120
141
|
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
|
@@ -135,7 +156,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
135
156
|
end
|
|
136
157
|
end
|
|
137
158
|
|
|
138
|
-
taggings_alias, tags_alias = "#{alias_base_name}_taggings_group", "#{alias_base_name}_tags_group"
|
|
159
|
+
taggings_alias, tags_alias = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
|
|
139
160
|
|
|
140
161
|
if options.delete(:match_all)
|
|
141
162
|
joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
|
@@ -158,6 +179,13 @@ module ActsAsTaggableOn::Taggable
|
|
|
158
179
|
def is_taggable?
|
|
159
180
|
true
|
|
160
181
|
end
|
|
182
|
+
|
|
183
|
+
def adjust_taggings_alias(taggings_alias)
|
|
184
|
+
if taggings_alias.size > 75
|
|
185
|
+
taggings_alias = 'taggings_alias_' + Digest::SHA1.hexdigest(taggings_alias)
|
|
186
|
+
end
|
|
187
|
+
taggings_alias
|
|
188
|
+
end
|
|
161
189
|
end
|
|
162
190
|
|
|
163
191
|
module InstanceMethods
|
|
@@ -226,13 +254,19 @@ module ActsAsTaggableOn::Taggable
|
|
|
226
254
|
##
|
|
227
255
|
# Returns all tags that are not owned of a given context
|
|
228
256
|
def tags_on(context)
|
|
229
|
-
base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s])
|
|
257
|
+
scope = base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s])
|
|
258
|
+
# when preserving tag order, return tags in created order
|
|
259
|
+
# if we added the order to the association this would always apply
|
|
260
|
+
scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
|
|
261
|
+
scope.all
|
|
230
262
|
end
|
|
231
263
|
|
|
232
264
|
def set_tag_list_on(context, new_list)
|
|
233
265
|
add_custom_context(context)
|
|
234
266
|
|
|
235
267
|
variable_name = "@#{context.to_s.singularize}_list"
|
|
268
|
+
process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
|
|
269
|
+
|
|
236
270
|
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
|
|
237
271
|
end
|
|
238
272
|
|
|
@@ -240,6 +274,20 @@ module ActsAsTaggableOn::Taggable
|
|
|
240
274
|
custom_contexts + self.class.tag_types.map(&:to_s)
|
|
241
275
|
end
|
|
242
276
|
|
|
277
|
+
def process_dirty_object(context,new_list)
|
|
278
|
+
value = new_list.is_a?(Array) ? new_list.join(', ') : new_list
|
|
279
|
+
attrib = "#{context.to_s.singularize}_list"
|
|
280
|
+
|
|
281
|
+
if changed_attributes.include?(attrib)
|
|
282
|
+
# The attribute already has an unsaved change.
|
|
283
|
+
old = changed_attributes[attrib]
|
|
284
|
+
changed_attributes.delete(attrib) if (old.to_s == value.to_s)
|
|
285
|
+
else
|
|
286
|
+
old = tag_list_on(context).to_s
|
|
287
|
+
changed_attributes[attrib] = old if (old.to_s != value.to_s)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
243
291
|
def reload(*args)
|
|
244
292
|
self.class.tag_types.each do |context|
|
|
245
293
|
instance_variable_set("@#{context.to_s.singularize}_list", nil)
|
|
@@ -253,21 +301,38 @@ module ActsAsTaggableOn::Taggable
|
|
|
253
301
|
tagging_contexts.each do |context|
|
|
254
302
|
next unless tag_list_cache_set_on(context)
|
|
255
303
|
|
|
304
|
+
# List of currently assigned tag names
|
|
256
305
|
tag_list = tag_list_cache_on(context).uniq
|
|
257
306
|
|
|
258
307
|
# Find existing tags or create non-existing tags:
|
|
259
|
-
|
|
308
|
+
tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
|
|
260
309
|
|
|
310
|
+
# Tag objects for currently assigned tags
|
|
261
311
|
current_tags = tags_on(context)
|
|
262
|
-
|
|
263
|
-
|
|
312
|
+
|
|
313
|
+
# Tag maintenance based on whether preserving the created order of tags
|
|
314
|
+
if self.class.preserve_tag_order?
|
|
315
|
+
# First off order the array of tag objects to match the tag list
|
|
316
|
+
# rather than existing tags followed by new tags
|
|
317
|
+
tags = tag_list.map{|l| tags.detect{|t| t.name.downcase == l.downcase}}
|
|
318
|
+
# To preserve tags in the order in which they were added
|
|
319
|
+
# delete all current tags and create new tags if the content or order has changed
|
|
320
|
+
old_tags = (tags == current_tags ? [] : current_tags)
|
|
321
|
+
new_tags = (tags == current_tags ? [] : tags)
|
|
322
|
+
else
|
|
323
|
+
# Delete discarded tags and create new tags
|
|
324
|
+
old_tags = current_tags - tags
|
|
325
|
+
new_tags = tags - current_tags
|
|
326
|
+
end
|
|
264
327
|
|
|
265
328
|
# Find taggings to remove:
|
|
266
|
-
|
|
267
|
-
|
|
329
|
+
if old_tags.present?
|
|
330
|
+
old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
|
|
331
|
+
:context => context.to_s, :tag_id => old_tags).all
|
|
332
|
+
end
|
|
268
333
|
|
|
334
|
+
# Destroy old taggings:
|
|
269
335
|
if old_taggings.present?
|
|
270
|
-
# Destroy old taggings:
|
|
271
336
|
ActsAsTaggableOn::Tagging.destroy_all "#{ActsAsTaggableOn::Tagging.primary_key}".to_sym => old_taggings.map(&:id)
|
|
272
337
|
end
|
|
273
338
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module ActsAsTaggableOn::Taggable
|
|
2
|
+
module Dirty
|
|
3
|
+
def self.included(base)
|
|
4
|
+
base.extend ActsAsTaggableOn::Taggable::Dirty::ClassMethods
|
|
5
|
+
|
|
6
|
+
base.initialize_acts_as_taggable_on_dirty
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
def initialize_acts_as_taggable_on_dirty
|
|
11
|
+
tag_types.map(&:to_s).each do |tags_type|
|
|
12
|
+
tag_type = tags_type.to_s.singularize
|
|
13
|
+
context_tags = tags_type.to_sym
|
|
14
|
+
|
|
15
|
+
class_eval %(
|
|
16
|
+
def #{tag_type}_list_changed?
|
|
17
|
+
changed_attributes.include?("#{tag_type}_list")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def #{tag_type}_list_was
|
|
21
|
+
changed_attributes.include?("#{tag_type}_list") ? changed_attributes["#{tag_type}_list"] : __send__("#{tag_type}_list")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def #{tag_type}_list_change
|
|
25
|
+
[changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def #{tag_type}_list_changes
|
|
29
|
+
[changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
|
|
30
|
+
end
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -31,12 +31,16 @@ module ActsAsTaggableOn::Taggable
|
|
|
31
31
|
module InstanceMethods
|
|
32
32
|
def owner_tags_on(owner, context)
|
|
33
33
|
if owner.nil?
|
|
34
|
-
base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s])
|
|
34
|
+
scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s])
|
|
35
35
|
else
|
|
36
|
-
base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
|
|
37
|
+
#{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
|
|
38
|
+
#{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.to_s])
|
|
39
39
|
end
|
|
40
|
+
# when preserving tag order, return tags in created order
|
|
41
|
+
# if we added the order to the association this would always apply
|
|
42
|
+
scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
|
|
43
|
+
scope.all
|
|
40
44
|
end
|
|
41
45
|
|
|
42
46
|
def cached_owned_tag_list_on(context)
|
|
@@ -73,21 +77,38 @@ module ActsAsTaggableOn::Taggable
|
|
|
73
77
|
def save_owned_tags
|
|
74
78
|
tagging_contexts.each do |context|
|
|
75
79
|
cached_owned_tag_list_on(context).each do |owner, tag_list|
|
|
80
|
+
|
|
76
81
|
# Find existing tags or create non-existing tags:
|
|
77
|
-
|
|
82
|
+
tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
# Tag objects for owned tags
|
|
85
|
+
owned_tags = owner_tags_on(owner, context)
|
|
86
|
+
|
|
87
|
+
# Tag maintenance based on whether preserving the created order of tags
|
|
88
|
+
if self.class.preserve_tag_order?
|
|
89
|
+
# First off order the array of tag objects to match the tag list
|
|
90
|
+
# rather than existing tags followed by new tags
|
|
91
|
+
tags = tag_list.uniq.map{|s| tags.detect{|t| t.name.downcase == s.downcase}}
|
|
92
|
+
# To preserve tags in the order in which they were added
|
|
93
|
+
# delete all owned tags and create new tags if the content or order has changed
|
|
94
|
+
old_tags = (tags == owned_tags ? [] : owned_tags)
|
|
95
|
+
new_tags = (tags == owned_tags ? [] : tags)
|
|
96
|
+
else
|
|
97
|
+
# Delete discarded tags and create new tags
|
|
98
|
+
old_tags = owned_tags - tags
|
|
99
|
+
new_tags = tags - owned_tags
|
|
100
|
+
end
|
|
82
101
|
|
|
83
102
|
# Find all taggings that belong to the taggable (self), are owned by the owner,
|
|
84
103
|
# have the correct context, and are removed from the list.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
104
|
+
if old_tags.present?
|
|
105
|
+
old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
|
|
106
|
+
:tagger_type => owner.class.to_s, :tagger_id => owner.id,
|
|
107
|
+
:tag_id => old_tags, :context => context).all
|
|
108
|
+
end
|
|
88
109
|
|
|
110
|
+
# Destroy old taggings:
|
|
89
111
|
if old_taggings.present?
|
|
90
|
-
# Destroy old taggings:
|
|
91
112
|
ActsAsTaggableOn::Tagging.destroy_all(:id => old_taggings.map(&:id))
|
|
92
113
|
end
|
|
93
114
|
|
|
@@ -60,7 +60,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
60
60
|
|
|
61
61
|
exclude_self = "#{klass.table_name}.#{klass.primary_key} != #{id} AND" if [self.class.base_class, self.class].include? klass
|
|
62
62
|
|
|
63
|
-
group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
|
|
63
|
+
group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
|
|
64
64
|
|
|
65
65
|
klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count",
|
|
66
66
|
:from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
|
|
@@ -12,15 +12,16 @@ module ActsAsTaggableOn
|
|
|
12
12
|
|
|
13
13
|
validates_presence_of :name
|
|
14
14
|
validates_uniqueness_of :name
|
|
15
|
+
validates_length_of :name, :maximum => 255
|
|
15
16
|
|
|
16
17
|
### SCOPES:
|
|
17
18
|
|
|
18
19
|
def self.named(name)
|
|
19
|
-
where(["name
|
|
20
|
+
where(["lower(name) = ?", name.downcase])
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def self.named_any(list)
|
|
23
|
-
where(list.map { |tag| sanitize_sql(["name
|
|
24
|
+
where(list.map { |tag| sanitize_sql(["lower(name) = ?", tag.to_s.downcase]) }.join(" OR "))
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def self.named_like(name)
|
|
@@ -66,15 +67,11 @@ module ActsAsTaggableOn
|
|
|
66
67
|
read_attribute(:count).to_i
|
|
67
68
|
end
|
|
68
69
|
|
|
69
|
-
def safe_name
|
|
70
|
-
name.gsub(/[^a-zA-Z0-9]/, '')
|
|
71
|
-
end
|
|
72
|
-
|
|
73
70
|
class << self
|
|
74
71
|
private
|
|
75
72
|
def comparable_name(str)
|
|
76
|
-
|
|
73
|
+
str.mb_chars.downcase.to_s
|
|
77
74
|
end
|
|
78
75
|
end
|
|
79
76
|
end
|
|
80
|
-
end
|
|
77
|
+
end
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
|
2
|
+
|
|
1
3
|
module ActsAsTaggableOn
|
|
2
4
|
class TagList < Array
|
|
3
|
-
cattr_accessor :delimiter
|
|
4
|
-
self.delimiter = ','
|
|
5
|
-
|
|
6
5
|
attr_accessor :owner
|
|
7
6
|
|
|
8
7
|
def initialize(*args)
|
|
9
8
|
add(*args)
|
|
10
9
|
end
|
|
11
|
-
|
|
10
|
+
|
|
12
11
|
##
|
|
13
12
|
# Returns a new TagList using the given tag string.
|
|
14
13
|
#
|
|
@@ -16,17 +15,16 @@ module ActsAsTaggableOn
|
|
|
16
15
|
# tag_list = TagList.from("One , Two, Three")
|
|
17
16
|
# tag_list # ["One", "Two", "Three"]
|
|
18
17
|
def self.from(string)
|
|
19
|
-
|
|
20
|
-
string = string.join(glue) if string.respond_to?(:join)
|
|
18
|
+
string = string.join(ActsAsTaggableOn.glue) if string.respond_to?(:join)
|
|
21
19
|
|
|
22
20
|
new.tap do |tag_list|
|
|
23
21
|
string = string.to_s.dup
|
|
24
22
|
|
|
25
23
|
# Parse the quoted tags
|
|
26
|
-
string.gsub!(/(\A|#{delimiter})\s*"(.*?)"\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
|
27
|
-
string.gsub!(/(\A|#{delimiter})\s*'(.*?)'\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
|
24
|
+
string.gsub!(/(\A|#{ActsAsTaggableOn.delimiter})\s*"(.*?)"\s*(#{ActsAsTaggableOn.delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
|
25
|
+
string.gsub!(/(\A|#{ActsAsTaggableOn.delimiter})\s*'(.*?)'\s*(#{ActsAsTaggableOn.delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
|
28
26
|
|
|
29
|
-
tag_list.add(string.split(delimiter))
|
|
27
|
+
tag_list.add(string.split(ActsAsTaggableOn.delimiter))
|
|
30
28
|
end
|
|
31
29
|
end
|
|
32
30
|
|
|
@@ -69,16 +67,19 @@ module ActsAsTaggableOn
|
|
|
69
67
|
tags.send(:clean!)
|
|
70
68
|
|
|
71
69
|
tags.map do |name|
|
|
72
|
-
name.include?(delimiter) ? "\"#{name}\"" : name
|
|
73
|
-
end.join(
|
|
70
|
+
name.include?(ActsAsTaggableOn.delimiter) ? "\"#{name}\"" : name
|
|
71
|
+
end.join(ActsAsTaggableOn.glue)
|
|
74
72
|
end
|
|
75
73
|
|
|
76
74
|
private
|
|
77
|
-
|
|
75
|
+
|
|
78
76
|
# Remove whitespace, duplicates, and blanks.
|
|
79
77
|
def clean!
|
|
80
78
|
reject!(&:blank?)
|
|
81
79
|
map!(&:strip)
|
|
80
|
+
map!(&:downcase) if ActsAsTaggableOn.force_lowercase
|
|
81
|
+
map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
|
|
82
|
+
|
|
82
83
|
uniq!
|
|
83
84
|
end
|
|
84
85
|
|