acts_as_taggable3 2.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+