acts-as-taggable-on 0.0.0 → 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/CHANGELOG +2 -9
  2. data/README +196 -0
  3. data/Rakefile +10 -47
  4. data/VERSION +1 -1
  5. data/lib/acts-as-taggable-on.rb +6 -30
  6. data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +313 -28
  7. data/lib/acts_as_taggable_on/acts_as_tagger.rb +45 -40
  8. data/lib/acts_as_taggable_on/tag.rb +6 -48
  9. data/lib/acts_as_taggable_on/tag_list.rb +29 -31
  10. data/lib/acts_as_taggable_on/tagging.rb +1 -18
  11. data/lib/acts_as_taggable_on/tags_helper.rb +2 -8
  12. data/rails/init.rb +6 -1
  13. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +47 -148
  14. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +4 -46
  15. data/spec/acts_as_taggable_on/tag_list_spec.rb +0 -29
  16. data/spec/acts_as_taggable_on/tag_spec.rb +7 -95
  17. data/spec/acts_as_taggable_on/taggable_spec.rb +48 -178
  18. data/spec/acts_as_taggable_on/tagger_spec.rb +5 -57
  19. data/spec/acts_as_taggable_on/tagging_spec.rb +1 -25
  20. data/spec/schema.rb +2 -12
  21. data/spec/spec.opts +6 -1
  22. data/spec/spec_helper.rb +34 -35
  23. metadata +7 -31
  24. data/Gemfile +0 -6
  25. data/README.rdoc +0 -212
  26. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +0 -56
  27. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +0 -97
  28. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +0 -220
  29. data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +0 -29
  30. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +0 -101
  31. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +0 -64
  32. data/lib/acts_as_taggable_on/compatibility/Gemfile +0 -6
  33. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +0 -17
  34. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +0 -31
  35. data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +0 -28
  36. data/spec/acts_as_taggable_on/tags_helper_spec.rb +0 -28
  37. data/spec/bm.rb +0 -52
  38. data/spec/models.rb +0 -36
