ghazel-acts-as-taggable-on 2.0.6.1

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.
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