acts-as-taggable-on 0.0.0

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 +25 -0
  2. data/Gemfile +6 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +212 -0
  5. data/Rakefile +59 -0
  6. data/VERSION +1 -0
  7. data/lib/acts-as-taggable-on.rb +30 -0
  8. data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +41 -0
  9. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +56 -0
  10. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +97 -0
  11. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +220 -0
  12. data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +29 -0
  13. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +101 -0
  14. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +64 -0
  15. data/lib/acts_as_taggable_on/acts_as_tagger.rb +47 -0
  16. data/lib/acts_as_taggable_on/compatibility/Gemfile +6 -0
  17. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +17 -0
  18. data/lib/acts_as_taggable_on/tag.rb +65 -0
  19. data/lib/acts_as_taggable_on/tag_list.rb +95 -0
  20. data/lib/acts_as_taggable_on/tagging.rb +23 -0
  21. data/lib/acts_as_taggable_on/tags_helper.rb +17 -0
  22. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +31 -0
  23. data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +28 -0
  24. data/rails/init.rb +1 -0
  25. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +266 -0
  26. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +114 -0
  27. data/spec/acts_as_taggable_on/tag_list_spec.rb +70 -0
  28. data/spec/acts_as_taggable_on/tag_spec.rb +115 -0
  29. data/spec/acts_as_taggable_on/taggable_spec.rb +277 -0
  30. data/spec/acts_as_taggable_on/tagger_spec.rb +75 -0
  31. data/spec/acts_as_taggable_on/tagging_spec.rb +31 -0
  32. data/spec/acts_as_taggable_on/tags_helper_spec.rb +28 -0
  33. data/spec/bm.rb +52 -0
  34. data/spec/models.rb +36 -0
  35. data/spec/schema.rb +42 -0
  36. data/spec/spec.opts +2 -0
  37. data/spec/spec_helper.rb +47 -0
  38. metadata +109 -0
@@ -0,0 +1,97 @@
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
@@ -0,0 +1,220 @@
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
@@ -0,0 +1,29 @@
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
@@ -0,0 +1,101 @@
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