@@ -1,56 +0,0 @@
1
- module ActsAsTaggableOn::Taggable
2
- module Cache
3
- def self.included(base)
4
- # Skip adding caching capabilities if no cache columns exist
5
- return unless base.tag_types.any? { |context| base.column_names.include?("cached_#{context.to_s.singularize}_list") }
6
-
7
- base.send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods
8
- base.extend ActsAsTaggableOn::Taggable::Cache::ClassMethods
9
-
10
- base.class_eval do
11
- before_save :save_cached_tag_list
12
- end
13
-
14
- base.intialize_acts_as_taggable_on_cache
15
- end
16
-
17
- module ClassMethods
18
- def intialize_acts_as_taggable_on_cache
19
- tag_types.map(&:to_s).each do |tag_type|
20
- class_eval %(
21
- def self.caching_#{tag_type.singularize}_list?
22
- caching_tag_list_on?("#{tag_type}")
23
- end
24
- )
25
- end
26
- end
27
-
28
- def acts_as_taggable_on(*args)
29
- super(*args)
30
- intialize_acts_as_taggable_on_cache
31
- end
32
-
33
- def caching_tag_list_on?(context)
34
- column_names.include?("cached_#{context.to_s.singularize}_list")
35
- end
36
- end
37
-
38
- module InstanceMethods
39
- def tag_list_cache_set_on(context)
40
- variable_name = "@#{context.to_s.singularize}_list"
41
- !instance_variable_get(variable_name).nil?
42
- end
43
-
44
- def save_cached_tag_list
45
- tag_types.map(&:to_s).each do |tag_type|
46
- if self.class.send("caching_#{tag_type.singularize}_list?")
47
- if tag_list_cache_set_on(tag_type)
48
- list = tag_list_cache_on(tag_type.singularize).to_a.flatten.compact.join(', ')
49
- self["cached_#{tag_type.singularize}_list"] = list
50
- end
51
- end
52
- end
53
- end
54
- end
55
- end
56
- end
@@ -1,97 +0,0 @@
1
- module ActsAsTaggableOn::Taggable
2
- module Collection
3
- def self.included(base)
4
- base.send :include, ActsAsTaggableOn::Taggable::Collection::InstanceMethods
5
- base.extend ActsAsTaggableOn::Taggable::Collection::ClassMethods
6
- base.initialize_acts_as_taggable_on_collection
7
- end
8
-
9
- module ClassMethods
10
- def initialize_acts_as_taggable_on_collection
11
- tag_types.map(&:to_s).each do |tag_type|
12
- class_eval %(
13
- def self.#{tag_type.singularize}_counts(options={})
14
- tag_counts_on('#{tag_type}', options)
15
- end
16
-
17
- def #{tag_type.singularize}_counts(options = {})
18
- tag_counts_on('#{tag_type}', options)
19
- end
20
-
21
- def top_#{tag_type}(limit = 10)
22
- tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
23
- end
24
-
25
- def self.top_#{tag_type}(limit = 10)
26
- tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
27
- end
28
- )
29
- end
30
- end
31
-
32
- def acts_as_taggable_on(*args)
33
- super(*args)
34
- initialize_acts_as_taggable_on_collection
35
- end
36
-
37
- def tag_counts_on(context, options = {})
38
- all_tag_counts(options.merge({:on => context.to_s}))
39
- end
40
-
41
- # Calculate the tag counts for all tags.
42
- #
43
- # Options:
44
- # :start_at - Restrict the tags to those created after a certain time
45
- # :end_at - Restrict the tags to those created before a certain time
46
- # :conditions - A piece of SQL conditions to add to the query
47
- # :limit - The maximum number of tags to return
48
- # :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
49
- # :at_least - Exclude tags with a frequency less than the given value
50
- # :at_most - Exclude tags with a frequency greater than the given value
51
- # :on - Scope the find to only include a certain context
52
- def all_tag_counts(options = {})
53
- options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
54
-
55
- start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
56
- end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
57
-
58
- taggable_type = sanitize_sql(["#{Tagging.table_name}.taggable_type = ?", base_class.name])
59
- taggable_id = sanitize_sql(["#{Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
60
- options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
61
-
62
- conditions = [
63
- taggable_type,
64
- taggable_id,
65
- options[:conditions],
66
- start_at,
67
- end_at
68
- ]
69
-
70
- conditions = conditions.compact.join(' AND ')
71
-
72
- joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
73
- joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
74
- joins << " INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
75
-
76
- unless descends_from_active_record?
77
- # Current model is STI descendant, so add type checking to the join condition
78
- joins << " AND #{table_name}.#{inheritance_column} = '#{name}'"
79
- end
80
-
81
- at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
82
- at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
83
- having = [at_least, at_most].compact.join(' AND ')
84
- group_by = "#{grouped_column_names_for(Tag)} HAVING COUNT(*) > 0"
85
- group_by << " AND #{having}" unless having.blank?
86
-
87
- Tag.select("#{Tag.table_name}.*, COUNT(*) AS count").joins(joins.join(" ")).where(conditions).group(group_by).limit(options[:limit]).order(options[:order])
88
- end
89
- end
90
-
91
- module InstanceMethods
92
- def tag_counts_on(context, options={})
93
- self.class.tag_counts_on(context, options.merge(:id => id))
94
- end
95
- end
96
- end
97
- end
@@ -1,220 +0,0 @@
1
- module ActsAsTaggableOn::Taggable
2
- module Core
3
- def self.included(base)
4
- base.send :include, ActsAsTaggableOn::Taggable::Core::InstanceMethods
5
- base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods
6
-
7
- base.class_eval do
8
- attr_writer :custom_contexts
9
- after_save :save_tags
10
- end
11
-
12
- base.initialize_acts_as_taggable_on_core
13
- end
14
-
15
- module ClassMethods
16
- def initialize_acts_as_taggable_on_core
17
- tag_types.map(&:to_s).each do |tag_type|
18
- context_taggings = "#{tag_type.singularize}_taggings".to_sym
19
- context_tags = tag_type.to_sym
20
-
21
- class_eval do
22
- has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "Tagging",
23
- :conditions => ['#{Tagging.table_name}.tagger_id IS NULL AND #{Tagging.table_name}.context = ?', tag_type]
24
- has_many context_tags, :through => context_taggings, :source => :tag
25
- end
26
-
27
- class_eval %(
28
- def #{tag_type.singularize}_list
29
- tag_list_on('#{tag_type}')
30
- end
31
-
32
- def #{tag_type.singularize}_list=(new_tags)
33
- set_tag_list_on('#{tag_type}', new_tags)
34
- end
35
-
36
- def all_#{tag_type}_list
37
- all_tags_list_on('#{tag_type}')
38
- end
39
- )
40
- end
41
- end
42
-
43
- def acts_as_taggable_on(*args)
44
- super(*args)
45
- initialize_acts_as_taggable_on_core
46
- end
47
-
48
- # all column names are necessary for PostgreSQL group clause
49
- def grouped_column_names_for(object)
50
- object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
51
- end
52
-
53
- def tagged_with(tags, options = {})
54
- tag_list = TagList.from(tags)
55
-
56
- return {} if tag_list.empty?
57
-
58
- joins = []
59
- conditions = []
60
-
61
- context = options.delete(:on)
62
-
63
- if options.delete(:exclude)
64
- tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
65
- conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND (#{tags_conditions}) WHERE #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
66
-
67
- elsif options.delete(:any)
68
- tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
69
- conditions << "#{table_name}.#{primary_key} IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND (#{tags_conditions}) WHERE #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
70
-
71
- else
72
- tags = Tag.named_any(tag_list)
73
- return where("1 = 0") unless tags.length == tag_list.length
74
-
75
- tags.each do |tag|
76
- safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
77
- prefix = "#{safe_tag}_#{rand(1024)}"
78
-
79
- taggings_alias = "#{table_name}_taggings_#{prefix}"
80
-
81
- tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" +
82
- " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
83
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
84
- " AND #{taggings_alias}.tag_id = #{tag.id}"
85
- tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
86
-
87
- joins << tagging_join
88
- end
89
- end
90
-
91
- taggings_alias, tags_alias = "#{table_name}_taggings_group", "#{table_name}_tags_group"
92
-
93
- if options.delete(:match_all)
94
- joins << "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias}" +
95
- " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
96
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
97
-
98
- group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
99
- end
100
-
101
-
102
- joins(joins.join(" ")).group(group).where(conditions.join(" AND ")).readonly(false)
103
- end
104
-
105
- def is_taggable?
106
- true
107
- end
108
- end
109
-
110
- module InstanceMethods
111
- # all column names are necessary for PostgreSQL group clause
112
- def grouped_column_names_for(object)
113
- self.class.grouped_column_names_for(object)
114
- end
115
-
116
- def custom_contexts
117
- @custom_contexts ||= []
118
- end
119
-
120
- def is_taggable?
121
- self.class.is_taggable?
122
- end
123
-
124
- def add_custom_context(value)
125
- custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
126
- end
127
-
128
- def cached_tag_list_on(context)
129
- self["cached_#{context.to_s.singularize}_list"]
130
- end
131
-
132
- def tag_list_cache_on(context)
133
- variable_name = "@#{context.to_s.singularize}_list"
134
- instance_variable_get(variable_name) || instance_variable_set(variable_name, TagList.new(tags_on(context).map(&:name)))
135
- end
136
-
137
- def tag_list_on(context)
138
- add_custom_context(context)
139
- tag_list_cache_on(context)
140
- end
141
-
142
- def all_tags_list_on(context)
143
- variable_name = "@all_#{context.to_s.singularize}_list"
144
- return instance_variable_get(variable_name) if instance_variable_get(variable_name)
145
-
146
- instance_variable_set(variable_name, TagList.new(all_tags_on(context).map(&:name)).freeze)
147
- end
148
-
149
- ##
150
- # Returns all tags of a given context
151
- def all_tags_on(context)
152
- opts = ["#{Tagging.table_name}.context = ?", context.to_s]
153
- base_tags.where(opts).order("#{Tagging.table_name}.created_at").group("#{Tagging.table_name}.tag_id").all
154
- end
155
-
156
- ##
157
- # Returns all tags that are not owned of a given context
158
- def tags_on(context)
159
- if respond_to?(context)
160
- # If the association is available, use it:
161
- send(context).all
162
- else
163
- # If the association is not available, query it the old fashioned way
164
- base_tags.where(["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id IS NULL", context.to_s]).all
165
- end
166
- end
167
-
168
- def set_tag_list_on(context, new_list)
169
- add_custom_context(context)
170
-
171
- variable_name = "@#{context.to_s.singularize}_list"
172
- instance_variable_set(variable_name, TagList.from(new_list))
173
- end
174
-
175
- def tagging_contexts
176
- custom_contexts + self.class.tag_types.map(&:to_s)
177
- end
178
-
179
- def reload
180
- self.class.tag_types.each do |context|
181
- instance_variable_set("@#{context.to_s.singularize}_list", nil)
182
- instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
183
- end
184
-
185
- super
186
- end
187
-
188
- def save_tags
189
- transaction do
190
- tagging_contexts.each do |context|
191
- tag_list = tag_list_cache_on(context).uniq
192
-
193
- # Find existing tags or create non-existing tags:
194
- tag_list = Tag.find_or_create_all_with_like_by_name(tag_list)
195
-
196
- current_tags = tags_on(context)
197
- old_tags = current_tags - tag_list
198
- new_tags = tag_list - current_tags
199
-
200
- # Find taggings to remove:
201
- old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
202
- :context => context.to_s, :tag_id => old_tags).all
203
-
204
- if old_taggings.present?
205
- # Destroy old taggings:
206
- Tagging.destroy_all :id => old_taggings.map(&:id)
207
- end
208
-
209
- # Create new taggings:
210
- new_tags.each do |tag|
211
- Tagging.create!(:tag_id => tag.id, :context => context.to_s, :taggable => self)
212
- end
213
- end
214
- end
215
-
216
- true
217
- end
218
- end
219
- end
220
- end
@@ -1,29 +0,0 @@
1
- module ActsAsTaggableOn::Taggable
2
- module Dirty
3
- def self.included(base)
4
- include ActsAsTaggableOn::Taggable::Dirty::InstanceMethods
5
-
6
- base.tag_types.map(&:to_s).each do |tag_type|
7
- base.class_eval %(
8
- def #{tag_type.singularize}_list_changed?
9
- tag_list_changed_on?('#{tag_type}')
10
- tag_list_on('#{tag_type}')
11
- end
12
-
13
- def #{tag_type.singularize}_list=(new_tags)
14
- change_tag_list_on('#{tag_type}', new_tags)
15
- super(new_tags)
16
- end
17
- )
18
- end
19
- end
20
-
21
- module InstanceMethods
22
- def tag_list_changed_on?(context)
23
- end
24
-
25
- def change_tag_list_on(context, new_tags)
26
- end
27
- end
28
- end
29
- end
@@ -1,101 +0,0 @@
1
- module ActsAsTaggableOn::Taggable
2
- module Ownership
3
- def self.included(base)
4
- base.send :include, ActsAsTaggableOn::Taggable::Ownership::InstanceMethods
5
- base.extend ActsAsTaggableOn::Taggable::Ownership::ClassMethods
6
-
7
- base.class_eval do
8
- after_save :save_owned_tags
9
- end
10
-
11
- base.initialize_acts_as_taggable_on_ownership
12
- end
13
-
14
- module ClassMethods
15
- def acts_as_taggable_on(*args)
16
- initialize_acts_as_taggable_on_ownership
17
- super(*args)
18
- end
19
-
20
- def initialize_acts_as_taggable_on_ownership
21
- tag_types.map(&:to_s).each do |tag_type|
22
- class_eval %(
23
- def #{tag_type}_from(owner)
24
- owner_tag_list_on(owner, '#{tag_type}')
25
- end
26
- )
27
- end
28
- end
29
- end
30
-
31
- module InstanceMethods
32
- def owner_tags_on(owner, context)
33
- base_tags.where([%(#{Tagging.table_name}.context = ? AND
34
- #{Tagging.table_name}.tagger_id = ? AND
35
- #{Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.to_s]).all
36
- end
37
-
38
- def cached_owned_tag_list_on(context)
39
- variable_name = "@owned_#{context}_list"
40
- cache = instance_variable_get(variable_name) || instance_variable_set(variable_name, {})
41
- end
42
-
43
- def owner_tag_list_on(owner, context)
44
- add_custom_context(context)
45
-
46
- cache = cached_owned_tag_list_on(context)
47
- cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
48
-
49
- cache[owner] ||= TagList.new(*owner_tags_on(owner, context).map(&:name))
50
- end
51
-
52
- def set_owner_tag_list_on(owner, context, new_list)
53
- add_custom_context(context)
54
-
55
- cache = cached_owned_tag_list_on(context)
56
- cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
57
-
58
- cache[owner] = TagList.from(new_list)
59
- end
60
-
61
- def reload
62
- self.class.tag_types.each do |context|
63
- instance_variable_set("@owned_#{context}_list", nil)
64
- end
65
-
66
- super
67
- end
68
-
69
- def save_owned_tags
70
- transaction do
71
- tagging_contexts.each do |context|
72
- cached_owned_tag_list_on(context).each do |owner, tag_list|
73
- # Find existing tags or create non-existing tags:
74
- tag_list = Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
75
-
76
- owned_tags = owner_tags_on(owner, context)
77
- old_tags = owned_tags - tag_list
78
- new_tags = tag_list - owned_tags
79
-
80
- # Find all taggings that belong to the taggable (self), are owned by the owner,
81
- # have the correct context, and are removed from the list.
82
- old_taggings = Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
83
- :tagger_type => owner.class.to_s, :tagger_id => owner.id,
84
- :tag_id => old_tags, :context => context).all
85
-
86
- if old_taggings.present?
87
- # Destroy old taggings:
88
- Tagging.destroy_all(:id => old_taggings.map(&:id))
89
- end
90
-
91
- # Create new taggings:
92
- new_tags.each do |tag|
93
- Tagging.create!(:tag_id => tag.id, :context => context.to_s, :tagger => owner, :taggable => self)
94
- end
95
- end
96
- end
97
- end
98
- end
99
- end
100
- end
101
- end