yury-acts-as-taggable-on 1.0.3 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -19,9 +19,11 @@ module ActiveRecord
19
19
  args.compact! if args
20
20
  for tag_type in args
21
21
  tag_type = tag_type.to_s
22
+ # use aliased_join_table_name for context condition so that sphix can join multiple
23
+ # tag references from same model without getting an ambiguous column error
22
24
  self.class_eval do
23
25
  has_many "#{tag_type.singularize}_taggings".to_sym, :as => :taggable, :dependent => :destroy,
24
- :include => :tag, :conditions => ["#{Tagging.table_name}.context = ?",tag_type], :class_name => "Tagging"
26
+ :include => :tag, :conditions => ['#{aliased_join_table_name rescue "taggings"}.context = ?',tag_type], :class_name => "Tagging"
25
27
  has_many "#{tag_type}".to_sym, :through => "#{tag_type.singularize}_taggings".to_sym, :source => :tag
26
28
  end
27
29
 
@@ -45,6 +47,14 @@ module ActiveRecord
45
47
  def #{tag_type.singularize}_list=(new_tags)
46
48
  set_tag_list_on('#{tag_type}',new_tags)
47
49
  end
50
+
51
+ def #{tag_type.singularize}_list_changed=(changed)
52
+ instance_variable_set("@#{tag_type.singularize}_list_changed", changed)
53
+ end
54
+
55
+ def #{tag_type.singularize}_list_changed?
56
+ instance_variable_get("@#{tag_type.singularize}_list_changed") || false
57
+ end
48
58
 
49
59
  def #{tag_type.singularize}_counts(options = {})
50
60
  tag_counts_on('#{tag_type}',options)
@@ -183,7 +193,7 @@ module ActiveRecord
183
193
  conditions = merge_conditions(conditions, scope[:conditions]) if scope
184
194
 
185
195
  joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
186
- joins << sanitize_sql(["AND context = ?",options.delete(:on).to_s]) unless options[:on].nil?
196
+ joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
187
197
  joins << "LEFT OUTER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
188
198
  joins << scope[:joins] if scope && scope[:joins]
189
199
 
@@ -226,21 +236,27 @@ module ActiveRecord
226
236
  def tag_list_on(context, owner=nil)
227
237
  var_name = context.to_s.singularize + "_list"
228
238
  add_custom_context(context)
229
- return instance_variable_get("@#{var_name}") unless instance_variable_get("@#{var_name}").nil?
230
-
231
- if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
232
- instance_variable_set("@#{var_name}", TagList.from(self["cached_#{var_name}"]))
233
- else
234
- instance_variable_set("@#{var_name}", TagList.new(*tags_on(context, owner).map(&:name)))
239
+ list = instance_variable_get("@#{var_name}")
240
+
241
+ if list.nil?
242
+ if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
243
+ list = instance_variable_set("@#{var_name}", TagList.from(self["cached_#{var_name}"]))
244
+ else
245
+ list = instance_variable_set("@#{var_name}", TagList.new(*tags_on(context, owner).map(&:name)))
246
+ end
235
247
  end
248
+
249
+ list.context = context.to_s
250
+ list.taggable = self
251
+ list
236
252
  end
237
253
 
238
254
  def tags_on(context, owner=nil)
239
255
  if owner
