ghazel-acts-as-taggable-on 2.0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/CHANGELOG +25 -0
  2. data/Gemfile +10 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +221 -0
  5. data/Rakefile +59 -0
  6. data/VERSION +1 -0
  7. data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +7 -0
  8. data/generators/acts_as_taggable_on_migration/templates/migration.rb +29 -0
  9. data/lib/acts-as-taggable-on.rb +30 -0
  10. data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +53 -0
  11. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +53 -0
  12. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +139 -0
  13. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +262 -0
  14. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +105 -0
  15. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +69 -0
  16. data/lib/acts_as_taggable_on/acts_as_tagger.rb +67 -0
  17. data/lib/acts_as_taggable_on/compatibility/Gemfile +8 -0
  18. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +21 -0
  19. data/lib/acts_as_taggable_on/tag.rb +84 -0
  20. data/lib/acts_as_taggable_on/tag_list.rb +96 -0
  21. data/lib/acts_as_taggable_on/tagging.rb +24 -0
  22. data/lib/acts_as_taggable_on/tags_helper.rb +17 -0
  23. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +32 -0
  24. data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +28 -0
  25. data/rails/init.rb +1 -0
  26. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +268 -0
  27. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +114 -0
  28. data/spec/acts_as_taggable_on/tag_list_spec.rb +70 -0
  29. data/spec/acts_as_taggable_on/tag_spec.rb +115 -0
  30. data/spec/acts_as_taggable_on/taggable_spec.rb +333 -0
  31. data/spec/acts_as_taggable_on/tagger_spec.rb +91 -0
  32. data/spec/acts_as_taggable_on/tagging_spec.rb +31 -0
  33. data/spec/acts_as_taggable_on/tags_helper_spec.rb +28 -0
  34. data/spec/bm.rb +52 -0
  35. data/spec/database.yml.sample +17 -0
  36. data/spec/models.rb +31 -0
  37. data/spec/schema.rb +43 -0
  38. data/spec/spec_helper.rb +60 -0
  39. metadata +114 -0
