protolif-acts_as_taggable_on_steroids 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.markdown +157 -0
  3. data/Rakefile +22 -0
  4. data/app/helpers/tags_helper.rb +13 -0
  5. data/app/models/tag.rb +63 -0
  6. data/app/models/tag_list.rb +109 -0
  7. data/app/models/tagging.rb +16 -0
  8. data/lib/acts_as_taggable.rb +2 -0
  9. data/lib/acts_as_taggable/active_record_extension.rb +244 -0
  10. data/lib/acts_as_taggable/engine.rb +16 -0
  11. data/lib/generators/acts_as_taggable_migration/acts_as_taggable_migration_generator.rb +24 -0
  12. data/lib/generators/acts_as_taggable_migration/templates/migration.rb +22 -0
  13. data/test/acts_as_taggable_test.rb +373 -0
  14. data/test/database.yml +11 -0
  15. data/test/dummy/Rakefile +7 -0
  16. data/test/dummy/app/assets/javascripts/application.js +9 -0
  17. data/test/dummy/app/assets/stylesheets/application.css +7 -0
  18. data/test/dummy/app/controllers/application_controller.rb +3 -0
  19. data/test/dummy/app/helpers/application_helper.rb +2 -0
  20. data/test/dummy/app/models/magazine.rb +3 -0
  21. data/test/dummy/app/models/photo.rb +8 -0
  22. data/test/dummy/app/models/post.rb +7 -0
  23. data/test/dummy/app/models/special_post.rb +2 -0
  24. data/test/dummy/app/models/subscription.rb +4 -0
  25. data/test/dummy/app/models/user.rb +7 -0
  26. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  27. data/test/dummy/config.ru +4 -0
  28. data/test/dummy/config/application.rb +45 -0
  29. data/test/dummy/config/boot.rb +10 -0
  30. data/test/dummy/config/database.yml +25 -0
  31. data/test/dummy/config/environment.rb +5 -0
  32. data/test/dummy/config/environments/development.rb +30 -0
  33. data/test/dummy/config/environments/production.rb +60 -0
  34. data/test/dummy/config/environments/test.rb +42 -0
  35. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  36. data/test/dummy/config/initializers/inflections.rb +10 -0
  37. data/test/dummy/config/initializers/mime_types.rb +5 -0
  38. data/test/dummy/config/initializers/secret_token.rb +7 -0
  39. data/test/dummy/config/initializers/session_store.rb +8 -0
  40. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  41. data/test/dummy/config/locales/en.yml +5 -0
  42. data/test/dummy/config/routes.rb +58 -0
  43. data/test/dummy/db/development.sqlite3 +0 -0
  44. data/test/dummy/db/schema.rb +48 -0
  45. data/test/dummy/db/test.sqlite3 +0 -0
  46. data/test/dummy/log/test.log +2041 -0
  47. data/test/dummy/public/404.html +26 -0
  48. data/test/dummy/public/422.html +26 -0
  49. data/test/dummy/public/500.html +26 -0
  50. data/test/dummy/public/favicon.ico +0 -0
  51. data/test/dummy/script/rails +6 -0
  52. data/test/fixtures/magazines.yml +5 -0
  53. data/test/fixtures/photos.yml +19 -0
  54. data/test/fixtures/posts.yml +27 -0
  55. data/test/fixtures/subscriptions.yml +3 -0
  56. data/test/fixtures/taggings.yml +107 -0
  57. data/test/fixtures/tags.yml +14 -0
  58. data/test/fixtures/users.yml +5 -0
  59. data/test/support/query_counter.rb +29 -0
  60. data/test/tag_list_test.rb +119 -0
  61. data/test/tag_test.rb +63 -0
  62. data/test/tagging_test.rb +11 -0
  63. data/test/tags_helper_test.rb +25 -0
  64. data/test/test_helper.rb +61 -0
  65. metadata +189 -0
