acts_as_taggable3 2.0.beta2

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,212 @@
1
+ [11 Jun 09]
2
+
3
+ * Remove deprecated TagCountsExtension.
4
+
5
+ * Update tests to use foxy fixtures [Jonas Wagner]
6
+
7
+ * Allow hash conditions to be passed to Tag.counts [Jonas Wagner]
8
+
9
+ [3 Jun 09]
10
+
11
+ * Upgrade tests for Rails 2.3
12
+
13
+ [18 Mar 09]
14
+
15
+ * Change callbacks used to save tags.
16
+
17
+ [18 Feb 09]
18
+
19
+ * Greatly improve speed when using find_tagged_with and :match_all [notonthehighstreet.com].
20
+
21
+ [17 Sep 08]
22
+
23
+ * Sanitize scope conditions in find_options_for_tag_counts [Rémy-Christophe Schermesser]
24
+
25
+ [23 Aug 08]
26
+
27
+ * Fix tag_counts instance method when no tags are present.
28
+
29
+ * Make tag_counts instance_method merge any :conditions passed to it.
30
+
31
+ [30 Mar 08]
32
+
33
+ * Make TagList.from accept array arguments.
34
+
35
+ [29 Mar 08]
36
+
37
+ * Improve parsing of quotes inside tags [Arturas Slajus].
38
+
39
+ * Add Tag.counts method.
40
+
41
+ [28 Mar 08]
42
+
43
+ * Make Tag#taggings :dependent => :destroy.
44
+
45
+ [27 Mar 08]
46
+
47
+ * Fix documentation for tag_counts.
48
+
49
+ [18 Mar 08]
50
+
51
+ * Add TagList#toggle [Pete Yandell].
52
+
53
+ # Add find_related_tags method [Austin Foncaier].
54
+
55
+ [30 Jan 08]
56
+
57
+ * Fix Tag.destroy_unused on Rails 2.0.
58
+
59
+ [23 October 2007]
60
+
61
+ * Make find_options_for_tag_counts and find_options_for_tagged_with dup their options.
62
+
63
+ * Apply conditions properly in find_options_for_tag_counts.
64
+
65
+ * Fix tag_cloud when no tags are present.
66
+
67
+ [22 October 2007]
68
+
69
+ * Fix find_tagged_with using :match_all and :include.
70
+
71
+ * Use inner joins instead of left outer joins.
72
+
73
+ [15 October 2007]
74
+
75
+ * Make find_tagged_with correctly apply :conditions
76
+
77
+ * Add Tag.destroy_unused option.
78
+
79
+ [11 October 2007]
80
+
81
+ * Make tag_counts work correctly with STI.
82
+
83
+ [3 October 2007]
84
+
85
+ * Improve documentation.
86
+
87
+ * Fix TagsHelper and test.
88
+
89
+ [2 October 2007]
90
+
91
+ * Remove TagList.parse, use TagList.from instead.
92
+
93
+ * Add :parse option to TagList#new, TagList#add, and TagList#remove.
94
+
95
+ tag_list = TagList.new("One, Two", :parse => true) # ["One", "Two"]
96
+
97
+ tag_list # ["One", "Two"]
98
+ tag_list.add("Three, Four", :parse => true) # ["One", "Two", "Three", "Four"]
99
+
100
+ * Remove TagList#names.
101
+
102
+ [29 September 2007]
103
+
104
+ * Add TagsHelper to assist with generating tag clouds and provide a simple example.
105
+
106
+ [27 September 2007]
107
+
108
+ * Add #tag_counts method to get tag counts for a specific object's tags.
109
+
110
+ * BACKWARDS INCOMPATIBILITY: Rename #find_options_for_tagged_with to #find_options_for_find_tagged_with
111
+
112
+ [17 September 2007]
113
+
114
+ * Fix clearing of cached tag list when all tags removed.
115
+
116
+ [12 September 2007]
117
+
118
+ * Make the TagList class inherit from Array.
119
+
120
+ * Deprecate obsolete TagList#names.
121
+
122
+ [6 September 2007]
123
+
124
+ * Add TagList#include? and TagList#empty?
125
+
126
+ [26 August 2007]
127
+
128
+ * Remove deprecated Tag.delimiter. Use TagList.delimiter instead.
129
+
130
+ [25 August 2007]
131
+
132
+ * Make tag_counts work with has_many :through
133
+
134
+ [23 August 2007]
135
+
136
+ * Make search comparisons case-insensitive across different databases. [Moisés Machado]
137
+
138
+ * Improve compatiblity with STI. [Moisés Machado]
139
+
140
+ [25 July 2007]
141
+
142
+ * Respect custom table names for the Tag and Tagging classes.
143
+
144
+ * Fix the :exclude option for find_tagged_with
145
+
146
+ [17 July 2007]
147
+
148
+ * Make the migration work on edge rails
149
+
150
+ [8 July 2007]
151
+
152
+ * find_options_for_tagged_with should not alter its arguments
153
+
154
+ [1 July 2007]
155
+
156
+ * Fix incorrect tagging when the case of the tag list is changed.
157
+
158
+ * Fix deprecated Tag.delimiter accessor.
159
+
160
+ [23 June 2007]
161
+
162
+ * Add validation to Tag model.
163
+
164
+ * find_options_for_tagged_with should always return a hash.
165
+
166
+ * find_tagged_with passing in no tags should return an empty array.
167
+
168
+ * Improve compatibility with PostgreSQL.
169
+
170
+ [21 June 2007]
171
+
172
+ * Remove extra .rb from generated migration file name.
173
+
174
+ [15 June 2007]
175
+
176
+ * Introduce TagList class.
177
+
178
+ * Various cleanups and improvements.
179
+
180
+ * Use TagList.delimiter now, not Tag.delimiter. Tag.delimiter will be removed at some stage.
181
+
182
+ [11 June 2007]
183
+
184
+ * Restructure the creation of the options for find_tagged_with [Thijs Cadier]
185
+
186
+ * Add an example migration with a generator.
187
+
188
+ * Add caching.
189
+
190
+ * Fix compatibility with Ruby < 1.8.6
191
+
192
+ [23 April 2007]
193
+
194
+ * Make tag_list to respect Tag.delimiter
195
+
196
+ [31 March 2007]
197
+
198
+ * Add Tag.delimiter accessor to change how tags are parsed.
199
+
200
+ * Fix :include => :tags when used with find_tagged_with
201
+
202
+ [7 March 2007]
203
+
204
+ * Fix tag_counts for SQLServer [Brad Young]
205
+
206
+ [21 Feb 2007]
207
+
208
+ * Use scoping instead of TagCountsExtension [Michael Schuerig]
209
+
210
+ [7 Jan 2007]
211
+
212
+ * Add :match_all to find_tagged_with [Michael Sheakoski]
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2006 Jonathan Viney
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,157 @@
1
+ = acts_as_taggable_on_steroids
2
+
3
+ If you find this plugin useful, please consider a donation to show your support!
4
+
5
+ http://www.paypal.com/cgi-bin/webscr?cmd=_send-money
6
+
7
+ Email address: jonathan.viney@gmail.com
8
+
9
+ == Instructions
10
+
11
+ This plugin is based on acts_as_taggable by DHH but includes extras
12
+ such as tests, smarter tag assignment, and tag cloud calculations.
13
+
14
+ == Installation
15
+
16
+ Rails 2.*
17
+
18
+ ruby script/plugin install git://github.com/jviney/acts_as_taggable_on_steroids.git
19
+
20
+ Rails 3:
21
+
22
+ rails plugin install git://github.com/jviney/acts_as_taggable_on_steroids.git
23
+
24
+ == Usage
25
+
26
+ === Prepare database
27
+
28
+ Generate and apply the migration:
29
+
30
+ ruby script/generate acts_as_taggable_migration
31
+ rake db:migrate
32
+
33
+ === Basic tagging
34
+
35
+ Let's suppose users have many posts and we want those posts to have tags.
36
+ The first step is to add +acts_as_taggable+ to the Post class:
37
+
38
+ class Post < ActiveRecord::Base
39
+ acts_as_taggable
40
+
41
+ belongs_to :user
42
+ end
43
+
44
+ We can now use the tagging methods provided by acts_as_taggable, <tt>#tag_list</tt> and <tt>#tag_list=</tt>. Both these
45
+ methods work like regular attribute accessors.
46
+
47
+ p = Post.find(:first)
48
+ p.tag_list # []
49
+ p.tag_list = "Funny, Silly"
50
+ p.save
51
+ p.tag_list # ["Funny", "Silly"]
52
+
53
+ You can also add or remove arrays of tags.
54
+
55
+ p.tag_list.add("Great", "Awful")
56
+ p.tag_list.remove("Funny")
57
+
58
+ In your views you should use something like the following:
59
+
60
+ <%= f.label :tag_list %>
61
+ <%= f.text_field :tag_list, :size => 80 %>
62
+
63
+ === Finding tagged objects
64
+
65
+ To retrieve objects tagged with a certain tag, use find_tagged_with.
66
+
67
+ Post.find_tagged_with('Funny, Silly')
68
+
69
+ By default, find_tagged_with will find objects that have any of the given tags. To
70
+ find only objects that are tagged with all the given tags, use match_all.
71
+
72
+ Post.find_tagged_with('Funny, Silly', :match_all => true)
73
+
74
+ See <tt>ActiveRecord::Acts::Taggable::InstanceMethods</tt> for more methods and options.
75
+
76
+ === Tag cloud calculations
77
+
78
+ To construct tag clouds, the frequency of each tag needs to be calculated.
79
+ Because we specified +acts_as_taggable+ on the <tt>Post</tt> class, we can
80
+ get a calculation of all the tag counts by using <tt>Post.tag_counts</tt>. But what if we wanted a tag count for
81
+ an single user's posts? To achieve this we call tag_counts on the association:
82
+
83
+ User.find(:first).posts.tag_counts
84
+
85
+ A helper is included to assist with generating tag clouds. Include it in your helper file:
86
+
87
+ module ApplicationHelper
88
+ include TagsHelper
89
+ end
90
+
91
+ You can also use the <tt>counts</tt> method on <tt>Tag</tt> to get the counts for all tags in the database.
92
+
93
+ Tag.counts
94
+
95
+ Here is an example that generates a tag cloud.
96
+
97
+ Controller:
98
+
99
+ class PostController < ApplicationController
100
+ def tag_cloud
101
+ @tags = Post.tag_counts
102
+ end
103
+ end
104
+
105
+ View:
106
+ <% tag_cloud @tags, %w(css1 css2 css3 css4) do |tag, css_class| %>
107
+ <%= link_to tag.name, { :action => :tag, :id => tag.name }, :class => css_class %>
108
+ <% end %>
109
+
110
+ CSS:
111
+
112
+ .css1 { font-size: 1.0em; }
113
+ .css2 { font-size: 1.2em; }
114
+ .css3 { font-size: 1.4em; }
115
+ .css4 { font-size: 1.6em; }
116
+
117
+ === Caching
118
+
119
+ It is useful to cache the list of tags to reduce the number of queries executed. To do this,
120
+ add a column named <tt>cached_tag_list</tt> to the model which is being tagged. The column should be long enough to hold
121
+ the full tag list and must have a default value of null, not an empty string.
122
+
123
+ class CachePostTagList < ActiveRecord::Migration
124
+ def self.up
125
+ add_column :posts, :cached_tag_list, :string
126
+ end
127
+ end
128
+
129
+ class Post < ActiveRecord::Base
130
+ acts_as_taggable
131
+
132
+ # The caching column defaults to cached_tag_list, but can be changed:
133
+ #
134
+ # set_cached_tag_list_column_name "my_caching_column_name"
135
+ end
136
+
137
+ The details of the caching are handled for you. Just continue to use the tag_list accessor as you normally would.
138
+ Note that the cached tag list will not be updated if you directly create Tagging objects or manually append to the
139
+ <tt>tags</tt> or <tt>taggings</tt> associations. To update the cached tag list you should call <tt>save_cached_tag_list</tt> manually.
140
+
141
+ === Delimiter
142
+
143
+ If you want to change the delimiter used to parse and present tags, set TagList.delimiter.
144
+ For example, to use spaces instead of commas, add the following to config/environment.rb:
145
+
146
+ TagList.delimiter = " "
147
+
148
+ === Unused tags
149
+
150
+ Set Tag.destroy_unused to remove tags when they are no longer being
151
+ used to tag any objects. Defaults to false.
152
+
153
+ Tag.destroy_unused = true
154
+
155
+ === Other
156
+
157
+ Problems, comments, and suggestions all welcome. jonathan.viney@gmail.com
@@ -0,0 +1,246 @@
1
+ require 'tag_list'
2
+ require 'tag'
3
+ require 'tagging'
4
+ require 'tags_helper'
5
+
6
+ module ActiveRecord #:nodoc:
7
+ module Acts #:nodoc:
8
+ module Taggable #:nodoc:
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+ def acts_as_taggable
15
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
16
+ has_many :tags, :through => :taggings
17
+
18
+ before_save :save_cached_tag_list
19
+ after_save :save_tags
20
+
21
+ include ActiveRecord::Acts::Taggable::InstanceMethods
22
+ extend ActiveRecord::Acts::Taggable::SingletonMethods
23
+
24
+ alias_method_chain :reload, :tag_list
25
+ end
26
+
27
+ def cached_tag_list_column_name
28
+ "cached_tag_list"
29
+ end
30
+
31
+ def set_cached_tag_list_column_name(value = nil, &block)
32
+ define_attr_method :cached_tag_list_column_name, value, &block
33
+ end
34
+ end
35
+
36
+ module SingletonMethods
37
+ # Pass either a tag, string, or an array of strings or tags.
38
+ #
39
+ # Options:
40
+ # - +:match_any+ - match any of the given tags (default).
41
+ # - +:match_all+ - match all of the given tags.
42
+ #
43
+ def tagged_with(tags, options = {})
44
+ tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
45
+ return [] if tags.empty?
46
+
47
+ records = select("DISTINCT #{quoted_table_name}.*")
48
+
49
+ if options[:match_all]
50
+ records.search_all_tags(tags)
51
+ else
52
+ records.search_any_tags(tags)
53
+ end
54
+ end
55
+
56
+ # Matches records that have none of the given tags.
57
+ def not_tagged_with(tags)
58
+ tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
59
+
60
+ sub = Tagging.select("#{Tagging.table_name}.taggable_id").joins(:tag).
61
+ where(:taggable_type => base_class.name, "#{Tag.table_name}.name" => tags)
62
+
63
+ where("#{quoted_table_name}.#{primary_key} NOT IN (" + sub.to_sql + ")")
64
+ end
65
+
66
+ # Returns an array of related tags. Related tags are all the other tags
67
+ # that are found on the models tagged with the provided tags.
68
+ def related_tags(tags)
69
+ search_related_tags(tags)
70
+ end
71
+
72
+ # Counts the number of occurences of all tags.
73
+ # See <tt>Tag.counts</tt> for options.
74
+ def tag_counts(options = {})
75
+ tags = Tag.joins(:taggings).
76
+ where("#{Tagging.table_name}.taggable_type" => base_class.name)
77
+
78
+ if options[:tags]
79
+ tags = tags.where("#{Tag.table_name}.name" => options.delete(:tags))
80
+ end
81
+
82
+ unless descends_from_active_record?
83
+ tags = tags.joins("INNER JOIN #{quoted_table_name} ON " +
84
+ "#{quoted_table_name}.#{primary_key} = #{Tagging.quoted_table_name}.taggable_id")
85
+ tags = tags.where(type_condition)
86
+ end
87
+
88
+ if scoped != unscoped
89
+ sub = scoped.except(:select).select("#{quoted_table_name}.#{primary_key}")
90
+ tags = tags.where("#{Tagging.quoted_table_name}.taggable_id IN (#{sub.to_sql})")
91
+ end
92
+
93
+ tags.counts(options)
94
+ end
95
+
96
+ # Returns an array of related tags.
97
+ # Related tags are all the other tags that are found on the models
98
+ # tagged with the provided tags.
99
+ #
100
+ # Pass either a tag, string, or an array of strings or tags.
101
+ #
102
+ # Options:
103
+ # - +:order+ - SQL Order how to order the tags. Defaults to "count_all DESC, tags.name".
104
+ # - +:include+
105
+ #
106
+ # DEPRECATED: use #related_tags instead.
107
+ def find_related_tags(tags, options = {})
108
+ rs = related_tags(tags).order(options[:order] || "count DESC, #{Tag.quoted_table_name}.name")
109
+ rs = rs.includes(options[:include]) if options[:include]
110
+ rs
111
+ end
112
+
113
+ # Pass either a tag, string, or an array of strings or tags.
114
+ #
115
+ # Options:
116
+ # - +:exclude+ - Find models that are not tagged with the given tags
117
+ # - +:match_all+ - Find models that match all of the given tags, not just one
118
+ # - +:conditions+ - A piece of SQL conditions to add to the query
119
+ # - +:include+
120
+ #
121
+ # DEPRECATED: use #tagged_with and #not_tagged_with instead.
122
+ def find_tagged_with(*args)
123
+ options = args.extract_options!
124
+ tags = args.first
125
+
126
+ records = self
127
+ records = records.where(options[:conditions]) if options[:conditions]
128
+ records = records.includes(options[:include]) if options[:include]
129
+ records = records.order(options[:order]) if options[:order]
130
+
131
+ if options[:exclude]
132
+ records.not_tagged_with(tags)
133
+ else
134
+ records.tagged_with(tags, options)
135
+ end
136
+ end
137
+
138
+ def caching_tag_list?
139
+ column_names.include?(cached_tag_list_column_name)
140
+ end
141
+
142
+ protected
143
+ def joins_tags(options = {}) # :nodoc:
144
+ options[:suffix] = "_#{options[:suffix]}" if options[:suffix]
145
+
146
+ taggings_alias = connection.quote_table_name(Tagging.table_name + options[:suffix].to_s)
147
+ tags_alias = connection.quote_table_name(Tag.table_name + options[:suffix].to_s)
148
+
149
+ taggings = "INNER JOIN #{Tagging.quoted_table_name} AS #{taggings_alias} " +
150
+ "ON #{taggings_alias}.taggable_id = #{quoted_table_name}.#{primary_key} " +
151
+ "AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
152
+
153
+ tags = "INNER JOIN #{Tag.quoted_table_name} AS #{tags_alias} " +
154
+ "ON #{tags_alias}.id = #{taggings_alias}.tag_id "
155
+ tags += "AND #{tags_alias}.name LIKE #{quote_value(options[:tag_name])}" if options[:tag_name]
156
+
157
+ joins([taggings, tags])
158
+ end
159
+
160
+ def search_all_tags(tags)
161
+ records = self
162
+
163
+ tags.dup.each_with_index do |tag_name, index|
164
+ records = records.joins_tags(:suffix => index, :tag_name => tag_name)
165
+ end
166
+
167
+ records
168
+ end
169
+
170
+ def search_any_tags(tags)
171
+ joins(:tags).where(Tag.arel_table[:name].matches_any(tags.dup))
172
+ end
173
+
174
+ def search_related_tags(tags)
175
+ tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
176
+ sub = select("#{quoted_table_name}.#{primary_key}").search_any_tags(tags)
177
+ _tags = tags.map { |tag| tag.downcase }
178
+
179
+ Tag.select("#{Tag.quoted_table_name}.*, COUNT(#{Tag.quoted_table_name}.id) AS count").
180
+ joins(:taggings).
181
+ where("#{Tagging.table_name}.taggable_type" => base_class.name).
182
+ where("#{Tagging.quoted_table_name}.taggable_id IN (" + sub.to_sql + ")").
183
+ group("#{Tag.quoted_table_name}.name").
184
+ having(Tag.arel_table[:name].does_not_match_all(_tags))
185
+ end
186
+ end
187
+
188
+ module InstanceMethods
189
+ def tag_list
190
+ return @tag_list if @tag_list
191
+
192
+ if self.class.caching_tag_list? and !(cached_value = send(self.class.cached_tag_list_column_name)).nil?
193
+ @tag_list = TagList.from(cached_value)
194
+ else
195
+ @tag_list = TagList.new(*tags.map(&:name))
196
+ end
197
+ end
198
+
199
+ def tag_list=(value)
200
+ @tag_list = TagList.from(value)
201
+ end
202
+
203
+ def save_cached_tag_list
204
+ if self.class.caching_tag_list?
205
+ self[self.class.cached_tag_list_column_name] = tag_list.to_s
206
+ end
207
+ end
208
+
209
+ def save_tags
210
+ return unless @tag_list
211
+
212
+ new_tag_names = @tag_list - tags.map(&:name)
213
+ old_tags = tags.reject { |tag| @tag_list.include?(tag.name) }
214
+
215
+ self.class.transaction do
216
+ if old_tags.any?
217
+ taggings.where(:tag_id => old_tags.map(&:id)).each(&:destroy)
218
+ taggings.reset
219
+ end
220
+
221
+ new_tag_names.each do |new_tag_name|
222
+ tags << Tag.find_or_create_with_like_by_name(new_tag_name)
223
+ end
224
+ end
225
+
226
+ true
227
+ end
228
+
229
+ # Calculate the tag counts for the tags used by this model.
230
+ # See <tt>Tag.counts</tt> for available options.
231
+ def tag_counts(options = {})
232
+ return [] if tag_list.blank?
233
+ self.class.tag_counts(options.merge(:tags => tag_list))
234
+ end
235
+
236
+ def reload_with_tag_list(*args) #:nodoc:
237
+ @tag_list = nil
238
+ reload_without_tag_list(*args)
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+
245
+ ActiveRecord::Base.send(:include, ActiveRecord::Acts::Taggable)
246
+