@@ -0,0 +1,53 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Cache
3
+ def self.included(base)
4
+ # Skip adding caching capabilities if table not exists or no cache columns exist
5
+ return unless base.table_exists? && 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.initialize_acts_as_taggable_on_cache
15
+ end
16
+
17
+ module ClassMethods
18
+ def initialize_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
+ initialize_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 save_cached_tag_list
40
+ tag_types.map(&:to_s).each do |tag_type|
41
+ if self.class.send("caching_#{tag_type.singularize}_list?")
42
+ if tag_list_cache_set_on(tag_type)
43
+ list = tag_list_cache_on(tag_type.singularize).to_a.flatten.compact.join(', ')
44
+ self["cached_#{tag_type.singularize}_list"] = list
45
+ end
46
+ end
47
+ end
48
+
49
+ true
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,139 @@
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
+ ##
42
+ # Calculate the tag counts for all tags.
43
+ #
44
+ # @param [Hash] options Options:
45
+ # * :start_at - Restrict the tags to those created after a certain time
46
+ # * :end_at - Restrict the tags to those created before a certain time
47
+ # * :conditions - A piece of SQL conditions to add to the query
48
+ # * :limit - The maximum number of tags to return
49
+ # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
50
+ # * :at_least - Exclude tags with a frequency less than the given value
51
+ # * :at_most - Exclude tags with a frequency greater than the given value
52
+ # * :on - Scope the find to only include a certain context
53
+ def all_tag_counts(options = {})
54
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
55
+
56
+ scope = if ActiveRecord::VERSION::MAJOR >= 3
57
+ {}
58
+ else
59
+ scope(:find) || {}
60
+ end
61
+
62
+ ## Generate conditions:
63
+ options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
64
+
65
+ start_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
66
+ end_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
67
+
68
+ taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?", base_class.name])
69
+ taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
70
+ taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
71
+
72
+ tagging_conditions = [
73
+ taggable_conditions,
74
+ scope[:conditions],
75
+ start_at_conditions,
76
+ end_at_conditions
77
+ ].compact.reverse
78
+
79
+ tag_conditions = [
80
+ options[:conditions]
81
+ ].compact.reverse
82
+
83
+ ## Generate joins:
84
+ taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id"
85
+ taggable_join << " AND #{table_name}.#{inheritance_column} = '#{name}'" unless descends_from_active_record? # Current model is STI descendant, so add type checking to the join condition
86
+
87
+ tagging_joins = [
88
+ taggable_join,
89
+ scope[:joins]
90
+ ].compact
91
+
92
+ tag_joins = [
93
+ ].compact
94
+
95
+ [tagging_joins, tag_joins].each(&:reverse!) if (ActiveRecord::VERSION::MAJOR < 3 and not defined? ActiveRecord::BUG_6668_FIXED)
96
+
97
+ ## Generate scope:
98
+ tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count")
99
+ tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
100
+
101
+ # Joins and conditions
102
+ tagging_joins.each { |join| tagging_scope = tagging_scope.joins(join) }
103
+ tagging_conditions.each { |condition| tagging_scope = tagging_scope.where(condition) }
104
+
105
+ tag_joins.each { |join| tag_scope = tag_scope.joins(join) }
106
+ tag_conditions.each { |condition| tag_scope = tag_scope.where(condition) }
107
+
108
+ # GROUP BY and HAVING clauses:
109
+ at_least = sanitize_sql(['tags_count >= ?', options.delete(:at_least)]) if options[:at_least]
110
+ at_most = sanitize_sql(['tags_count <= ?', options.delete(:at_most)]) if options[:at_most]
111
+ having = ["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) > 0", at_least, at_most].compact.join(' AND ')
112
+
113
+ group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
114
+
115
+ if ActiveRecord::VERSION::MAJOR >= 3
116
+ # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
117
+ scoped_select = "#{table_name}.#{primary_key}"
118
+ tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{select(scoped_select).to_sql})").
119
+ group(group_columns).
120
+ having(having)
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
127
+
128
+ tag_scope = tag_scope.joins("JOIN (#{tagging_scope.to_sql}) AS taggings ON taggings.tag_id = tags.id")
129
+ tag_scope
130
+ end
131
+ end
132
+
133
+ module InstanceMethods
134
+ def tag_counts_on(context, options={})
135
+ self.class.tag_counts_on(context, options.merge(:id => id))
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,262 @@
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 |tags_type|
18
+ tag_type = tags_type.to_s.singularize
19
+ context_taggings = "#{tag_type}_taggings".to_sym
20
+ context_tags = tags_type.to_sym
21
+
22
+ class_eval do
23
+ has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging",
24
+ :conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_type]
25
+ has_many context_tags, :through => context_taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
26
+ end
27
+
28
+ class_eval %(
29
+ def #{tag_type}_list
30
+ tag_list_on('#{tags_type}')
31
+ end
32
+
33
+ def #{tag_type}_list=(new_tags)
34
+ set_tag_list_on('#{tags_type}', new_tags)
35
+ end
36
+
37
+ def all_#{tags_type}_list
38
+ all_tags_list_on('#{tags_type}')
39
+ end
40
+ )
41
+ end
42
+ end
43
+
44
+ def acts_as_taggable_on(*args)
45
+ super(*args)
46
+ initialize_acts_as_taggable_on_core
47
+ end
48
+
49
+ # all column names are necessary for PostgreSQL group clause
50
+ def grouped_column_names_for(object)
51
+ object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
52
+ end
53
+
54
+ ##
55
+ # Return a scope of objects that are tagged with the specified tags.
56
+ #
57
+ # @param tags The tags that we want to query for
58
+ # @param [Hash] options A hash of options to alter you query:
59
+ # * <tt>:exclude</tt> - if set to true, return objects that are *NOT* tagged with the specified tags
60
+ # * <tt>:any</tt> - if set to true, return objects that are tagged with *ANY* of the specified tags
61
+ # * <tt>:match_all</tt> - if set to true, return objects that are *ONLY* tagged with the specified tags
62
+ #
63
+ # Example:
64
+ # User.tagged_with("awesome", "cool") # Users that are tagged with awesome and cool
65
+ # User.tagged_with("awesome", "cool", :exclude => true) # Users that are not tagged with awesome or cool
66
+ # User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
67
+ # User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
68
+ def tagged_with(tags, options = {})
69
+ tag_list = ActsAsTaggableOn::TagList.from(tags)
70
+
71
+ return {} if tag_list.empty?
72
+
73
+ joins = []
74
+ conditions = []
75
+
76
+ context = options.delete(:on)
77
+
78
+ if options.delete(:exclude)
79
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
80
+ 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}.id AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
81
+
82
+ elsif options.delete(:any)
83
+ conditions << tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
84
+
85
+ tagging_join = " JOIN #{ActsAsTaggableOn::Tagging.table_name}" +
86
+ " ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{table_name}.#{primary_key}" +
87
+ " AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)}" +
88
+ " JOIN #{ActsAsTaggableOn::Tag.table_name}" +
89
+ " ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id"
90
+
91
+ tagging_join << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.context = ?", context.to_s]) if context
92
+ select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one?
93
+
94
+ joins << tagging_join
95
+
96
+ else
97
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
98
+ return scoped(:conditions => "1 = 0") unless tags.length == tag_list.length
99
+
100
+ tags.each do |tag|
101
+ safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
102
+ prefix = "#{safe_tag}_#{rand(1024)}"
103
+
104
+ taggings_alias = "#{undecorated_table_name}_taggings_#{prefix}"
105
+
106
+ tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
107
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
108
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
109
+ " AND #{taggings_alias}.tag_id = #{tag.id}"
110
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
111
+
112
+ joins << tagging_join
113
+ end
114
+ end
115
+
116
+ taggings_alias, tags_alias = "#{undecorated_table_name}_taggings_group", "#{undecorated_table_name}_tags_group"
117
+
118
+ if options.delete(:match_all)
119
+ joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
120
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
121
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
122
+
123
+
124
+ group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
125
+ group = "#{group_columns} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
126
+ end
127
+
128
+ scoped(:select => select_clause,
129
+ :joins => joins.join(" "),
130
+ :group => group,
131
+ :conditions => conditions.join(" AND "),
132
+ :order => options[:order],
133
+ :readonly => false)
134
+ end
135
+
136
+ def is_taggable?
137
+ true
138
+ end
139
+ end
140
+
141
+ module InstanceMethods
142
+ # all column names are necessary for PostgreSQL group clause
143
+ def grouped_column_names_for(object)
144
+ self.class.grouped_column_names_for(object)
145
+ end
146
+
147
+ def custom_contexts
148
+ @custom_contexts ||= []
149
+ end
150
+
151
+ def is_taggable?
152
+ self.class.is_taggable?
153
+ end
154
+
155
+ def add_custom_context(value)
156
+ custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
157
+ end
158
+
159
+ def cached_tag_list_on(context)
160
+ self["cached_#{context.to_s.singularize}_list"]
161
+ end
162
+
163
+ def tag_list_cache_set_on(context)
164
+ variable_name = "@#{context.to_s.singularize}_list"
165
+ !instance_variable_get(variable_name).nil?
166
+ end
167
+
168
+ def tag_list_cache_on(context)
169
+ variable_name = "@#{context.to_s.singularize}_list"
170
+ instance_variable_get(variable_name) || instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
171
+ end
172
+
173
+ def tag_list_on(context)
174
+ add_custom_context(context)
175
+ tag_list_cache_on(context)
176
+ end
177
+
178
+ def all_tags_list_on(context)
179
+ variable_name = "@all_#{context.to_s.singularize}_list"
180
+ return instance_variable_get(variable_name) if instance_variable_get(variable_name)
181
+
182
+ instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze)
183
+ end
184
+
185
+ ##
186
+ # Returns all tags of a given context
187
+ def all_tags_on(context)
188
+ tag_table_name = ActsAsTaggableOn::Tag.table_name
189
+ tagging_table_name = ActsAsTaggableOn::Tagging.table_name
190
+
191
+ opts = ["#{tagging_table_name}.context = ?", context.to_s]
192
+ scope = base_tags.where(opts)
193
+
194
+ if ActsAsTaggableOn::Tag.using_postgresql?
195
+ group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
196
+ scope = scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
197
+ else
198
+ scope = scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
199
+ end
200
+
201
+ scope.all
202
+ end
203
+
204
+ ##
205
+ # Returns all tags that are not owned of a given context
206
+ def tags_on(context)
207
+ base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s]).all
208
+ end
209
+
210
+ def set_tag_list_on(context, new_list)
211
+ add_custom_context(context)
212
+
213
+ variable_name = "@#{context.to_s.singularize}_list"
214
+ instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
215
+ end
216
+
217
+ def tagging_contexts
218
+ custom_contexts + self.class.tag_types.map(&:to_s)
219
+ end
220
+
221
+ def reload(*args)
222
+ self.class.tag_types.each do |context|
223
+ instance_variable_set("@#{context.to_s.singularize}_list", nil)
224
+ instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
225
+ end
226
+
227
+ super(*args)
228
+ end
229
+
230
+ def save_tags
231
+ tagging_contexts.each do |context|
232
+ next unless tag_list_cache_set_on(context)
233
+
234
+ tag_list = tag_list_cache_on(context).uniq
235
+
236
+ # Find existing tags or create non-existing tags:
237
+ tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
238
+
239
+ current_tags = tags_on(context)
240
+ old_tags = current_tags - tag_list
241
+ new_tags = tag_list - current_tags
242
+
243
+ # Find taggings to remove:
244
+ old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
245
+ :context => context.to_s, :tag_id => old_tags).all
246
+
247
+ if old_taggings.present?
248
+ # Destroy old taggings:
249
+ ActsAsTaggableOn::Tagging.destroy_all :id => old_taggings.map(&:id)
250
+ end
251
+
252
+ # Create new taggings:
253
+ new_tags.each do |tag|
254
+ taggings.create!(:tag_id => tag.id, :context => context.to_s, :taggable => self)
255
+ end
256
+ end
257
+
258
+ true
259
+ end
260
+ end
261
+ end
262
+ end