@@ -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.
@@ -0,0 +1,157 @@
1
+ ActsAsTaggableOnSteroids
2
+ ========================
3
+
4
+ [![Build Status](https://secure.travis-ci.org/bborn/acts_as_taggable_on_steroids.png)](http://travis-ci.org/bborn/acts_as_taggable_on_steroids)
5
+
6
+
7
+ Instructions
8
+ ------------
9
+
10
+ This plugin is based on acts_as_taggable by DHH but includes extras
11
+ such as tests, smarter tag assignment, and tag cloud calculations.
12
+
13
+ Installation
14
+ ------------
15
+
16
+ gem "bborn-acts_as_taggable_on_steroids"
17
+
18
+
19
+ Usage
20
+ =====
21
+
22
+ Prepare database
23
+ ----------------
24
+
25
+ Generate and apply the migration:
26
+
27
+ ruby script/generate acts_as_taggable_migration
28
+ rake db:migrate
29
+
30
+ Basic tagging
31
+ -------------
32
+
33
+ Let's suppose users have many posts and we want those posts to have tags.
34
+ The first step is to add `acts_as_taggable` to the Post class:
35
+
36
+ class Post < ActiveRecord::Base
37
+ acts_as_taggable
38
+
39
+ belongs_to :user
40
+ end
41
+
42
+ We can now use the tagging methods provided by acts_as_taggable, `#tag_list` and `#tag_list=`. Both these
43
+ methods work like regular attribute accessors.
44
+
45
+ p = Post.find(:first)
46
+ p.tag_list # []
47
+ p.tag_list = "Funny, Silly"
48
+ p.save
49
+ p.tag_list # ["Funny", "Silly"]
50
+
51
+ You can also add or remove arrays of tags.
52
+
53
+ p.tag_list.add("Great", "Awful")
54
+ p.tag_list.remove("Funny")
55
+
56
+ In your views you should use something like the following:
57
+
58
+ <%= f.label :tag_list %>
59
+ <%= f.text_field :tag_list, :size => 80 %>
60
+
61
+ Finding tagged objects
62
+ ----------------------
63
+
64
+ To retrieve objects tagged with a certain tag, use find_tagged_with.
65
+
66
+ Post.tagged_with('Funny, Silly')
67
+
68
+ By default, find_tagged_with will find objects that have any of the given tags. To
69
+ find only objects that are tagged with all the given tags, use match_all.
70
+
71
+ Post.tagged_with('Funny, Silly', :match_all => true)
72
+
73
+ See `ActsAsTaggable::ActiveRecordExtension::InstanceMethods` for more methods and options.
74
+
75
+ Tag cloud calculations
76
+ ----------------------
77
+
78
+ To construct tag clouds, the frequency of each tag needs to be calculated.
79
+ Because we specified `acts_as_taggable` on the `Post` class, we can
80
+ get a calculation of all the tag counts by using `Post.tag_counts`. 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 `counts` method on `Tag` 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
+
107
+ <% tag_cloud @tags, %w(css1 css2 css3 css4) do |tag, css_class| %>
108
+ <%= link_to tag.name, { :action => :tag, :id => tag.name }, :class => css_class %>
109
+ <% end %>
110
+
111
+ CSS:
112
+
113
+ .css1 { font-size: 1.0em; }
114
+ .css2 { font-size: 1.2em; }
115
+ .css3 { font-size: 1.4em; }
116
+ .css4 { font-size: 1.6em; }
117
+
118
+ Caching
119
+ -------
120
+
121
+ It is useful to cache the list of tags to reduce the number of queries executed. To do this,
122
+ add a column named `cached_tag_list` to the model which is being tagged. The column should be long enough to hold
123
+ the full tag list and must have a default value of null, not an empty string.
124
+
125
+ class CachePostTagList < ActiveRecord::Migration
126
+ def self.up
127
+ add_column :posts, :cached_tag_list, :string
128
+ end
129
+ end
130
+
131
+ class Post < ActiveRecord::Base
132
+ acts_as_taggable
133
+
134
+ # The caching column defaults to cached_tag_list, but can be changed:
135
+ #
136
+ # set_cached_tag_list_column_name "my_caching_column_name"
137
+ end
138
+
139
+ The details of the caching are handled for you. Just continue to use the tag_list accessor as you normally would.
140
+ Note that the cached tag list will not be updated if you directly create Tagging objects or manually append to the
141
+ `tags` or `taggings` associations. To update the cached tag list you should call `save_cached_tag_list` manually.
142
+
143
+ Delimiter
144
+ ---------
145
+
146
+ If you want to change the delimiter used to parse and present tags, set TagList.delimiter.
147
+ For example, to use spaces instead of commas, add the following to your `application.rb`:
148
+
149
+ TagList.delimiter = " "
150
+
151
+ Unused tags
152
+ -----------
153
+
154
+ Set Tag.destroy_unused to remove tags when they are no longer being
155
+ used to tag any objects. Defaults to false.
156
+
157
+ Tag.destroy_unused = true
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rdoc/task'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the acts_as_taggable_on_steroids plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'test'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the acts_as_taggable_on_steroids plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'Acts As Taggable On Steroids'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
@@ -0,0 +1,13 @@
1
+ module TagsHelper
2
+ # See the README for an example using tag_cloud.
3
+ def tag_cloud(tags, classes)
4
+ return if tags.all.empty?
5
+
6
+ max_count = tags.sort_by(&:count).last.count.to_f
7
+
8
+ tags.each do |tag|
9
+ index = ((tag.count / max_count) * (classes.size - 1)).round
10
+ yield tag, classes[index]
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,63 @@
1
+ require 'active_support/core_ext/module/deprecation'
2
+
3
+ class Tag < ActiveRecord::Base
4
+ cattr_accessor :destroy_unused
5
+ self.destroy_unused = false
6
+
7
+ has_many :taggings, :dependent => :delete_all
8
+
9
+ validates_presence_of :name
10
+ validates_uniqueness_of :name
11
+
12
+ def ==(object)
13
+ super || (object.is_a?(Tag) && name == object.name)
14
+ end
15
+
16
+ def to_s
17
+ name
18
+ end
19
+
20
+ def count
21
+ read_attribute(:count).to_i
22
+ end
23
+
24
+ class << self
25
+ def find_or_create_with_like_by_name(name)
26
+ where(arel_table[:name].matches(name)).first || create(:name => name)
27
+ end
28
+
29
+ # Calculate the tag counts for all tags.
30
+ #
31
+ # - +:start_at+ - restrict the tags to those created after a certain time
32
+ # - +:end_at+ - restrict the tags to those created before a certain time
33
+ # - +:at_least+ - exclude tags with a frequency less than the given value
34
+ # - +:at_most+ - exclude tags with a frequency greater than the given value
35
+ #
36
+ # Deprecated:
37
+ #
38
+ # - +:conditions+
39
+ # - +:limit+
40
+ # - +:order+
41
+ #
42
+ def counts(options = {})
43
+ options.assert_valid_keys :start_at, :end_at, :at_least, :at_most, :conditions, :limit, :order, :joins
44
+
45
+ tags = joins(:taggings)
46
+ tags = tags.having(["COUNT(#{quoted_table_name}.id) >= ?", options[:at_least]]) if options[:at_least]
47
+ tags = tags.having(["COUNT(#{quoted_table_name}.id) <= ?", options[:at_most]]) if options[:at_most]
48
+ tags = tags.where("#{Tagging.quoted_table_name}.created_at >= ?", options[:start_at]) if options[:start_at]
49
+ tags = tags.where("#{Tagging.quoted_table_name}.created_at <= ?", options[:end_at]) if options[:end_at]
50
+
51
+ # TODO: deprecation warning
52
+ tags = tags.where(options[:conditions]) if options[:conditions]
53
+ tags = tags.limit(options[:limit]) if options[:limit]
54
+ tags = tags.order(options[:order]) if options[:order]
55
+
56
+ if joins = options.delete(:joins)
57
+ tags = tags.joins(joins)
58
+ end
59
+
60
+ tags.select("#{quoted_table_name}.id, #{quoted_table_name}.name, COUNT(#{quoted_table_name}.id) AS count").group('tags.id, tags.name')
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,109 @@
1
+ class TagList < Array
2
+ cattr_accessor :delimiter
3
+ self.delimiter = ','
4
+
5
+ def initialize(*args)
6
+ add(*args)
7
+ end
8
+
9
+ # Add tags to the tag_list. Duplicate or blank tags will be ignored.
10
+ #
11
+ # tag_list.add("Fun", "Happy")
12
+ #
13
+ # Use the <tt>:parse</tt> option to add an unparsed tag string.
14
+ #
15
+ # tag_list.add("Fun, Happy", :parse => true)
16
+ def add(*names)
17
+ extract_and_apply_options!(names)
18
+ concat(names)
19
+ clean!
20
+ self
21
+ end
22
+
23
+ # Remove specific tags from the tag_list.
24
+ #
25
+ # tag_list.remove("Sad", "Lonely")
26
+ #
27
+ # Like #add, the <tt>:parse</tt> option can be used to remove multiple tags in a string.
28
+ #
29
+ # tag_list.remove("Sad, Lonely", :parse => true)
30
+ def remove(*names)
31
+ extract_and_apply_options!(names)
32
+ delete_if { |name| names.include?(name) }
33
+ self
34
+ end
35
+
36
+ # Toggle the presence of the given tags.
37
+ # If a tag is already in the list it is removed, otherwise it is added.
38
+ def toggle(*names)
39
+ extract_and_apply_options!(names)
40
+
41
+ names.each do |name|
42
+ include?(name) ? delete(name) : push(name)
43
+ end
44
+
45
+ clean!
46
+ self
47
+ end
48
+
49
+ # Transform the tag_list into a tag string suitable for edting in a form.
50
+ # The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
51
+ #
52
+ # tag_list = TagList.new("Round", "Square,Cube")
53
+ # tag_list.to_s # 'Round, "Square,Cube"'
54
+ def to_s
55
+ clean!
56
+
57
+ map do |name|
58
+ name.include?(delimiter) ? "\"#{name}\"" : name
59
+ end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
60
+ end
61
+
62
+ private
63
+ # Remove whitespace, duplicates, and blanks.
64
+ def clean!
65
+ reject!(&:blank?)
66
+ map!(&:strip)
67
+ uniq!
68
+ end
69
+
70
+ def extract_and_apply_options!(args)
71
+ options = args.last.is_a?(Hash) ? args.pop : {}
72
+ options.assert_valid_keys :parse
73
+
74
+ if options[:parse]
75
+ args.map! { |a| self.class.from(a) }
76
+ end
77
+
78
+ args.flatten!
79
+ end
80
+
81
+ class << self
82
+ # Returns a new TagList using the given tag string.
83
+ #
84
+ # tag_list = TagList.from("One , Two, Three")
85
+ # tag_list # ["One", "Two", "Three"]
86
+ def from(source)
87
+ tag_list = new
88
+
89
+ case source
90
+ when Array
91
+ tag_list.add(source)
92
+ else
93
+ string = source.to_s.dup
94
+
95
+ # Parse the quoted tags
96
+ [
97
+ /\s*#{delimiter}\s*(['"])(.*?)\1\s*/,
98
+ /^\s*(['"])(.*?)\1\s*#{delimiter}?/
99
+ ].each do |re|
100
+ string.gsub!(re) { tag_list << $2; "" }
101
+ end
102
+
103
+ tag_list.add(string.split(delimiter))
104
+ end
105
+
106
+ tag_list
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,16 @@
1
+ class Tagging < ActiveRecord::Base #:nodoc:
2
+ belongs_to :tag
3
+ belongs_to :taggable, :polymorphic => true
4
+
5
+ after_destroy :destroy_tag_if_unused
6
+
7
+ private
8
+
9
+ def destroy_tag_if_unused
10
+ if Tag.destroy_unused
11
+ if tag.taggings.count.zero?
12
+ tag.destroy
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,2 @@
1
+
2
+ require File.join(File.dirname(__FILE__), 'acts_as_taggable/engine')
@@ -0,0 +1,244 @@
1
+
2
+ module ActsAsTaggable #:nodoc:
3
+ module ActiveRecordExtension
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ self.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def acts_as_taggable
12
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
13
+ has_many :tags, :through => :taggings
14
+
15
+ before_save :save_cached_tag_list
16
+ after_save :save_tags
17
+
18
+ #include ActsAsTaggable::ActiveRecordExtension::InstanceMethods
19
+ extend ActsAsTaggable::ActiveRecordExtension::SingletonMethods
20
+
21
+ alias_method_chain :reload, :tag_list
22
+ end
23
+
24
+ def cached_tag_list_column_name
25
+ "cached_tag_list"
26
+ end
27
+
28
+ def set_cached_tag_list_column_name(value = nil, &block)
29
+ define_attr_method :cached_tag_list_column_name, value, &block
30
+ end
31
+ end
32
+
33
+ module SingletonMethods
34
+ # Pass either a tag, string, or an array of strings or tags.
35
+ #
36
+ # Options:
37
+ # - +:match_any+ - match any of the given tags (default).
38
+ # - +:match_all+ - match all of the given tags.
39
+ #
40
+ def tagged_with(tags, options = {})
41
+ tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
42
+ return [] if tags.empty?
43
+
44
+ records = select("DISTINCT #{quoted_table_name}.*")
45
+
46
+ if options[:match_all]
47
+ records.search_all_tags(tags)
48
+ else
49
+ records.search_any_tags(tags)
50
+ end
51
+ end
52
+
53
+ # Matches records that have none of the given tags.
54
+ def not_tagged_with(tags)
55
+ tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
56
+
57
+ sub = Tagging.select("#{Tagging.table_name}.taggable_id").joins(:tag).
58
+ where(:taggable_type => base_class.name, "#{Tag.table_name}.name" => tags)
59
+
60
+ where("#{quoted_table_name}.#{primary_key} NOT IN (" + sub.to_sql + ")")
61
+ end
62
+
63
+ # Returns an array of related tags. Related tags are all the other tags
64
+ # that are found on the models tagged with the provided tags.
65
+ def related_tags(tags)
66
+ search_related_tags(tags)
67
+ end
68
+
69
+ # Counts the number of occurences of all tags.
70
+ # See <tt>Tag.counts</tt> for options.
71
+ def tag_counts(options = {})
72
+ tags = Tag.joins(:taggings).
73
+ where("#{Tagging.table_name}.taggable_type" => base_class.name)
74
+
75
+ if options[:tags]
76
+ tags = tags.where("#{Tag.table_name}.name" => options.delete(:tags))
77
+ end
78
+
79
+ unless descends_from_active_record?
80
+ tags = tags.joins("INNER JOIN #{quoted_table_name} ON " +
81
+ "#{quoted_table_name}.#{primary_key} = #{Tagging.quoted_table_name}.taggable_id")
82
+ tags = tags.where(type_condition)
83
+ end
84
+
85
+ if scoped != unscoped
86
+ sub = scoped.except(:select).select("#{quoted_table_name}.#{primary_key}")
87
+ tags = tags.where("#{Tagging.quoted_table_name}.taggable_id IN (#{sub.to_sql})")
88
+ end
89
+
90
+ tags.counts(options)
91
+ end
92
+
93
+ # Returns an array of related tags.
94
+ # Related tags are all the other tags that are found on the models
95
+ # tagged with the provided tags.
96
+ #
97
+ # Pass either a tag, string, or an array of strings or tags.
98
+ #
99
+ # Options:
100
+ # - +:order+ - SQL Order how to order the tags. Defaults to "count_all DESC, tags.name".
101
+ # - +:include+
102
+ #
103
+ # DEPRECATED: use #related_tags instead.
104
+ def find_related_tags(tags, options = {})
105
+ ActiveSupport::Deprecation.warn "#find_related_tags() is deprecated and will be removed in the next release. Use #related_tags", caller
106
+
107
+ rs = related_tags(tags).order(options[:order] || "count DESC, #{Tag.quoted_table_name}.name")
108
+ rs = rs.includes(options[:include]) if options[:include]
109
+ rs
110
+ end
111
+
112
+ # Pass either a tag, string, or an array of strings or tags.
113
+ #
114
+ # Options:
115
+ # - +:exclude+ - Find models that are not tagged with the given tags
116
+ # - +:match_all+ - Find models that match all of the given tags, not just one
117
+ # - +:conditions+ - A piece of SQL conditions to add to the query
118
+ # - +:include+
119
+ #
120
+ # DEPRECATED: use #tagged_with and #not_tagged_with instead.
121
+ def find_tagged_with(*args)
122
+ ActiveSupport::Deprecation.warn "#find_tagged_with() is deprecated and will be removed in the next release. Use #tagged_with and #not_tagged_with instead", caller
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
+
177
+ return where('1=0') if tags.empty?
178
+
179
+ sub = select("#{quoted_table_name}.#{primary_key}").search_any_tags(tags)
180
+ _tags = tags.map { |tag| tag.downcase }
181
+
182
+ Tag.select("#{Tag.quoted_table_name}.*, COUNT(#{Tag.quoted_table_name}.id) AS count").
183
+ joins(:taggings).
184
+ where("#{Tagging.table_name}.taggable_type" => base_class.name).
185
+ where("#{Tagging.quoted_table_name}.taggable_id IN (" + sub.to_sql + ")").
186
+ group("#{Tag.quoted_table_name}.name").
187
+ having(Tag.arel_table[:name].does_not_match_all(_tags))
188
+ end
189
+ end
190
+
191
+ def tag_list
192
+ return @tag_list if @tag_list
193
+
194
+ if self.class.caching_tag_list? and !(cached_value = send(self.class.cached_tag_list_column_name)).nil?
195
+ @tag_list = TagList.from(cached_value)
196
+ else
197
+ @tag_list = TagList.new(*tags.map(&:name))
198
+ end
199
+ end
200
+
201
+ def tag_list=(value)
202
+ @tag_list = TagList.from(value)
203
+ end
204
+
205
+ def save_cached_tag_list
206
+ if self.class.caching_tag_list?
207
+ self[self.class.cached_tag_list_column_name] = tag_list.to_s
208
+ end
209
+ end
210
+
211
+ def save_tags
212
+ return unless @tag_list
213
+
214
+ new_tag_names = @tag_list - tags.map(&:name)
215
+ old_tags = tags.reject { |tag| @tag_list.include?(tag.name) }
216
+
217
+ self.class.transaction do
218
+ if old_tags.any?
219
+ taggings.where(:tag_id => old_tags.map(&:id)).each(&:destroy)
220
+ taggings.reset
221
+ end
222
+
223
+ new_tag_names.each do |new_tag_name|
224
+ tags << Tag.find_or_create_with_like_by_name(new_tag_name)
225
+ end
226
+ end
227
+
228
+ true
229
+ end
230
+
231
+ # Calculate the tag counts for the tags used by this model.
232
+ # See <tt>Tag.counts</tt> for available options.
233
+ def tag_counts(options = {})
234
+ return [] if tag_list.blank?
235
+ self.class.tag_counts(options.merge(:tags => tag_list))
236
+ end
237
+
238
+ def reload_with_tag_list(*args) #:nodoc:
239
+ @tag_list = nil
240
+ reload_without_tag_list(*args)
241
+ end
242
+ end
243
+ end
244
+