acts-as-taggable-on 2.0.0.pre1 → 2.0.0.pre3

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.
@@ -0,0 +1,90 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Aggregate
3
+ def self.included(base)
4
+ include InstanceMethods
5
+ base.extend ClassMethods
6
+
7
+ base.tag_types.map(&:to_s).each do |tag_type|
8
+ base.class_eval %(
9
+ def #{tag_type.singularize}_counts(options = {})
10
+ tag_counts_on('#{tag_type}', options)
11
+ end
12
+
13
+ def top_#{tag_type}(limit = 10)
14
+ tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
15
+ end
16
+
17
+ def self.top_#{tag_type}(limit = 10)
18
+ tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
19
+ end
20
+ )
21
+ end
22
+ end
23
+
24
+ module ClassMethods
25
+ def tag_counts_on(context, options = {})
26
+ find_for_tag_counts(options.merge({:on => context.to_s}))
27
+ end
28
+
29
+ def all_tag_counts(options = {})
30
+ find_for_tag_counts(options)
31
+ end
32
+
33
+ # Calculate the tag counts for all tags.
34
+ #
35
+ # Options:
36
+ # :start_at - Restrict the tags to those created after a certain time
37
+ # :end_at - Restrict the tags to those created before a certain time
38
+ # :conditions - A piece of SQL conditions to add to the query
39
+ # :limit - The maximum number of tags to return
40
+ # :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
41
+ # :at_least - Exclude tags with a frequency less than the given value
42
+ # :at_most - Exclude tags with a frequency greater than the given value
43
+ # :on - Scope the find to only include a certain context
44
+ def find_for_tag_counts(options = {})
45
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
46
+
47
+ start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
48
+ end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
49
+
50
+ taggable_type = sanitize_sql(["#{Tagging.table_name}.taggable_type = ?", base_class.name])
51
+ taggable_id = sanitize_sql(["#{Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
52
+ options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
53
+
54
+ conditions = [
55
+ taggable_type,
56
+ taggable_id,
57
+ options[:conditions],
58
+ start_at,
59
+ end_at
60
+ ]
61
+
62
+ conditions = conditions.compact.join(' AND ')
63
+
64
+ joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
65
+ joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
66
+ joins << " INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
67
+
68
+ unless descends_from_active_record?
69
+ # Current model is STI descendant, so add type checking to the join condition
70
+ joins << " AND #{table_name}.#{inheritance_column} = '#{name}'"
71
+ end
72
+
73
+ at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
74
+ at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
75
+ having = [at_least, at_most].compact.join(' AND ')
76
+ group_by = "#{grouped_column_names_for(Tag)} HAVING COUNT(*) > 0"
77
+ group_by << " AND #{having}" unless having.blank?
78
+
79
+ Tag.select("#{Tag.table_name}.*, COUNT(*) AS count").joins(joins.join(" ")).where(conditions).group(group_by).limit(options[:limit]).order(options[:order])
80
+
81
+ end
82
+ end
83
+
84
+ module InstanceMethods
85
+ def tag_counts_on(context, options={})
86
+ self.class.tag_counts_on(context, options.merge(:id => id))
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,39 @@
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.class_eval do
8
+ before_save :save_cached_tag_list
9
+ end
10
+
11
+ base.tag_types.map(&:to_s).each do |tag_type|
12
+ base.class_eval %(
13
+ def self.caching_#{tag_type.singularize}_list?
14
+ caching_tag_list_on?("#{tag_type}")
15
+ end
16
+ )
17
+ end
18
+
19
+ base.extend ClassMethods
20
+ include InstanceMethods
21
+ end
22
+
23
+ module ClassMethods
24
+ def caching_tag_list_on?(context)
25
+ column_names.include?("cached_#{context.to_s.singularize}_list")
26
+ end
27
+ end
28
+
29
+ module InstanceMethods
30
+ def save_cached_tag_list
31
+ tag_types.map(&:to_s).each do |tag_type|
32
+ if self.class.send("caching_#{tag_type.singularize}_list?")
33
+ self["cached_#{tag_type.singularize}_list"] = tag_list_cache_on(tag_type.singularize).to_a.flatten.compact.join(', ')
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,202 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Core
3
+ def self.included(base)
4
+ base.class_eval do
5
+ attr_writer :custom_contexts
6
+
7
+ after_save :save_tags
8
+
9
+ def self.tagged_with(*args)
10
+ find_options_for_find_tagged_with(*args)
11
+ end
12
+ end
13
+
14
+ base.tag_types.map(&:to_s).each do |tag_type|
15
+ context_taggings = "#{tag_type.singularize}_taggings".to_sym
16
+ context_tags = tag_type.to_sym
17
+
18
+ base.class_eval do
19
+ has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag,
20
+ :conditions => ['#{Tagging.table_name}.context = ?', tag_type], :class_name => "Tagging"
21
+ has_many context_tags, :through => context_taggings, :source => :tag
22
+ end
23
+
24
+ base.class_eval %(
25
+ def self.#{tag_type.singularize}_counts(options={})
26
+ tag_counts_on('#{tag_type}', options)
27
+ end
28
+
29
+ def #{tag_type.singularize}_list
30
+ tag_list_on('#{tag_type}')
31
+ end
32
+
33
+ def #{tag_type.singularize}_list=(new_tags)
34
+ set_tag_list_on('#{tag_type}', new_tags)
35
+ end
36
+
37
+ def all_#{tag_type}_list
38
+ all_tags_list_on('#{tag_type}')
39
+ end
40
+ )
41
+ end
42
+
43
+ base.extend ClassMethods
44
+ include InstanceMethods
45
+ end
46
+
47
+ module ClassMethods
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 find_options_for_find_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
+ joins(joins.join(" ")).group(group).where(conditions.join(" AND ")).readonly(false)
102
+ end
103
+
104
+ def is_taggable?
105
+ true
106
+ end
107
+
108
+ end
109
+
110
+ module InstanceMethods
111
+ # all column names are necessary for PostgreSQL group clause
112
+ def grouped_column_names_for(object)
113
+ object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
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
+ instance_variable_set(variable_name, TagList.new(all_tags_on(context).map(&:name)).freeze)
146
+ end
147
+
148
+ ##
149
+ # Returns all tags of a given context
150
+ def all_tags_on(context)
151
+ opts = ["#{Tagging.table_name}.context = ?", context.to_s]
152
+ base_tags.where(opts).order("#{Tagging.table_name}.created_at")
153
+ end
154
+
155
+ ##
156
+ # Returns all tags that are not owned of a given context
157
+ def tags_on(context)
158
+ base_tags.where(["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id IS NULL", context.to_s]).all
159
+ end
160
+
161
+ def set_tag_list_on(context, new_list)
162
+ add_custom_context(context)
163
+
164
+ variable_name = "@#{context.to_s.singularize}_list"
165
+ instance_variable_set(variable_name, TagList.from(new_list))
166
+ end
167
+
168
+ def tagging_contexts
169
+ custom_contexts + self.class.tag_types.map(&:to_s)
170
+ end
171
+
172
+ def save_tags
173
+ transaction do
174
+ tagging_contexts.each do |context|
175
+ tag_list = tag_list_cache_on(context).uniq
176
+
177
+ # Find existing tags or create non-existing tags:
178
+ tag_list = Tag.find_or_create_all_with_like_by_name(tag_list)
179
+
180
+ current_tags = tags_on(context)
181
+ old_tags = current_tags - tag_list
182
+ new_tags = tag_list - current_tags
183
+
184
+ # Find taggings to remove:
185
+ old_taggings = Tagging.where(:taggable_id => self.id, :taggable_type => self.class.base_class.to_s,
186
+ :tagger_type => nil, :tagger_id => nil,
187
+ :context => context, :tag_id => old_tags)
188
+
189
+ Tagging.destroy_all :id => old_taggings.map(&:id)
190
+
191
+ # Create new taggings:
192
+ new_tags.each do |tag|
193
+ Tagging.create!(:tag_id => tag.id, :context => context, :taggable => self)
194
+ end
195
+ end
196
+ end
197
+
198
+ true
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,74 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Ownership
3
+ def self.included(base)
4
+ include InstanceMethods
5
+ base.extend ClassMethods
6
+
7
+ base.tag_types.map(&:to_s).each do |tag_type|
8
+ base.class_eval %(
9
+ def #{tag_type}_from(owner)
10
+ owner_tag_list_on(owner, '#{tag_type}')
11
+ end
12
+ )
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ end
18
+
19
+ module InstanceMethods
20
+ def cached_owned_tag_list_on(context)
21
+ variable_name = "@owned_#{context}_list"
22
+ cache = instance_variable_get(variable_name) || instance_variable_set(variable_name, {})
23
+ end
24
+
25
+ def owner_tag_list_on(owner, context)
26
+ cache = cached_owned_tag_list_on(context)
27
+ cache[owner] ||= TagList.new(*owner_tags_on(owner, context).map(&:name))
28
+ end
29
+
30
+ def owner_tags_on(owner, context)
31
+ base_tags.where([%(#{Tagging.table_name}.context = ? AND
32
+ #{Tagging.table_name}.tagger_id = ? AND
33
+ #{Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.to_s])
34
+ end
35
+
36
+ def set_owner_tag_list_on(owner, context, new_list)
37
+ cache = cached_owned_tag_list_on(context)
38
+ cache[owner] = TagList.from(new_list)
39
+ end
40
+
41
+ def save_tags
42
+ transaction do
43
+ tagging_contexts.each do |context|
44
+ cached_owned_tag_list_on(context).each do |owner, tag_list|
45
+ # Find existing tags or create non-existing tags:
46
+ tag_list = Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
47
+
48
+ owned_tags = owner_tags_on(owner, context)
49
+
50
+ old_tags = owned_tags - tag_list
51
+ new_tags = tag_list - owned_tags
52
+
53
+ # Find all taggings that belong to the taggable (self), are owned by the owner,
54
+ # have the correct context, and are removed from the list.
55
+ old_taggings = Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
56
+ :tagger_type => owner.class.to_s, :tagger_id => owner.id,
57
+ :tag_id => old_tags, :context => context)
58
+
59
+ # Destroy old taggings:
60
+ Tagging.destroy_all(:id => old_taggings.map(&:id))
61
+
62
+ # Create new taggings:
63
+ new_tags.each do |tag|
64
+ Tagging.create!(:tag_id => tag.id, :context => context, :tagger => owner, :taggable => self)
65
+ end
66
+ end
67
+ end
68
+
69
+ super
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,74 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Related
3
+ def self.included(base)
4
+ include InstanceMethods
5
+ base.extend ClassMethods
6
+
7
+ base.tag_types.map(&:to_s).each do |tag_type|
8
+ base.class_eval %(
9
+ def find_related_#{tag_type}(options = {})
10
+ related_tags_for('#{tag_type}', self.class, options)
11
+ end
12
+ alias_method :find_related_on_#{tag_type}, :find_related_#{tag_type}
13
+
14
+ def find_related_#{tag_type}_for(klass, options = {})
15
+ related_tags_for('#{tag_type}', klass, options)
16
+ end
17
+
18
+ def find_matching_contexts(search_context, result_context, options = {})
19
+ matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
20
+ end
21
+
22
+ def find_matching_contexts_for(klass, search_context, result_context, options = {})
23
+ matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
24
+ end
25
+ )
26
+ end
27
+ end
28
+
29
+ module ClassMethods
30
+ end
31
+
32
+ module InstanceMethods
33
+ def matching_contexts_for(search_context, result_context, klass, options = {})
34
+ search_conditions = matching_context_search_options(search_context, result_context, klass, options)
35
+
36
+ # klass.select(search_conditions[:select]).from(search_conditions[:from]).where(search_conditions[:conditions]).group(search_conditions[:group]).order(search_conditions[:order])
37
+ klass.scoped(search_conditions)
38
+ end
39
+
40
+ def matching_context_search_options(search_context, result_context, klass, options = {})
41
+ tags_to_find = tags_on(search_context).collect { |t| t.name }
42
+
43
+ exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
44
+
45
+ { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
46
+ :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
47
+ :conditions => ["#{exclude_self} #{klass.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND #{Tag.table_name}.name IN (?) AND #{Tagging.table_name}.context = ?", tags_to_find, result_context],
48
+ :group => grouped_column_names_for(klass),
49
+ :order => "count DESC"
50
+ }.update(options)
51
+ end
52
+
53
+ def related_tags_for(context, klass, options = {})
54
+ search_conditions = related_search_options(context, klass, options)
55
+
56
+ # klass.select(search_conditions[:select]).from(search_conditions[:from]).where(search_conditions[:conditions]).group(search_conditions[:group]).order(search_conditions[:order])
57
+ klass.scoped(search_conditions)
58
+ end
59
+
60
+ def related_search_options(context, klass, options = {})
61
+ tags_to_find = tags_on(context).collect { |t| t.name }
62
+
63
+ exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
64
+
65
+ { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
66
+ :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
67
+ :conditions => ["#{exclude_self} #{klass.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND #{Tag.table_name}.name IN (?)", tags_to_find],
68
+ :group => grouped_column_names_for(klass),
69
+ :order => "count DESC"
70
+ }.update(options)
71
+ end
72
+ end
73
+ end
74
+ end