acts-as-taggable-on 0.0.0

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