240
- opts = {:conditions => ["#{Tagging.table_name}.context = ? AND tagger_id = ? AND tagger_type = ?",
256
+ opts = {:conditions => ["context = ? AND tagger_id = ? AND tagger_type = ?",
241
257
  context.to_s, owner.id, owner.class.to_s]}
242
258
  else
243
- opts = {:conditions => ["#{Tagging.table_name}.context = ?", context.to_s]}
259
+ opts = {:conditions => ["context = ?", context.to_s]}
244
260
  end
245
261
  base_tags.find(:all, opts)
246
262
  end
@@ -250,8 +266,13 @@ module ActiveRecord
250
266
  end
251
267
 
252
268
  def set_tag_list_on(context,new_list, tagger=nil)
269
+ old_list = instance_variable_get("@#{context.to_s.singularize}_list")
253
270
  instance_variable_set("@#{context.to_s.singularize}_list", TagList.from_owner(tagger, new_list))
254
271
  add_custom_context(context)
272
+
273
+ if old_list != instance_variable_get("@#{context.to_s.singularize}_list")
274
+ instance_variable_set("@#{context.to_s.singularize}_list_changed", true)
275
+ end
255
276
  end
256
277
 
257
278
  def tag_counts_on(context,options={})
@@ -266,10 +287,12 @@ module ActiveRecord
266
287
 
267
288
  def related_search_options(context, klass, options = {})
268
289
  tags_to_find = self.tags_on(context).collect { |t| t.name }
290
+
291
+ exclude_self = "#{klass.table_name}.id != #{self.id} AND" if self.class == klass
269
292
 
270
293
  { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
271
294
  :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
272
- :conditions => ["#{klass.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND #{Tag.table_name}.name IN (?)", tags_to_find],
295
+ :conditions => ["#{exclude_self} #{klass.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND #{Tag.table_name}.name IN (?)", tags_to_find],
273
296
  :group => "#{klass.table_name}.id",
274
297
  :order => "count DESC"
275
298
  }.update(options)
@@ -283,7 +306,7 @@ module ActiveRecord
283
306
  end
284
307
  end
285
308
 
286
- def save_tags
309
+ def save_tags
287
310
  (custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
288
311
  next unless instance_variable_get("@#{tag_type.singularize}_list")
289
312
  owner = instance_variable_get("@#{tag_type.singularize}_list").owner
@@ -298,6 +321,8 @@ module ActiveRecord
298
321
  :taggable => self, :tagger => owner)
299
322
  end
300
323
  end
324
+
325
+ instance_variable_set("@#{tag_type.singularize}_list_changed", false)
301
326
  end
302
327
 
303
328
  true
@@ -9,7 +9,7 @@ module ActiveRecord
9
9
  def acts_as_tagger(opts={})
10
10
  has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
11
11
  :include => :tag, :class_name => "Tagging")
12
- has_many :owned_tags, :through => :owned_taggings, :source => :tag
12
+ has_many :owned_tags, :through => :owned_taggings, :source => :tag, :uniq => true
13
13
  include ActiveRecord::Acts::Tagger::InstanceMethods
14
14
  extend ActiveRecord::Acts::Tagger::SingletonMethods
15
15
  end
@@ -49,4 +49,4 @@ module ActiveRecord
49
49
 
50
50
  end
51
51
  end
52
- end
52
+ end
@@ -1,12 +1,29 @@
1
- class Tag < ActiveRecord::Base
1
+ class Tag < ActiveRecord::Base
2
2
  has_many :taggings
3
3
 
4
4
  validates_presence_of :name
5
5
  validates_uniqueness_of :name
6
6
 
7
+ def self.cleanup(name)
8
+ n = name.to_s.downcase.gsub(/[^a-z0-9_-]+/, '').strip
9
+ n.blank? ? nil : n
10
+ end
11
+
12
+ def name=(name)
13
+ #strip any non-alphanumeric and downcase
14
+ self["name"] = Tag.cleanup(name)
15
+ end
16
+
7
17
  # LIKE is used for cross-database case-insensitivity
8
18
  def self.find_or_create_with_like_by_name(name)
9
- find(:first, :conditions => ["name LIKE ?", name]) || create(:name => name)
19
+ name = Tag.cleanup(name)
20
+
21
+ begin
22
+ find(:first, :conditions => ["name LIKE ?", name]) || create(:name => name)
23
+ rescue ActiveRecord::StatementInvalid
24
+ # If we can't insert the tag (perhaps a dupe), try to refetch.
25
+ find(:first, :conditions => ["name LIKE ?", name])
26
+ end
10
27
  end
11
28
 
12
29
  def ==(object)
@@ -17,7 +34,11 @@ class Tag < ActiveRecord::Base
17
34
  name
18
35
  end
19
36
 
20
- def count
37
+ def count
21
38
  read_attribute(:count).to_i
22
39
  end
40
+
41
+ def self.per_page
42
+ 10
43
+ end
23
44
  end
@@ -7,6 +7,8 @@ class TagList < Array
7
7
  end
8
8
 
9
9
  attr_accessor :owner
10
+ attr :context, true
11
+ attr :taggable, true
10
12
 
11
13
  # Add tags to the tag_list. Duplicate or blank tags will be ignored.
12
14
  #
@@ -16,6 +18,7 @@ class TagList < Array
16
18
  #
17
19
  # tag_list.add("Fun, Happy", :parse => true)
18
20
  def add(*names)
21
+ taggable.send("#{context.singularize}_list_changed=", true) if (taggable && context)
19
22
  extract_and_apply_options!(names)
20
23
  concat(names)
21
24
  clean!
@@ -30,6 +33,7 @@ class TagList < Array
30
33
  #
31
34
  # tag_list.remove("Sad, Lonely", :parse => true)
32
35
  def remove(*names)
36
+ taggable.send("#{context.singularize}_list_changed=", true) if (taggable && context)
33
37
  extract_and_apply_options!(names)
34
38
  delete_if { |name| names.include?(name) }
35
39
  self
@@ -77,8 +81,8 @@ class TagList < Array
77
81
  string = string.to_s.dup
78
82
 
79
83
  # Parse the quoted tags
80
- string.gsub!(/"(.*?)"\s*#{delimiter}?\s*/) { tag_list << $1; "" }
81
- string.gsub!(/'(.*?)'\s*#{delimiter}?\s*/) { tag_list << $1; "" }
84
+ string.gsub!(/"(.*?)"\s*#{delimiter}?\s*/) { tag_list.add $1; "" }
85
+ string.gsub!(/'(.*?)'\s*#{delimiter}?\s*/) { tag_list.add $1; "" }
82
86
 
83
87
  tag_list.add(string.split(delimiter))
84
88
  end
@@ -3,4 +3,6 @@ class Tagging < ActiveRecord::Base #:nodoc:
3
3
  belongs_to :taggable, :polymorphic => true
4
4
  belongs_to :tagger, :polymorphic => true
5
5
  validates_presence_of :context
6
+
7
+ named_scope :type_pictures, :conditions => "taggable_type = 'Picture'"
6
8
  end
data/rails/init.rb CHANGED
@@ -1,6 +1,4 @@
1
1
  require 'acts-as-taggable-on'
2
2
 
3
3
  ActiveRecord::Base.send :include, ActiveRecord::Acts::TaggableOn
4
- ActiveRecord::Base.send :include, ActiveRecord::Acts::Tagger
5
-
6
- RAILS_DEFAULT_LOGGER.info "** acts_as_taggable_on: initialized properly."
4
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::Tagger
@@ -40,6 +40,11 @@ describe "Acts As Taggable On" do
40
40
  @taggable.should respond_to(:tag_list, :skill_list, :language_list)
41
41
  @taggable.should respond_to(:tag_list=, :skill_list=, :language_list=)
42
42
  end
43
+
44
+ it "should generate a dirty field tracking accessor/setter for each tag type" do
45
+ @taggable.should respond_to(:tag_list_changed?, :skill_list_changed?, :language_list_changed?)
46
+ @taggable.should respond_to(:tag_list_changed=, :skill_list_changed=, :language_list_changed=)
47
+ end
43
48
  end
44
49
 
45
50
  describe "Single Table Inheritance" do
@@ -103,6 +108,20 @@ describe "Acts As Taggable On" do
103
108
  taggable1.find_related_tags_for(OtherTaggableModel).should include(taggable3)
104
109
  taggable1.find_related_tags_for(OtherTaggableModel).should_not include(taggable2)
105
110
  end
111
+
112
+ it "should not include the object itself in the list of related objects" do
113
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
114
+ taggable2 = TaggableModel.create!(:name => "Taggable 2")
115
+
116
+ taggable1.tag_list = "one"
117
+ taggable1.save
118
+
119
+ taggable2.tag_list = "one, two"
120
+ taggable2.save
121
+
122
+ taggable1.find_related_tags.should include(taggable2)
123
+ taggable1.find_related_tags.should_not include(taggable1)
124
+ end
106
125
  end
107
126
 
108
127
  describe 'Tagging Contexts' do
@@ -106,6 +106,51 @@ describe "Taggable" do
106
106
  TaggableModel.find_tagged_with("spinning", :on => :rotors).should_not be_empty
107
107
  end
108
108
 
109
+ it "should support dirty field tracking on assign" do
110
+ @taggable.tag_list_changed?.should == false
111
+
112
+ @taggable.tag_list = "bob, charlie"
113
+ @taggable.tag_list_changed?.should == true
114
+
115
+ @taggable.save
116
+ @taggable.tag_list_changed?.should == false
117
+
118
+ @taggable.tag_list = "bob, charlie"
119
+ @taggable.tag_list_changed?.should == false
120
+ end
121
+
122
+ it "should support dirty field tracking on append" do
123
+ @taggable.tag_list_changed?.should == false
124
+
125
+ @taggable.tag_list.add "bob, charlie"
126
+ @taggable.tag_list_changed?.should == true
127
+
128
+ @taggable.save
129
+ @taggable.tag_list_changed?.should == false
130
+
131
+ @taggable.tag_list.add "bob, charlie"
132
+ @taggable.tag_list_changed?.should == false
133
+ end
134
+
135
+ it "should support dirty field tracking on append" do
136
+ @taggable.tag_list_changed?.should == false
137
+
138
+ @taggable.tag_list = "bob, charlie"
139
+ @taggable.tag_list_changed?.should == true
140
+
141
+ @taggable.save
142
+ @taggable.tag_list_changed?.should == false
143
+
144
+ @taggable.tag_list.remove "bob"
145
+ @taggable.tag_list_changed?.should == true
146
+
147
+ @taggable.save
148
+ @taggable.tag_list_changed?.should == false
149
+
150
+ @taggable.tag_list.remove "bob"
151
+ @taggable.tag_list_changed?.should == false
152
+ end
153
+
109
154
  describe "Single Table Inheritance" do
110
155
  before do
111
156
  [TaggableModel, Tag, Tagging, TaggableUser].each(&:delete_all)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yury-acts-as-taggable-on
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Bleigh
@@ -9,12 +9,12 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-06-10 00:00:00 -07:00
12
+ date: 2009-03-06 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
16
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: yury.korolev@gmail.com
17
+ email: michael@intridea.com
18
18
  executables: []
19
19
 
20
20
  extensions: []
@@ -28,6 +28,7 @@ files:
28
28
  - generators/acts_as_taggable_on_migration
29
29
  - generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb
30
30
  - generators/acts_as_taggable_on_migration/templates
31
+ - generators/acts_as_taggable_on_migration/templates/add_users_migration.rb
31
32
  - generators/acts_as_taggable_on_migration/templates/migration.rb
32
33
  - init.rb
33
34
  - lib/acts-as-taggable-on.rb
@@ -45,6 +46,7 @@ files:
45
46
  - spec/acts_as_taggable_on/taggable_spec.rb
46
47
  - spec/acts_as_taggable_on/tagger_spec.rb
47
48
  - spec/acts_as_taggable_on/tagging_spec.rb
49
+ - spec/debug.log
48
50
  - spec/schema.rb
49
51
  - spec/spec_helper.rb
50
52
  - uninstall.rb