├── .rspec
├── Gemfile
├── lib
├── acts_as_taggable_on
│ ├── version.rb
│ ├── tags_helper.rb
│ ├── utils.rb
│ ├── tagging.rb
│ ├── acts_as_taggable_on
│ │ ├── compatibility.rb
│ │ ├── dirty.rb
│ │ ├── cache.rb
│ │ ├── related.rb
│ │ ├── ownership.rb
│ │ ├── collection.rb
│ │ └── core.rb
│ ├── tag_list.rb
│ ├── tagger.rb
│ ├── tag.rb
│ └── taggable.rb
├── generators
│ └── acts_as_taggable_on
│ │ └── migration
│ │ ├── templates
│ │ └── active_record
│ │ │ └── migration.rb
│ │ └── migration_generator.rb
└── acts-as-taggable-on.rb
├── Appraisals
├── .gitignore
├── gemfiles
├── rails_3.gemfile
└── rails_4.gemfile
├── Guardfile
├── .travis.yml
├── Rakefile
├── spec
├── database.yml.sample
├── acts_as_taggable_on
│ ├── utils_spec.rb
│ ├── tagging_spec.rb
│ ├── tags_helper_spec.rb
│ ├── caching_spec.rb
│ ├── acts_as_tagger_spec.rb
│ ├── tag_list_spec.rb
│ ├── related_spec.rb
│ ├── tagger_spec.rb
│ ├── tag_spec.rb
│ ├── single_table_inheritance_spec.rb
│ ├── acts_as_taggable_on_spec.rb
│ └── taggable_spec.rb
├── generators
│ └── acts_as_taggable_on
│ │ └── migration
│ │ └── migration_generator_spec.rb
├── models.rb
├── bm.rb
├── schema.rb
└── spec_helper.rb
├── LICENSE.md
├── acts-as-taggable-on.gemspec
└── README.md
/.rspec:
--------------------------------------------------------------------------------
1 | --colour
2 | --backtrace
3 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec
4 |
5 | gem 'appraisal'
6 |
--------------------------------------------------------------------------------
/lib/acts_as_taggable_on/version.rb:
--------------------------------------------------------------------------------
1 | module ActsAsTaggableOn
2 | VERSION = '2.4.2.pre'
3 | end
4 |
5 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | appraise "rails-3" do
2 | gem "rails", "3.2.13"
3 | end
4 |
5 | appraise "rails-4" do
6 | gem "rails", "4.0.0.beta1"
7 | end
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | *.sqlite3
3 | /pkg/*
4 | .bundle
5 | .ruby-version
6 | spec/database.yml
7 | tmp*.sw?
8 | *.sw?
9 | tmp
10 | *.gem
11 | *.lock
12 |
--------------------------------------------------------------------------------
/gemfiles/rails_3.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "rails", "3.2.13"
7 |
8 | gemspec :path=>"../"
--------------------------------------------------------------------------------
/Guardfile:
--------------------------------------------------------------------------------
1 | guard 'rspec' do
2 | watch(%r{^spec/.+_spec\.rb})
3 | watch(%r{^lib/(.+)\.rb}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4 | watch('spec/spec_helper.rb') { "spec" }
5 | end
6 |
--------------------------------------------------------------------------------
/gemfiles/rails_4.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "rails", :github => 'rails/rails'
7 |
8 | gemspec :path=>"../"
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | script: "cp spec/database.yml.sample spec/database.yml && bundle install && bundle exec rake"
2 | rvm:
3 | - 1.8.7
4 | - 1.9.2
5 | - 1.9.3
6 | env:
7 | - DB=sqlite3
8 | - DB=mysql
9 | - DB=postgresql
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'bundler/setup'
3 | require 'appraisal'
4 |
5 | desc 'Default: run specs'
6 | task :default => :spec
7 |
8 | require 'rspec/core/rake_task'
9 | RSpec::Core::RakeTask.new do |t|
10 | t.pattern = "spec/**/*_spec.rb"
11 | end
12 |
13 | Bundler::GemHelper.install_tasks
14 |
--------------------------------------------------------------------------------
/spec/database.yml.sample:
--------------------------------------------------------------------------------
1 | sqlite3:
2 | adapter: sqlite3
3 | database: acts_as_taggable_on.sqlite3
4 |
5 | mysql:
6 | adapter: mysql2
7 | hostname: localhost
8 | username: root
9 | password:
10 | database: acts_as_taggable_on
11 | charset: utf8
12 |
13 | postgresql:
14 | adapter: postgresql
15 | hostname: localhost
16 | username: postgres
17 | password:
18 | database: acts_as_taggable_on
19 | encoding: utf8
20 |
--------------------------------------------------------------------------------
/lib/acts_as_taggable_on/tags_helper.rb:
--------------------------------------------------------------------------------
1 | module ActsAsTaggableOn
2 | module TagsHelper
3 | # See the README for an example using tag_cloud.
4 | def tag_cloud(tags, classes)
5 | return [] if tags.empty?
6 |
7 | max_count = tags.sort_by(&:count).last.count.to_f
8 |
9 | tags.each do |tag|
10 | index = ((tag.count / max_count) * (classes.size - 1))
11 | yield tag, classes[index.nan? ? 0 : index.round]
12 | end
13 | end
14 | end
15 | end
--------------------------------------------------------------------------------
/spec/acts_as_taggable_on/utils_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActsAsTaggableOn::Utils do
4 | describe "like_operator" do
5 | before(:each) do
6 | clean_database!
7 | TaggableModel.acts_as_taggable_on(:tags, :languages, :skills, :needs, :offerings)
8 | @taggable = TaggableModel.new(:name => "Bob Jones")
9 | end
10 |
11 | it "should return 'ILIKE' when the adapter is PostgreSQL" do
12 | TaggableModel.connection.stub(:adapter_name).and_return("PostgreSQL")
13 | TaggableModel.send(:like_operator).should == "ILIKE"
14 | end
15 |
16 | it "should return 'LIKE' when the adapter is not PostgreSQL" do
17 | TaggableModel.connection.stub(:adapter_name).and_return("MySQL")
18 | TaggableModel.send(:like_operator).should == "LIKE"
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/generators/acts_as_taggable_on/migration/migration_generator_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | # Generators are not automatically loaded by Rails
4 | require 'generators/acts_as_taggable_on/migration/migration_generator'
5 |
6 | describe ActsAsTaggableOn::MigrationGenerator do
7 | # Tell the generator where to put its output (what it thinks of as Rails.root)
8 | destination File.expand_path("../../../../../tmp", __FILE__)
9 |
10 | before do
11 | prepare_destination
12 | Rails::Generators.options[:rails][:orm] = :active_record
13 | end
14 | describe 'no arguments' do
15 | before { run_generator }
16 |
17 | describe 'db/migrate/acts_as_taggable_on_migration.rb' do
18 | subject { file('db/migrate/acts_as_taggable_on_migration.rb') }
19 | it { should be_a_migration }
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb:
--------------------------------------------------------------------------------
1 | class ActsAsTaggableOnMigration < ActiveRecord::Migration
2 | def self.up
3 | create_table :tags do |t|
4 | t.string :name
5 | end
6 |
7 | create_table :taggings do |t|
8 | t.references :tag
9 |
10 | # You should make sure that the column created is
11 | # long enough to store the required class names.
12 | t.references :taggable, :polymorphic => true
13 | t.references :tagger, :polymorphic => true
14 |
15 | # Limit is created to prevent MySQL error on index
16 | # length for MyISAM table type: http://bit.ly/vgW2Ql
17 | t.string :context, :limit => 128
18 |
19 | t.datetime :created_at
20 | end
21 |
22 | add_index :taggings, :tag_id
23 | add_index :taggings, [:taggable_id, :taggable_type, :context]
24 | end
25 |
26 | def self.down
27 | drop_table :taggings
28 | drop_table :tags
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/acts_as_taggable_on/tagging_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActsAsTaggableOn::Tagging do
4 | before(:each) do
5 | clean_database!
6 | @tagging = ActsAsTaggableOn::Tagging.new
7 | end
8 |
9 | it "should not be valid with a invalid tag" do
10 | @tagging.taggable = TaggableModel.create(:name => "Bob Jones")
11 | @tagging.tag = ActsAsTaggableOn::Tag.new(:name => "")
12 | @tagging.context = "tags"
13 |
14 | @tagging.should_not be_valid
15 |
16 | @tagging.errors[:tag_id].should == ["can't be blank"]
17 | end
18 |
19 | it "should not create duplicate taggings" do
20 | @taggable = TaggableModel.create(:name => "Bob Jones")
21 | @tag = ActsAsTaggableOn::Tag.create(:name => "awesome")
22 |
23 | lambda {
24 | 2.times { ActsAsTaggableOn::Tagging.create(:taggable => @taggable, :tag => @tag, :context => 'tags') }
25 | }.should change(ActsAsTaggableOn::Tagging, :count).by(1)
26 | end
27 |
28 | end
29 |
--------------------------------------------------------------------------------
/lib/acts_as_taggable_on/utils.rb:
--------------------------------------------------------------------------------
1 | module ActsAsTaggableOn
2 | module Utils
3 | def self.included(base)
4 |
5 | base.send :include, ActsAsTaggableOn::Utils::OverallMethods
6 | base.extend ActsAsTaggableOn::Utils::OverallMethods
7 | end
8 |
9 | module OverallMethods
10 | def using_postgresql?
11 | ::ActiveRecord::Base.connection && ::ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
12 | end
13 |
14 | def using_sqlite?
15 | ::ActiveRecord::Base.connection && ::ActiveRecord::Base.connection.adapter_name == 'SQLite'
16 | end
17 |
18 | def sha_prefix(string)
19 | Digest::SHA1.hexdigest("#{string}#{rand}")[0..6]
20 | end
21 |
22 | private
23 | def like_operator
24 | using_postgresql? ? 'ILIKE' : 'LIKE'
25 | end
26 |
27 | # escape _ and % characters in strings, since these are wildcards in SQL.
28 | def escape_like(str)
29 | str.gsub(/[!%_]/){ |x| '!' + x }
30 | end
31 | end
32 |
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/acts_as_taggable_on/tagging.rb:
--------------------------------------------------------------------------------
1 | module ActsAsTaggableOn
2 | class Tagging < ::ActiveRecord::Base #:nodoc:
3 | attr_accessible :tag,
4 | :tag_id,
5 | :context,
6 | :taggable,
7 | :taggable_type,
8 | :taggable_id,
9 | :tagger,
10 | :tagger_type,
11 | :tagger_id if defined?(ActiveModel::MassAssignmentSecurity)
12 |
13 | belongs_to :tag, :class_name => 'ActsAsTaggableOn::Tag'
14 | belongs_to :taggable, :polymorphic => true
15 | belongs_to :tagger, :polymorphic => true
16 |
17 | validates_presence_of :context
18 | validates_presence_of :tag_id
19 |
20 | validates_uniqueness_of :tag_id, :scope => [ :taggable_type, :taggable_id, :context, :tagger_id, :tagger_type ]
21 |
22 | after_destroy :remove_unused_tags
23 |
24 | private
25 |
26 | def remove_unused_tags
27 | if ActsAsTaggableOn.remove_unused_tags
28 | if tag.taggings.count.zero?
29 | tag.destroy
30 | end
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | __Copyright (c) 2007 Michael Bleigh and Intridea Inc.__
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.
21 |
--------------------------------------------------------------------------------
/lib/acts_as_taggable_on/acts_as_taggable_on/compatibility.rb:
--------------------------------------------------------------------------------
1 | module ActsAsTaggableOn::Compatibility
2 | def has_many_with_compatibility(name, options = {}, &extention)
3 | if ActiveRecord::VERSION::MAJOR >= 4
4 | scope, opts = build_scope_and_options(options)
5 | has_many(name, scope, opts, &extention)
6 | else
7 | has_many(name, options, &extention)
8 | end
9 | end
10 |
11 | def build_scope_and_options(opts)
12 | scope_opts, opts = parse_options(opts)
13 |
14 | unless scope_opts.empty?
15 | scope = lambda do
16 | scope_opts.inject(self) { |result, hash| result.send *hash }
17 | end
18 | end
19 |
20 | [defined?(scope) ? scope : nil, opts]
21 | end
22 |
23 | def parse_options(opts)
24 | scope_opts = {}
25 | [:order, :having, :select, :group, :limit, :offset, :readonly].each do |o|
26 | scope_opts[o] = opts.delete o if opts[o]
27 | end
28 | scope_opts[:where] = opts.delete :conditions if opts[:conditions]
29 | scope_opts[:joins] = opts.delete :include if opts [:include]
30 | scope_opts[:distinct] = opts.delete :uniq if opts[:uniq]
31 |
32 | [scope_opts, opts]
33 | end
34 | end
--------------------------------------------------------------------------------
/lib/generators/acts_as_taggable_on/migration/migration_generator.rb:
--------------------------------------------------------------------------------
1 | require 'rails/generators'
2 | require 'rails/generators/migration'
3 |
4 | module ActsAsTaggableOn
5 | class MigrationGenerator < Rails::Generators::Base
6 | include Rails::Generators::Migration
7 |
8 | desc "Generates migration for Tag and Tagging models"
9 |
10 | def self.orm
11 | Rails::Generators.options[:rails][:orm]
12 | end
13 |
14 | def self.source_root
15 | File.join(File.dirname(__FILE__), 'templates', (orm.to_s unless orm.class.eql?(String)) )
16 | end
17 |
18 | def self.orm_has_migration?
19 | [:active_record].include? orm
20 | end
21 |
22 | def self.next_migration_number(dirname)
23 | if ActiveRecord::Base.timestamped_migrations
24 | migration_number = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
25 | migration_number += 1
26 | migration_number.to_s
27 | else
28 | "%.3d" % (current_migration_number(dirname) + 1)
29 | end
30 | end
31 |
32 | def create_migration_file
33 | if self.class.orm_has_migration?
34 | migration_template 'migration.rb', 'db/migrate/acts_as_taggable_on_migration'
35 | end
36 | end
37 | end
38 | end
39 |
40 |
--------------------------------------------------------------------------------
/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb:
--------------------------------------------------------------------------------
1 | module ActsAsTaggableOn::Taggable
2 | module Dirty
3 | def self.included(base)
4 | base.extend ActsAsTaggableOn::Taggable::Dirty::ClassMethods
5 |
6 | base.initialize_acts_as_taggable_on_dirty
7 | end
8 |
9 | module ClassMethods
10 | def initialize_acts_as_taggable_on_dirty
11 | tag_types.map(&:to_s).each do |tags_type|
12 | tag_type = tags_type.to_s.singularize
13 | context_tags = tags_type.to_sym
14 |
15 | class_eval <<-RUBY, __FILE__, __LINE__ + 1
16 | def #{tag_type}_list_changed?
17 | changed_attributes.include?("#{tag_type}_list")
18 | end
19 |
20 | def #{tag_type}_list_was
21 | changed_attributes.include?("#{tag_type}_list") ? changed_attributes["#{tag_type}_list"] : __send__("#{tag_type}_list")
22 | end
23 |
24 | def #{tag_type}_list_change
25 | [changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
26 | end
27 |
28 | def #{tag_type}_list_changes
29 | [changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
30 | end
31 | RUBY
32 |
33 | end
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/models.rb:
--------------------------------------------------------------------------------
1 | class TaggableModel < ActiveRecord::Base
2 | acts_as_taggable
3 | acts_as_taggable_on :languages
4 | acts_as_taggable_on :skills
5 | acts_as_taggable_on :needs, :offerings
6 | has_many :untaggable_models
7 |
8 | attr_reader :tag_list_submethod_called
9 | def tag_list=v
10 | @tag_list_submethod_called = true
11 | super
12 | end
13 | end
14 |
15 | class CachedModel < ActiveRecord::Base
16 | acts_as_taggable
17 | end
18 |
19 | class OtherCachedModel < ActiveRecord::Base
20 | acts_as_taggable_on :languages, :statuses, :glasses
21 | end
22 |
23 | class OtherTaggableModel < ActiveRecord::Base
24 | acts_as_taggable_on :tags, :languages
25 | acts_as_taggable_on :needs, :offerings
26 | end
27 |
28 | class InheritingTaggableModel < TaggableModel
29 | end
30 |
31 | class AlteredInheritingTaggableModel < TaggableModel
32 | acts_as_taggable_on :parts
33 | end
34 |
35 | class User < ActiveRecord::Base
36 | acts_as_tagger
37 | end
38 |
39 | class Student < User
40 | end
41 |
42 | class UntaggableModel < ActiveRecord::Base
43 | belongs_to :taggable_model
44 | end
45 |
46 | class NonStandardIdTaggableModel < ActiveRecord::Base
47 | primary_key = "an_id"
48 | acts_as_taggable
49 | acts_as_taggable_on :languages
50 | acts_as_taggable_on :skills
51 | acts_as_taggable_on :needs, :offerings
52 | has_many :untaggable_models
53 | end
54 |
55 | class OrderedTaggableModel < ActiveRecord::Base
56 | acts_as_ordered_taggable
57 | acts_as_ordered_taggable_on :colours
58 | end
59 |
--------------------------------------------------------------------------------
/spec/acts_as_taggable_on/tags_helper_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActsAsTaggableOn::TagsHelper do
4 | before(:each) do
5 | clean_database!
6 |
7 | @bob = TaggableModel.create(:name => "Bob Jones", :language_list => "ruby, php")
8 | @tom = TaggableModel.create(:name => "Tom Marley", :language_list => "ruby, java")
9 | @eve = TaggableModel.create(:name => "Eve Nodd", :language_list => "ruby, c++")
10 |
11 | @helper = class Helper
12 | include ActsAsTaggableOn::TagsHelper
13 | end.new
14 | end
15 |
16 | it "should yield the proper css classes" do
17 | tags = { }
18 |
19 | @helper.tag_cloud(TaggableModel.tag_counts_on(:languages), ["sucky", "awesome"]) do |tag, css_class|
20 | tags[tag.name] = css_class
21 | end
22 |
23 | tags["ruby"].should == "awesome"
24 | tags["java"].should == "sucky"
25 | tags["c++"].should == "sucky"
26 | tags["php"].should == "sucky"
27 | end
28 |
29 | it "should handle tags with zero counts (build for empty)" do
30 | bob = ActsAsTaggableOn::Tag.create(:name => "php")
31 | tom = ActsAsTaggableOn::Tag.create(:name => "java")
32 | eve = ActsAsTaggableOn::Tag.create(:name => "c++")
33 |
34 | tags = { }
35 |
36 | @helper.tag_cloud(ActsAsTaggableOn::Tag.all, ["sucky", "awesome"]) do |tag, css_class|
37 | tags[tag.name] = css_class
38 | end
39 |
40 | tags["java"].should == "sucky"
41 | tags["c++"].should == "sucky"
42 | tags["php"].should == "sucky"
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/acts-as-taggable-on.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'acts_as_taggable_on/version'
5 |
6 | Gem::Specification.new do |gem|
7 | gem.name = "acts-as-taggable-on"
8 | gem.version = ActsAsTaggableOn::VERSION
9 | gem.authors = ["Michael Bleigh", "Joost Baaij"]
10 | gem.email = ["michael@intridea.com", "joost@spacebabies.nl"]
11 | gem.description = %q{With ActsAsTaggableOn, you can tag a single model on several contexts, such as skills, interests, and awards. It also provides other advanced functionality.}
12 | gem.summary = "Advanced tagging for Rails."
13 | gem.homepage = 'https://github.com/mbleigh/acts-as-taggable-on'
14 | gem.license = "MIT"
15 |
16 | gem.files = `git ls-files`.split($/)
17 | gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
18 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19 | gem.require_paths = ["lib"]
20 |
21 | if File.exists?('UPGRADING')
22 | gem.post_install_message = File.read('UPGRADING')
23 | end
24 |
25 | gem.add_runtime_dependency 'rails', ['>= 3', '< 5']
26 |
27 | gem.add_development_dependency 'rspec-rails', '2.13.0' # 2.13.1 is broken
28 | gem.add_development_dependency 'rspec', '~> 2.6'
29 | gem.add_development_dependency 'ammeter'
30 | gem.add_development_dependency 'sqlite3'
31 | gem.add_development_dependency 'mysql2', '~> 0.3.7'
32 | gem.add_development_dependency 'pg'
33 | gem.add_development_dependency 'guard'
34 | gem.add_development_dependency 'guard-rspec'
35 | end
36 |
--------------------------------------------------------------------------------
/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb:
--------------------------------------------------------------------------------
1 | module ActsAsTaggableOn::Taggable
2 | module Cache
3 | def self.included(base)
4 | # Skip adding caching capabilities if table not exists or no cache columns exist
5 | return unless base.table_exists? && base.tag_types.any? { |context| base.column_names.include?("cached_#{context.to_s.singularize}_list") }
6 |
7 | base.send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods
8 | base.extend ActsAsTaggableOn::Taggable::Cache::ClassMethods
9 |
10 | base.class_eval do
11 | before_save :save_cached_tag_list
12 | end
13 |
14 | base.initialize_acts_as_taggable_on_cache
15 | end
16 |
17 | module ClassMethods
18 | def initialize_acts_as_taggable_on_cache
19 | tag_types.map(&:to_s).each do |tag_type|
20 | class_eval <<-RUBY, __FILE__, __LINE__ + 1
21 | def self.caching_#{tag_type.singularize}_list?
22 | caching_tag_list_on?("#{tag_type}")
23 | end
24 | RUBY
25 | end
26 | end
27 |
28 | def acts_as_taggable_on(*args)
29 | super(*args)
30 | initialize_acts_as_taggable_on_cache
31 | end
32 |
33 | def caching_tag_list_on?(context)
34 | column_names.include?("cached_#{context.to_s.singularize}_list")
35 | end
36 | end
37 |
38 | module InstanceMethods
39 | def save_cached_tag_list
40 | tag_types.map(&:to_s).each do |tag_type|
41 | if self.class.send("caching_#{tag_type.singularize}_list?")
42 | if tag_list_cache_set_on(tag_type)
43 | list = tag_list_cache_on(tag_type).to_a.flatten.compact.join(', ')
44 | self["cached_#{tag_type.singularize}_list"] = list
45 | end
46 | end
47 | end
48 |
49 | true
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/acts-as-taggable-on.rb:
--------------------------------------------------------------------------------
1 | require "active_record"
2 | require "active_record/version"
3 | require "action_view"
4 |
5 | require "digest/sha1"
6 |
7 | $LOAD_PATH.unshift(File.dirname(__FILE__))
8 |
9 | module ActsAsTaggableOn
10 | mattr_accessor :delimiter
11 | @@delimiter = ','
12 |
13 | mattr_accessor :force_lowercase
14 | @@force_lowercase = false
15 |
16 | mattr_accessor :force_parameterize
17 | @@force_parameterize = false
18 |
19 | mattr_accessor :strict_case_match
20 | @@strict_case_match = false
21 |
22 | mattr_accessor :remove_unused_tags
23 | self.remove_unused_tags = false
24 |
25 | def self.glue
26 | delimiter = @@delimiter.kind_of?(Array) ? @@delimiter[0] : @@delimiter
27 | delimiter.ends_with?(" ") ? delimiter : "#{delimiter} "
28 | end
29 |
30 | def self.setup
31 | yield self
32 | end
33 | end
34 |
35 |
36 | require "acts_as_taggable_on/utils"
37 |
38 | require "acts_as_taggable_on/taggable"
39 | require "acts_as_taggable_on/acts_as_taggable_on/compatibility"
40 | require "acts_as_taggable_on/acts_as_taggable_on/core"
41 | require "acts_as_taggable_on/acts_as_taggable_on/collection"
42 | require "acts_as_taggable_on/acts_as_taggable_on/cache"
43 | require "acts_as_taggable_on/acts_as_taggable_on/ownership"
44 | require "acts_as_taggable_on/acts_as_taggable_on/related"
45 | require "acts_as_taggable_on/acts_as_taggable_on/dirty"
46 |
47 | require "acts_as_taggable_on/tagger"
48 | require "acts_as_taggable_on/tag"
49 | require "acts_as_taggable_on/tag_list"
50 | require "acts_as_taggable_on/tags_helper"
51 | require "acts_as_taggable_on/tagging"
52 |
53 | $LOAD_PATH.shift
54 |
55 |
56 | if defined?(ActiveRecord::Base)
57 | ActiveRecord::Base.extend ActsAsTaggableOn::Compatibility
58 | ActiveRecord::Base.extend ActsAsTaggableOn::Taggable
59 | ActiveRecord::Base.send :include, ActsAsTaggableOn::Tagger
60 | end
61 |
62 | if defined?(ActionView::Base)
63 | ActionView::Base.send :include, ActsAsTaggableOn::TagsHelper
64 | end
65 |
66 |
--------------------------------------------------------------------------------
/spec/bm.rb:
--------------------------------------------------------------------------------
1 | require 'active_record'
2 | require 'action_view'
3 | require File.expand_path('../../lib/acts-as-taggable-on', __FILE__)
4 |
5 | if defined?(ActiveRecord::Acts::TaggableOn)
6 | ActiveRecord::Base.send :include, ActiveRecord::Acts::TaggableOn
7 | ActiveRecord::Base.send :include, ActiveRecord::Acts::Tagger
8 | ActionView::Base.send :include, TagsHelper if defined?(ActionView::Base)
9 | end
10 |
11 | TEST_DATABASE_FILE = File.join(File.dirname(__FILE__), '..', 'test.sqlite3')
12 | File.unlink(TEST_DATABASE_FILE) if File.exist?(TEST_DATABASE_FILE)
13 | ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => TEST_DATABASE_FILE
14 |
15 | ActiveRecord::Base.silence do
16 | ActiveRecord::Migration.verbose = false
17 | ActiveRecord::Schema.define :version => 0 do
18 | create_table "taggings", :force => true do |t|
19 | t.integer "tag_id", :limit => 11
20 | t.integer "taggable_id", :limit => 11
21 | t.string "taggable_type"
22 | t.string "context"
23 | t.datetime "created_at"
24 | t.integer "tagger_id", :limit => 11
25 | t.string "tagger_type"
26 | end
27 |
28 | add_index "taggings", ["tag_id"], :name => "index_taggings_on_tag_id"
29 | add_index "taggings", ["taggable_id", "taggable_type", "context"], :name => "index_taggings_on_taggable_id_and_taggable_type_and_context"
30 |
31 | create_table "tags", :force => true do |t|
32 | t.string "name"
33 | end
34 |
35 | create_table :taggable_models, :force => true do |t|
36 | t.column :name, :string
37 | t.column :type, :string
38 | t.column :cached_tag_list, :string
39 | end
40 | end
41 |
42 | class TaggableModel < ActiveRecord::Base
43 | acts_as_taggable
44 | acts_as_taggable_on :languages
45 | acts_as_taggable_on :skills
46 | acts_as_taggable_on :needs, :offerings
47 | end
48 | end
49 |
50 | puts Benchmark.measure {
51 | 1000.times { TaggableModel.create :tag_list => "awesome, epic, neat" }
52 | }
--------------------------------------------------------------------------------
/spec/schema.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Schema.define :version => 0 do
2 | create_table "taggings", :force => true do |t|
3 | t.integer "tag_id", :limit => 11
4 | t.integer "taggable_id", :limit => 11
5 | t.string "taggable_type"
6 | t.string "context"
7 | t.datetime "created_at"
8 | t.integer "tagger_id", :limit => 11
9 | t.string "tagger_type"
10 | end
11 |
12 | add_index "taggings", ["tag_id"], :name => "index_taggings_on_tag_id"
13 | add_index "taggings", ["taggable_id", "taggable_type", "context"], :name => "index_taggings_on_taggable_id_and_taggable_type_and_context"
14 |
15 | create_table "tags", :force => true do |t|
16 | t.string "name"
17 | end
18 |
19 | create_table :taggable_models, :force => true do |t|
20 | t.column :name, :string
21 | t.column :type, :string
22 | end
23 |
24 | create_table :non_standard_id_taggable_models, :primary_key => "an_id", :force => true do |t|
25 | t.column :name, :string
26 | t.column :type, :string
27 | end
28 |
29 | create_table :untaggable_models, :force => true do |t|
30 | t.column :taggable_model_id, :integer
31 | t.column :name, :string
32 | end
33 |
34 | create_table :cached_models, :force => true do |t|
35 | t.column :name, :string
36 | t.column :type, :string
37 | t.column :cached_tag_list, :string
38 | end
39 |
40 | create_table :other_cached_models, :force => true do |t|
41 | t.column :name, :string
42 | t.column :type, :string
43 | t.column :cached_language_list, :string
44 | t.column :cached_status_list, :string
45 | t.column :cached_glass_list, :string
46 | end
47 |
48 | create_table :users, :force => true do |t|
49 | t.column :name, :string
50 | end
51 |
52 | create_table :other_taggable_models, :force => true do |t|
53 | t.column :name, :string
54 | t.column :type, :string
55 | end
56 |
57 | create_table :ordered_taggable_models, :force => true do |t|
58 | t.column :name, :string
59 | t.column :type, :string
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/lib/acts_as_taggable_on/tag_list.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/module/delegation'
2 |
3 | module ActsAsTaggableOn
4 | class TagList < Array
5 | attr_accessor :owner
6 |
7 | def initialize(*args)
8 | add(*args)
9 | end
10 |
11 | ##
12 | # Returns a new TagList using the given tag string.
13 | #
14 | # Example:
15 | # tag_list = TagList.from("One , Two, Three")
16 | # tag_list # ["One", "Two", "Three"]
17 | def self.from(string)
18 |
19 | end
20 |
21 | ##
22 | # Add tags to the tag_list. Duplicate or blank tags will be ignored.
23 | # Use the :parse option to add an unparsed tag string.
24 | #
25 | # Example:
26 | # tag_list.add("Fun", "Happy")
27 | # tag_list.add("Fun, Happy", :parse => true)
28 | def add(*names)
29 | extract_and_apply_options!(names)
30 | concat(names)
31 | clean!
32 | self
33 | end
34 |
35 | ##
36 | # Remove specific tags from the tag_list.
37 | # Use the :parse option to add an unparsed tag string.
38 | #
39 | # Example:
40 | # tag_list.remove("Sad", "Lonely")
41 | # tag_list.remove("Sad, Lonely", :parse => true)
42 | def remove(*names)
43 | extract_and_apply_options!(names)
44 | delete_if { |name| names.include?(name) }
45 | self
46 | end
47 |
48 | ##
49 | # Transform the tag_list into a tag string suitable for editing in a form.
50 | # The tags are joined with TagList.delimiter and quoted if necessary.
51 | #
52 | # Example:
53 | # tag_list = TagList.new("Round", "Square,Cube")
54 | # tag_list.to_s # 'Round, "Square,Cube"'
55 | def to_s
56 | tags = frozen? ? self.dup : self
57 | tags.send(:clean!)
58 |
59 | tags.map do |name|
60 | d = ActsAsTaggableOn.delimiter
61 | d = Regexp.new d.join("|") if d.kind_of? Array
62 | name.index(d) ? "\"#{name}\"" : name
63 | end.join(ActsAsTaggableOn.glue)
64 | end
65 |
66 | private
67 |
68 | # Remove whitespace, duplicates, and blanks.
69 | def clean!
70 | reject!(&:blank?)
71 | map!(&:strip)
72 | map!{ |tag| tag.mb_chars.downcase.to_s } if ActsAsTaggableOn.force_lowercase
73 | map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
74 |
75 | uniq!
76 | end
77 |
78 | def extract_and_apply_options!(args)
79 | options = args.last.is_a?(Hash) ? args.pop : {}
80 | options.assert_valid_keys :parse
81 |
82 | if options[:parse]
83 | args.map! { |a| self.class.from(a) }
84 | end
85 |
86 | args.flatten!
87 | end
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/lib/acts_as_taggable_on/tagger.rb:
--------------------------------------------------------------------------------
1 | module ActsAsTaggableOn
2 | module Tagger
3 | def self.included(base)
4 | base.extend ClassMethods
5 | end
6 |
7 | module ClassMethods
8 | ##
9 | # Make a model a tagger. This allows an instance of a model to claim ownership
10 | # of tags.
11 | #
12 | # Example:
13 | # class User < ActiveRecord::Base
14 | # acts_as_tagger
15 | # end
16 | def acts_as_tagger(opts={})
17 | class_eval do
18 | has_many_with_compatibility :owned_taggings,
19 | opts.merge(
20 | :as => :tagger,
21 | :dependent => :destroy,
22 | :class_name => "ActsAsTaggableOn::Tagging"
23 | )
24 |
25 | has_many_with_compatibility :owned_tags,
26 | :through => :owned_taggings,
27 | :source => :tag,
28 | :class_name => "ActsAsTaggableOn::Tag",
29 | :uniq => true
30 | end
31 |
32 | include ActsAsTaggableOn::Tagger::InstanceMethods
33 | extend ActsAsTaggableOn::Tagger::SingletonMethods
34 | end
35 |
36 | def is_tagger?
37 | false
38 | end
39 | end
40 |
41 | module InstanceMethods
42 | ##
43 | # Tag a taggable model with tags that are owned by the tagger.
44 | #
45 | # @param taggable The object that will be tagged
46 | # @param [Hash] options An hash with options. Available options are:
47 | # * :with - The tags that you want to
48 | # * :on - The context on which you want to tag
49 | #
50 | # Example:
51 | # @user.tag(@photo, :with => "paris, normandy", :on => :locations)
52 | def tag(taggable, opts={})
53 | opts.reverse_merge!(:force => true)
54 | skip_save = opts.delete(:skip_save)
55 | return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
56 |
57 | raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
58 | raise "You need to specify some tags using :with" unless opts.has_key?(:with)
59 | raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless (opts[:force] || taggable.tag_types.include?(opts[:on]))
60 |
61 | taggable.set_owner_tag_list_on(self, opts[:on].to_s, opts[:with])
62 | taggable.save unless skip_save
63 | end
64 |
65 | def is_tagger?
66 | self.class.is_tagger?
67 | end
68 | end
69 |
70 | module SingletonMethods
71 | def is_tagger?
72 | true
73 | end
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/spec/acts_as_taggable_on/caching_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "Acts As Taggable On" do
4 |
5 | before(:each) do
6 | clean_database!
7 | end
8 |
9 | describe 'Caching' do
10 | before(:each) do
11 | @taggable = CachedModel.new(:name => "Bob Jones")
12 | @another_taggable = OtherCachedModel.new(:name => "John Smith")
13 | end
14 |
15 | it "should add saving of tag lists and cached tag lists to the instance" do
16 | @taggable.should respond_to(:save_cached_tag_list)
17 | @another_taggable.should respond_to(:save_cached_tag_list)
18 |
19 | @taggable.should respond_to(:save_tags)
20 | end
21 |
22 | it "should add cached tag lists to the instance if cached column is not present" do
23 | TaggableModel.new(:name => "Art Kram").should_not respond_to(:save_cached_tag_list)
24 | end
25 |
26 | it "should generate a cached column checker for each tag type" do
27 | CachedModel.should respond_to(:caching_tag_list?)
28 | OtherCachedModel.should respond_to(:caching_language_list?)
29 | end
30 |
31 | it 'should not have cached tags' do
32 | @taggable.cached_tag_list.should be_blank
33 | @another_taggable.cached_language_list.should be_blank
34 | end
35 |
36 | it 'should cache tags' do
37 | @taggable.update_attributes(:tag_list => 'awesome, epic')
38 | @taggable.cached_tag_list.should == 'awesome, epic'
39 |
40 | @another_taggable.update_attributes(:language_list => 'ruby, .net')
41 | @another_taggable.cached_language_list.should == 'ruby, .net'
42 | end
43 |
44 | it 'should keep the cache' do
45 | @taggable.update_attributes(:tag_list => 'awesome, epic')
46 | @taggable = CachedModel.find(@taggable)
47 | @taggable.save!
48 | @taggable.cached_tag_list.should == 'awesome, epic'
49 | end
50 |
51 | it 'should update the cache' do
52 | @taggable.update_attributes(:tag_list => 'awesome, epic')
53 | @taggable.update_attributes(:tag_list => 'awesome')
54 | @taggable.cached_tag_list.should == 'awesome'
55 | end
56 |
57 | it 'should remove the cache' do
58 | @taggable.update_attributes(:tag_list => 'awesome, epic')
59 | @taggable.update_attributes(:tag_list => '')
60 | @taggable.cached_tag_list.should be_blank
61 | end
62 |
63 | it 'should have a tag list' do
64 | @taggable.update_attributes(:tag_list => 'awesome, epic')
65 | @taggable = CachedModel.find(@taggable.id)
66 | @taggable.tag_list.sort.should == %w(awesome epic).sort
67 | end
68 |
69 | it 'should keep the tag list' do
70 | @taggable.update_attributes(:tag_list => 'awesome, epic')
71 | @taggable = CachedModel.find(@taggable.id)
72 | @taggable.save!
73 | @taggable.tag_list.sort.should == %w(awesome epic).sort
74 | end
75 | end
76 |
77 | end
78 |
--------------------------------------------------------------------------------
/lib/acts_as_taggable_on/tag.rb:
--------------------------------------------------------------------------------
1 | module ActsAsTaggableOn
2 | class Tag < ::ActiveRecord::Base
3 | include ActsAsTaggableOn::Utils
4 |
5 | attr_accessible :name if defined?(ActiveModel::MassAssignmentSecurity)
6 |
7 | ### ASSOCIATIONS:
8 |
9 | has_many :taggings, :dependent => :destroy, :class_name => 'ActsAsTaggableOn::Tagging'
10 |
11 | ### VALIDATIONS:
12 |
13 | validates_presence_of :name
14 | validates_uniqueness_of :name, :if => :validates_name_uniqueness?
15 | validates_length_of :name, :maximum => 255
16 |
17 | # monkey patch this method if don't need name uniqueness validation
18 | def validates_name_uniqueness?
19 | true
20 | end
21 |
22 | ### SCOPES:
23 |
24 | def self.named(name)
25 | if ActsAsTaggableOn.strict_case_match
26 | where(["name = #{binary}?", name])
27 | else
28 | where(["lower(name) = ?", name.downcase])
29 | end
30 | end
31 |
32 | def self.named_any(list)
33 | if ActsAsTaggableOn.strict_case_match
34 | where(list.map { |tag| sanitize_sql(["name = #{binary}?", tag.to_s.mb_chars]) }.join(" OR "))
35 | else
36 | where(list.map { |tag| sanitize_sql(["lower(name) = ?", tag.to_s.mb_chars.downcase]) }.join(" OR "))
37 | end
38 | end
39 |
40 | def self.named_like(name)
41 | where(["name #{like_operator} ? ESCAPE '!'", "%#{escape_like(name)}%"])
42 | end
43 |
44 | def self.named_like_any(list)
45 | where(list.map { |tag| sanitize_sql(["name #{like_operator} ? ESCAPE '!'", "%#{escape_like(tag.to_s)}%"]) }.join(" OR "))
46 | end
47 |
48 | ### CLASS METHODS:
49 |
50 | def self.find_or_create_with_like_by_name(name)
51 | if (ActsAsTaggableOn.strict_case_match)
52 | self.find_or_create_all_with_like_by_name([name]).first
53 | else
54 | named_like(name).first || create(:name => name)
55 | end
56 | end
57 |
58 | def self.find_or_create_all_with_like_by_name(*list)
59 | list = [list].flatten
60 |
61 | return [] if list.empty?
62 |
63 | existing_tags = Tag.named_any(list)
64 |
65 | list.map do |tag_name|
66 | comparable_tag_name = comparable_name(tag_name)
67 | existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
68 |
69 | existing_tag || Tag.create(:name => tag_name)
70 | end
71 | end
72 |
73 | ### INSTANCE METHODS:
74 |
75 | def ==(object)
76 | super || (object.is_a?(Tag) && name == object.name)
77 | end
78 |
79 | def to_s
80 | name
81 | end
82 |
83 | def count
84 | read_attribute(:count).to_i
85 | end
86 |
87 | class << self
88 | private
89 |
90 | def comparable_name(str)
91 | str.mb_chars.downcase.to_s
92 | end
93 |
94 | def binary
95 | /mysql/ === ActiveRecord::Base.connection_config[:adapter] ? "BINARY " : nil
96 | end
97 | end
98 | end
99 | end
100 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | $LOAD_PATH << "." unless $LOAD_PATH.include?(".")
2 | require 'logger'
3 |
4 | begin
5 | require "rubygems"
6 | require "bundler"
7 |
8 | if Gem::Version.new(Bundler::VERSION) <= Gem::Version.new("0.9.5")
9 | raise RuntimeError, "Your bundler version is too old." +
10 | "Run `gem install bundler` to upgrade."
11 | end
12 |
13 | # Set up load paths for all bundled gems
14 | Bundler.setup
15 | rescue Bundler::GemNotFound
16 | raise RuntimeError, "Bundler couldn't find some gems." +
17 | "Did you run \`bundlee install\`?"
18 | end
19 |
20 | Bundler.require
21 | require File.expand_path('../../lib/acts-as-taggable-on', __FILE__)
22 | require 'ammeter/init'
23 |
24 | unless [].respond_to?(:freq)
25 | class Array
26 | def freq
27 | k=Hash.new(0)
28 | each {|e| k[e]+=1}
29 | k
30 | end
31 | end
32 | end
33 |
34 | # set adapter to use, default is sqlite3
35 | # to use an alternative adapter run => rake spec DB='postgresql'
36 | db_name = ENV['DB'] || 'sqlite3'
37 | database_yml = File.expand_path('../database.yml', __FILE__)
38 |
39 | if File.exists?(database_yml)
40 | active_record_configuration = YAML.load_file(database_yml)
41 |
42 | ActiveRecord::Base.configurations = active_record_configuration
43 | config = ActiveRecord::Base.configurations[db_name]
44 |
45 | begin
46 | ActiveRecord::Base.establish_connection(db_name)
47 | ActiveRecord::Base.connection
48 | rescue
49 | case db_name
50 | when /mysql/
51 | ActiveRecord::Base.establish_connection(config.merge('database' => nil))
52 | ActiveRecord::Base.connection.create_database(config['database'], {:charset => 'utf8', :collation => 'utf8_unicode_ci'})
53 | when 'postgresql'
54 | ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public'))
55 | ActiveRecord::Base.connection.create_database(config['database'], config.merge('encoding' => 'utf8'))
56 | end
57 |
58 | ActiveRecord::Base.establish_connection(config)
59 | end
60 |
61 | logger = ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), "debug.log"))
62 | ActiveRecord::Base.default_timezone = :utc
63 |
64 | begin
65 | old_logger_level, logger.level = logger.level, ::Logger::ERROR
66 | ActiveRecord::Migration.verbose = false
67 |
68 | load(File.dirname(__FILE__) + '/schema.rb')
69 | load(File.dirname(__FILE__) + '/models.rb')
70 | ensure
71 | logger.level = old_logger_level
72 | end
73 |
74 | else
75 | raise "Please create #{database_yml} first to configure your database. Take a look at: #{database_yml}.sample"
76 | end
77 |
78 | def clean_database!
79 | models = [ActsAsTaggableOn::Tag, ActsAsTaggableOn::Tagging, TaggableModel, OtherTaggableModel, InheritingTaggableModel,
80 | AlteredInheritingTaggableModel, User, UntaggableModel, OrderedTaggableModel]
81 | models.each do |model|
82 | ActiveRecord::Base.connection.execute "DELETE FROM #{model.table_name}"
83 | end
84 | end
85 |
86 | clean_database!
87 |
--------------------------------------------------------------------------------
/lib/acts_as_taggable_on/taggable.rb:
--------------------------------------------------------------------------------
1 | module ActsAsTaggableOn
2 | module Taggable
3 | def taggable?
4 | false
5 | end
6 |
7 | ##
8 | # This is an alias for calling acts_as_taggable_on :tags.
9 | #
10 | # Example:
11 | # class Book < ActiveRecord::Base
12 | # acts_as_taggable
13 | # end
14 | def acts_as_taggable
15 | acts_as_taggable_on :tags
16 | end
17 |
18 | ##
19 | # This is an alias for calling acts_as_ordered_taggable_on :tags.
20 | #
21 | # Example:
22 | # class Book < ActiveRecord::Base
23 | # acts_as_ordered_taggable
24 | # end
25 | def acts_as_ordered_taggable
26 | acts_as_ordered_taggable_on :tags
27 | end
28 |
29 | ##
30 | # Make a model taggable on specified contexts.
31 | #
32 | # @param [Array] tag_types An array of taggable contexts
33 | #
34 | # Example:
35 | # class User < ActiveRecord::Base
36 | # acts_as_taggable_on :languages, :skills
37 | # end
38 | def acts_as_taggable_on(*tag_types)
39 | taggable_on(false, tag_types)
40 | end
41 |
42 |
43 | ##
44 | # Make a model taggable on specified contexts
45 | # and preserves the order in which tags are created
46 | #
47 | # @param [Array] tag_types An array of taggable contexts
48 | #
49 | # Example:
50 | # class User < ActiveRecord::Base
51 | # acts_as_ordered_taggable_on :languages, :skills
52 | # end
53 | def acts_as_ordered_taggable_on(*tag_types)
54 | taggable_on(true, tag_types)
55 | end
56 |
57 | private
58 |
59 | # Make a model taggable on specified contexts
60 | # and optionally preserves the order in which tags are created
61 | #
62 | # Seperate methods used above for backwards compatibility
63 | # so that the original acts_as_taggable_on method is unaffected
64 | # as it's not possible to add another arguement to the method
65 | # without the tag_types being enclosed in square brackets
66 | #
67 | # NB: method overridden in core module in order to create tag type
68 | # associations and methods after this logic has executed
69 | #
70 | def taggable_on(preserve_tag_order, *tag_types)
71 | tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
72 |
73 | if taggable?
74 | self.tag_types = (self.tag_types + tag_types).uniq
75 | self.preserve_tag_order = preserve_tag_order
76 | else
77 | class_attribute :tag_types
78 | self.tag_types = tag_types
79 | class_attribute :preserve_tag_order
80 | self.preserve_tag_order = preserve_tag_order
81 |
82 | class_eval do
83 | has_many :taggings, :as => :taggable, :dependent => :destroy, :class_name => "ActsAsTaggableOn::Tagging"
84 | has_many :base_tags, :through => :taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
85 |
86 | def self.taggable?
87 | true
88 | end
89 |
90 | include ActsAsTaggableOn::Utils
91 | end
92 | end
93 |
94 | # each of these add context-specific methods and must be
95 | # called on each call of taggable_on
96 | include ActsAsTaggableOn::Taggable::Core
97 | include ActsAsTaggableOn::Taggable::Collection
98 | include ActsAsTaggableOn::Taggable::Cache
99 | include ActsAsTaggableOn::Taggable::Ownership
100 | include ActsAsTaggableOn::Taggable::Related
101 | include ActsAsTaggableOn::Taggable::Dirty
102 | end
103 |
104 | end
105 | end
106 |
--------------------------------------------------------------------------------
/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb:
--------------------------------------------------------------------------------
1 | module ActsAsTaggableOn::Taggable
2 | module Related
3 | def self.included(base)
4 | base.send :include, ActsAsTaggableOn::Taggable::Related::InstanceMethods
5 | base.extend ActsAsTaggableOn::Taggable::Related::ClassMethods
6 | base.initialize_acts_as_taggable_on_related
7 | end
8 |
9 | module ClassMethods
10 | def initialize_acts_as_taggable_on_related
11 | tag_types.map(&:to_s).each do |tag_type|
12 | class_eval <<-RUBY, __FILE__, __LINE__ + 1
13 | def find_related_#{tag_type}(options = {})
14 | related_tags_for('#{tag_type}', self.class, options)
15 | end
16 | alias_method :find_related_on_#{tag_type}, :find_related_#{tag_type}
17 |
18 | def find_related_#{tag_type}_for(klass, options = {})
19 | related_tags_for('#{tag_type}', klass, options)
20 | end
21 | RUBY
22 | end
23 |
24 | unless tag_types.empty?
25 | class_eval <<-RUBY, __FILE__, __LINE__ + 1
26 | def find_matching_contexts(search_context, result_context, options = {})
27 | matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
28 | end
29 |
30 | def find_matching_contexts_for(klass, search_context, result_context, options = {})
31 | matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
32 | end
33 | RUBY
34 | end
35 | end
36 |
37 | def acts_as_taggable_on(*args)
38 | super(*args)
39 | initialize_acts_as_taggable_on_related
40 | end
41 | end
42 |
43 | module InstanceMethods
44 | def matching_contexts_for(search_context, result_context, klass, options = {})
45 | tags_to_find = tags_on(search_context).collect { |t| t.name }
46 |
47 | klass.select("#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count") \
48 | .from("#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}") \
49 | .where(["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?) AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_to_find, result_context]) \
50 | .group(group_columns(klass)) \
51 | .order("count DESC")
52 | end
53 |
54 | def related_tags_for(context, klass, options = {})
55 | tags_to_ignore = Array.wrap(options.delete(:ignore)).map(&:to_s) || []
56 | tags_to_find = tags_on(context).collect { |t| t.name }.reject { |t| tags_to_ignore.include? t }
57 |
58 | klass.select("#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count") \
59 | .from("#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}") \
60 | .where(["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?)", tags_to_find]) \
61 | .group(group_columns(klass)) \
62 | .order("count DESC")
63 | end
64 |
65 | private
66 |
67 | def exclude_self(klass, id)
68 | if [self.class.base_class, self.class].include? klass
69 | "#{klass.table_name}.#{klass.primary_key} != #{id} AND"
70 | else
71 | nil
72 | end
73 | end
74 |
75 | def group_columns(klass)
76 | if ActsAsTaggableOn::Tag.using_postgresql?
77 | grouped_column_names_for(klass)
78 | else
79 | "#{klass.table_name}.#{klass.primary_key}"
80 | end
81 | end
82 | end
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/spec/acts_as_taggable_on/acts_as_tagger_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "acts_as_tagger" do
4 | before(:each) do
5 | clean_database!
6 | end
7 |
8 | describe "Tagger Method Generation" do
9 | before(:each) do
10 | @tagger = User.new
11 | end
12 |
13 | it "should add #is_tagger? query method to the class-side" do
14 | User.should respond_to(:is_tagger?)
15 | end
16 |
17 | it "should return true from the class-side #is_tagger?" do
18 | User.is_tagger?.should be_true
19 | end
20 |
21 | it "should return false from the base #is_tagger?" do
22 | ActiveRecord::Base.is_tagger?.should be_false
23 | end
24 |
25 | it "should add #is_tagger? query method to the singleton" do
26 | @tagger.should respond_to(:is_tagger?)
27 | end
28 |
29 | it "should add #tag method on the instance-side" do
30 | @tagger.should respond_to(:tag)
31 | end
32 |
33 | it "should generate an association for #owned_taggings and #owned_tags" do
34 | @tagger.should respond_to(:owned_taggings, :owned_tags)
35 | end
36 | end
37 |
38 | describe "#tag" do
39 | context 'when called with a non-existent tag context' do
40 | before(:each) do
41 | @tagger = User.new
42 | @taggable = TaggableModel.new(:name=>"Richard Prior")
43 | end
44 |
45 | it "should by default not throw an exception " do
46 | @taggable.tag_list_on(:foo).should be_empty
47 | lambda {
48 | @tagger.tag(@taggable, :with=>'this, and, that', :on=>:foo)
49 | }.should_not raise_error
50 | end
51 |
52 | it 'should by default create the tag context on-the-fly' do
53 | @taggable.tag_list_on(:here_ond_now).should be_empty
54 | @tagger.tag(@taggable, :with=>'that', :on => :here_ond_now)
55 | @taggable.tag_list_on(:here_ond_now).should_not include('that')
56 | @taggable.all_tags_list_on(:here_ond_now).should include('that')
57 | end
58 |
59 | it "should show all the tag list when both public and owned tags exist" do
60 | @taggable.tag_list = 'ruby, python'
61 | @tagger.tag(@taggable, :with => 'java, lisp', :on => :tags)
62 | @taggable.all_tags_on(:tags).map(&:name).sort.should == %w(ruby python java lisp).sort
63 | end
64 |
65 | it "should not add owned tags to the common list" do
66 | @taggable.tag_list = 'ruby, python'
67 | @tagger.tag(@taggable, :with => 'java, lisp', :on => :tags)
68 | @taggable.tag_list.should == %w(ruby python)
69 | @tagger.tag(@taggable, :with => '', :on => :tags)
70 | @taggable.tag_list.should == %w(ruby python)
71 | end
72 |
73 | it "should throw an exception when the default is over-ridden" do
74 | @taggable.tag_list_on(:foo_boo).should be_empty
75 | lambda {
76 | @tagger.tag(@taggable, :with=>'this, and, that', :on=>:foo_boo, :force=>false)
77 | }.should raise_error
78 | end
79 |
80 | it "should not create the tag context on-the-fly when the default is over-ridden" do
81 | @taggable.tag_list_on(:foo_boo).should be_empty
82 | @tagger.tag(@taggable, :with=>'this, and, that', :on=>:foo_boo, :force=>false) rescue
83 | @taggable.tag_list_on(:foo_boo).should be_empty
84 | end
85 | end
86 |
87 | describe "when called by multiple tagger's" do
88 | before(:each) do
89 | @user_x = User.create(:name => "User X")
90 | @user_y = User.create(:name => "User Y")
91 | @taggable = TaggableModel.create(:name => 'acts_as_taggable_on', :tag_list => 'plugin')
92 |
93 | @user_x.tag(@taggable, :with => 'ruby, rails', :on => :tags)
94 | @user_y.tag(@taggable, :with => 'ruby, plugin', :on => :tags)
95 |
96 | @user_y.tag(@taggable, :with => '', :on => :tags)
97 | @user_y.tag(@taggable, :with => '', :on => :tags)
98 | end
99 |
100 | it "should delete owned tags" do
101 | @user_y.owned_tags.should == []
102 | end
103 |
104 | it "should not delete other taggers tags" do
105 | @user_x.owned_tags.should have(2).items
106 | end
107 |
108 | it "should not delete original tags" do
109 | @taggable.all_tags_list_on(:tags).should include('plugin')
110 | end
111 | end
112 | end
113 |
114 | end
115 |
--------------------------------------------------------------------------------
/spec/acts_as_taggable_on/tag_list_spec.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | require 'spec_helper'
3 |
4 | describe ActsAsTaggableOn::TagList do
5 | let(:tag_list) { ActsAsTaggableOn::TagList.new("awesome","radical") }
6 |
7 | it { should be_kind_of Array }
8 |
9 | it "#from should return empty array if empty array is passed" do
10 | ActsAsTaggableOn::TagList.from([]).should be_empty
11 | end
12 |
13 | describe "#add" do
14 | it "should be able to be add a new tag word" do
15 | tag_list.add("cool")
16 | tag_list.include?("cool").should be_true
17 | end
18 |
19 | it "should be able to add delimited lists of words" do
20 | tag_list.add("cool, wicked", :parse => true)
21 | tag_list.should include("cool", "wicked")
22 | end
23 |
24 | it "should be able to add delimited list of words with quoted delimiters" do
25 | tag_list.add("'cool, wicked', \"really cool, really wicked\"", :parse => true)
26 | tag_list.should include("cool, wicked", "really cool, really wicked")
27 | end
28 |
29 | it "should be able to handle other uses of quotation marks correctly" do
30 | tag_list.add("john's cool car, mary's wicked toy", :parse => true)
31 | tag_list.should include("john's cool car", "mary's wicked toy")
32 | end
33 |
34 | it "should be able to add an array of words" do
35 | tag_list.add(["cool", "wicked"], :parse => true)
36 | tag_list.should include("cool", "wicked")
37 | end
38 |
39 | it "should quote escape tags with commas in them" do
40 | tag_list.add("cool","rad,bodacious")
41 | tag_list.to_s.should == "awesome, radical, cool, \"rad,bodacious\""
42 | end
43 |
44 | end
45 |
46 | describe "#remove" do
47 | it "should be able to remove words" do
48 | tag_list.remove("awesome")
49 | tag_list.should_not include("awesome")
50 | end
51 |
52 | it "should be able to remove delimited lists of words" do
53 | tag_list.remove("awesome, radical", :parse => true)
54 | tag_list.should be_empty
55 | end
56 |
57 | it "should be able to remove an array of words" do
58 | tag_list.remove(["awesome", "radical"], :parse => true)
59 | tag_list.should be_empty
60 | end
61 | end
62 |
63 | describe "#to_s" do
64 | it "should give a delimited list of words when converted to string" do
65 | tag_list.to_s.should == "awesome, radical"
66 | end
67 |
68 | it "should be able to call to_s on a frozen tag list" do
69 | tag_list.freeze
70 | lambda { tag_list.add("cool","rad,bodacious") }.should raise_error
71 | lambda { tag_list.to_s }.should_not raise_error
72 | end
73 | end
74 |
75 | describe "cleaning" do
76 | it "should parameterize if force_parameterize is set to true" do
77 | ActsAsTaggableOn.force_parameterize = true
78 | tag_list = ActsAsTaggableOn::TagList.new("awesome()","radical)(cc")
79 |
80 | tag_list.to_s.should == "awesome, radical-cc"
81 | ActsAsTaggableOn.force_parameterize = false
82 | end
83 |
84 | it "should lowercase if force_lowercase is set to true" do
85 | ActsAsTaggableOn.force_lowercase = true
86 |
87 | tag_list = ActsAsTaggableOn::TagList.new("aweSomE","RaDicaL","Entrée")
88 | tag_list.to_s.should == "awesome, radical, entrée"
89 |
90 | ActsAsTaggableOn.force_lowercase = false
91 | end
92 |
93 | end
94 |
95 | describe "Multiple Delimiter" do
96 | before do
97 | @old_delimiter = ActsAsTaggableOn.delimiter
98 | end
99 |
100 | after do
101 | ActsAsTaggableOn.delimiter = @old_delimiter
102 | end
103 |
104 | it "should separate tags by delimiters" do
105 | ActsAsTaggableOn.delimiter = [',', ' ', '\|']
106 | tag_list = ActsAsTaggableOn::TagList.from "cool, data|I have"
107 | tag_list.to_s.should == 'cool, data, I, have'
108 | end
109 |
110 | it "should escape quote" do
111 | ActsAsTaggableOn.delimiter = [',', ' ', '\|']
112 | tag_list = ActsAsTaggableOn::TagList.from "'I have'|cool, data"
113 | tag_list.to_s.should == '"I have", cool, data'
114 |
115 | tag_list = ActsAsTaggableOn::TagList.from '"I, have"|cool, data'
116 | tag_list.to_s.should == '"I, have", cool, data'
117 | end
118 |
119 | it "should work for utf8 delimiter and long delimiter" do
120 | ActsAsTaggableOn.delimiter = [',', '的', '可能是']
121 | tag_list = ActsAsTaggableOn::TagList.from "我的东西可能是不见了,还好有备份"
122 | tag_list.to_s.should == "我, 东西, 不见了, 还好有备份"
123 | end
124 | end
125 |
126 | end
127 |
--------------------------------------------------------------------------------
/spec/acts_as_taggable_on/related_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "Acts As Taggable On" do
4 | before(:each) do
5 | clean_database!
6 | end
7 |
8 | describe "Related Objects" do
9 | it "should find related objects based on tag names on context" do
10 | taggable1 = TaggableModel.create!(:name => "Taggable 1")
11 | taggable2 = TaggableModel.create!(:name => "Taggable 2")
12 | taggable3 = TaggableModel.create!(:name => "Taggable 3")
13 |
14 | taggable1.tag_list = "one, two"
15 | taggable1.save
16 |
17 | taggable2.tag_list = "three, four"
18 | taggable2.save
19 |
20 | taggable3.tag_list = "one, four"
21 | taggable3.save
22 |
23 | taggable1.find_related_tags.should include(taggable3)
24 | taggable1.find_related_tags.should_not include(taggable2)
25 | end
26 |
27 | it "finds related tags for ordered taggable on" do
28 | taggable1 = OrderedTaggableModel.create!(:name => "Taggable 1")
29 | taggable2 = OrderedTaggableModel.create!(:name => "Taggable 2")
30 | taggable3 = OrderedTaggableModel.create!(:name => "Taggable 3")
31 |
32 | taggable1.colour_list = "one, two"
33 | taggable1.save
34 |
35 | taggable2.colour_list = "three, four"
36 | taggable2.save
37 |
38 | taggable3.colour_list = "one, four"
39 | taggable3.save
40 |
41 | taggable1.find_related_colours.should include(taggable3)
42 | taggable1.find_related_colours.should_not include(taggable2)
43 | end
44 |
45 | it "should find related objects based on tag names on context - non standard id" do
46 | taggable1 = NonStandardIdTaggableModel.create!(:name => "Taggable 1")
47 | taggable2 = NonStandardIdTaggableModel.create!(:name => "Taggable 2")
48 | taggable3 = NonStandardIdTaggableModel.create!(:name => "Taggable 3")
49 |
50 | taggable1.tag_list = "one, two"
51 | taggable1.save
52 |
53 | taggable2.tag_list = "three, four"
54 | taggable2.save
55 |
56 | taggable3.tag_list = "one, four"
57 | taggable3.save
58 |
59 | taggable1.find_related_tags.should include(taggable3)
60 | taggable1.find_related_tags.should_not include(taggable2)
61 | end
62 |
63 | it "should find other related objects based on tag names on context" do
64 | taggable1 = TaggableModel.create!(:name => "Taggable 1")
65 | taggable2 = OtherTaggableModel.create!(:name => "Taggable 2")
66 | taggable3 = OtherTaggableModel.create!(:name => "Taggable 3")
67 |
68 | taggable1.tag_list = "one, two"
69 | taggable1.save
70 |
71 | taggable2.tag_list = "three, four"
72 | taggable2.save
73 |
74 | taggable3.tag_list = "one, four"
75 | taggable3.save
76 |
77 | taggable1.find_related_tags_for(OtherTaggableModel).should include(taggable3)
78 | taggable1.find_related_tags_for(OtherTaggableModel).should_not include(taggable2)
79 | end
80 |
81 | it "should not include the object itself in the list of related objects" do
82 | taggable1 = TaggableModel.create!(:name => "Taggable 1")
83 | taggable2 = TaggableModel.create!(:name => "Taggable 2")
84 |
85 | taggable1.tag_list = "one"
86 | taggable1.save
87 |
88 | taggable2.tag_list = "one, two"
89 | taggable2.save
90 |
91 | taggable1.find_related_tags.should include(taggable2)
92 | taggable1.find_related_tags.should_not include(taggable1)
93 | end
94 |
95 | it "should not include the object itself in the list of related objects - non standard id" do
96 | taggable1 = NonStandardIdTaggableModel.create!(:name => "Taggable 1")
97 | taggable2 = NonStandardIdTaggableModel.create!(:name => "Taggable 2")
98 |
99 | taggable1.tag_list = "one"
100 | taggable1.save
101 |
102 | taggable2.tag_list = "one, two"
103 | taggable2.save
104 |
105 | taggable1.find_related_tags.should include(taggable2)
106 | taggable1.find_related_tags.should_not include(taggable1)
107 | end
108 |
109 | context "Ignored Tags" do
110 | let(:taggable1) { TaggableModel.create!(:name => "Taggable 1") }
111 | let(:taggable2) { TaggableModel.create!(:name => "Taggable 2") }
112 | let(:taggable3) { TaggableModel.create!(:name => "Taggable 3") }
113 | before(:each) do
114 | taggable1.tag_list = "one, two, four"
115 | taggable1.save
116 |
117 | taggable2.tag_list = "two, three"
118 | taggable2.save
119 |
120 | taggable3.tag_list = "one, three"
121 | taggable3.save
122 | end
123 | it "should not include ignored tags in related search" do
124 | taggable1.find_related_tags(:ignore => 'two').should_not include(taggable2)
125 | taggable1.find_related_tags(:ignore => 'two').should include(taggable3)
126 | end
127 |
128 | it "should accept array of ignored tags" do
129 | taggable4 = TaggableModel.create!(:name => "Taggable 4")
130 | taggable4.tag_list = "four"
131 | taggable4.save
132 |
133 | taggable1.find_related_tags(:ignore => ['two', 'four']).should_not include(taggable2)
134 | taggable1.find_related_tags(:ignore => ['two', 'four']).should_not include(taggable4)
135 | end
136 |
137 | it "should accept symbols as ignored tags" do
138 | taggable1.find_related_tags(:ignore => :two).should_not include(taggable2)
139 | end
140 | end
141 |
142 | end
143 | end
144 |
--------------------------------------------------------------------------------
/spec/acts_as_taggable_on/tagger_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "Tagger" do
4 | before(:each) do
5 | clean_database!
6 | @user = User.create
7 | @taggable = TaggableModel.create(:name => "Bob Jones")
8 | end
9 |
10 | it "should have taggings" do
11 | @user.tag(@taggable, :with=>'ruby,scheme', :on=>:tags)
12 | @user.owned_taggings.size == 2
13 | end
14 |
15 | it "should have tags" do
16 | @user.tag(@taggable, :with=>'ruby,scheme', :on=>:tags)
17 | @user.owned_tags.size == 2
18 | end
19 |
20 | it "should scope objects returned by tagged_with by owners" do
21 | @taggable2 = TaggableModel.create(:name => "Jim Jones")
22 | @taggable3 = TaggableModel.create(:name => "Jane Doe")
23 |
24 | @user2 = User.new
25 | @user.tag(@taggable, :with => 'ruby, scheme', :on => :tags)
26 | @user2.tag(@taggable2, :with => 'ruby, scheme', :on => :tags)
27 | @user2.tag(@taggable3, :with => 'ruby, scheme', :on => :tags)
28 |
29 | TaggableModel.tagged_with(%w(ruby scheme), :owned_by => @user).count.should == 1
30 | TaggableModel.tagged_with(%w(ruby scheme), :owned_by => @user2).count.should == 2
31 | end
32 |
33 | it "only returns objects tagged by owned_by when any is true" do
34 | @user2 = User.new
35 | @taggable2 = TaggableModel.create(:name => "Jim Jones")
36 | @taggable3 = TaggableModel.create(:name => "Jane Doe")
37 |
38 | @user.tag(@taggable, :with => 'ruby', :on => :tags)
39 | @user.tag(@taggable2, :with => 'java', :on => :tags)
40 | @user2.tag(@taggable3, :with => 'ruby', :on => :tags)
41 |
42 | tags = TaggableModel.tagged_with(%w(ruby java), :owned_by => @user, :any => true)
43 | tags.should match_array [@taggable, @taggable2]
44 | end
45 |
46 | it "only returns objects tagged by owned_by when exclude is true" do
47 | @user2 = User.new
48 | @taggable2 = TaggableModel.create(:name => "Jim Jones")
49 | @taggable3 = TaggableModel.create(:name => "Jane Doe")
50 |
51 | @user.tag(@taggable, :with => 'ruby', :on => :tags)
52 | @user.tag(@taggable2, :with => 'java', :on => :tags)
53 | @user2.tag(@taggable3, :with => 'java', :on => :tags)
54 |
55 | tags = TaggableModel.tagged_with(%w(ruby), :owned_by => @user, :exclude => true)
56 | tags.should match_array [@taggable2]
57 | end
58 |
59 | it "should not overlap tags from different taggers" do
60 | @user2 = User.new
61 | lambda{
62 | @user.tag(@taggable, :with => 'ruby, scheme', :on => :tags)
63 | @user2.tag(@taggable, :with => 'java, python, lisp, ruby', :on => :tags)
64 | }.should change(ActsAsTaggableOn::Tagging, :count).by(6)
65 |
66 | [@user, @user2, @taggable].each(&:reload)
67 |
68 | @user.owned_tags.map(&:name).sort.should == %w(ruby scheme).sort
69 | @user2.owned_tags.map(&:name).sort.should == %w(java python lisp ruby).sort
70 |
71 | @taggable.tags_from(@user).sort.should == %w(ruby scheme).sort
72 | @taggable.tags_from(@user2).sort.should == %w(java lisp python ruby).sort
73 |
74 | @taggable.all_tags_list.sort.should == %w(ruby scheme java python lisp).sort
75 | @taggable.all_tags_on(:tags).size.should == 5
76 | end
77 |
78 | it "should not lose tags from different taggers" do
79 | @user2 = User.create
80 | @user2.tag(@taggable, :with => 'java, python, lisp, ruby', :on => :tags)
81 | @user.tag(@taggable, :with => 'ruby, scheme', :on => :tags)
82 |
83 | lambda {
84 | @user2.tag(@taggable, :with => 'java, python, lisp', :on => :tags)
85 | }.should change(ActsAsTaggableOn::Tagging, :count).by(-1)
86 |
87 | [@user, @user2, @taggable].each(&:reload)
88 |
89 | @taggable.tags_from(@user).sort.should == %w(ruby scheme).sort
90 | @taggable.tags_from(@user2).sort.should == %w(java python lisp).sort
91 |
92 | @taggable.all_tags_list.sort.should == %w(ruby scheme java python lisp).sort
93 | @taggable.all_tags_on(:tags).length.should == 5
94 | end
95 |
96 | it "should not lose tags" do
97 | @user2 = User.create
98 |
99 | @user.tag(@taggable, :with => 'awesome', :on => :tags)
100 | @user2.tag(@taggable, :with => 'awesome, epic', :on => :tags)
101 |
102 | lambda {
103 | @user2.tag(@taggable, :with => 'epic', :on => :tags)
104 | }.should change(ActsAsTaggableOn::Tagging, :count).by(-1)
105 |
106 | @taggable.reload
107 | @taggable.all_tags_list.should include('awesome')
108 | @taggable.all_tags_list.should include('epic')
109 | end
110 |
111 | it "should not lose tags" do
112 | @taggable.update_attributes(:tag_list => 'ruby')
113 | @user.tag(@taggable, :with => 'ruby, scheme', :on => :tags)
114 |
115 | [@taggable, @user].each(&:reload)
116 | @taggable.tag_list.should == %w(ruby)
117 | @taggable.all_tags_list.sort.should == %w(ruby scheme).sort
118 |
119 | lambda {
120 | @taggable.update_attributes(:tag_list => "")
121 | }.should change(ActsAsTaggableOn::Tagging, :count).by(-1)
122 |
123 | @taggable.tag_list.should == []
124 | @taggable.all_tags_list.sort.should == %w(ruby scheme).sort
125 | end
126 |
127 | it "is tagger" do
128 | @user.is_tagger?.should(be_true)
129 | end
130 |
131 | it "should skip save if skip_save is passed as option" do
132 | lambda {
133 | @user.tag(@taggable, :with => 'epic', :on => :tags, :skip_save => true)
134 | }.should_not change(ActsAsTaggableOn::Tagging, :count)
135 | end
136 |
137 | end
138 |
--------------------------------------------------------------------------------
/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb:
--------------------------------------------------------------------------------
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 <<-RUBY, __FILE__, __LINE__ + 1
23 | def #{tag_type}_from(owner)
24 | owner_tag_list_on(owner, '#{tag_type}')
25 | end
26 | RUBY
27 | end
28 | end
29 | end
30 |
31 | module InstanceMethods
32 | def owner_tags_on(owner, context)
33 | if owner.nil?
34 | scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s])
35 | else
36 | scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
37 | #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
38 | #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.base_class.to_s])
39 | end
40 |
41 | # when preserving tag order, return tags in created order
42 | # if we added the order to the association this would always apply
43 | if self.class.preserve_tag_order?
44 | scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id")
45 | else
46 | scope
47 | end
48 | end
49 |
50 | def cached_owned_tag_list_on(context)
51 | variable_name = "@owned_#{context}_list"
52 | cache = (instance_variable_defined?(variable_name) && instance_variable_get(variable_name)) || instance_variable_set(variable_name, {})
53 | end
54 |
55 | def owner_tag_list_on(owner, context)
56 | add_custom_context(context)
57 |
58 | cache = cached_owned_tag_list_on(context)
59 |
60 | cache[owner] ||= ActsAsTaggableOn::TagList.new(*owner_tags_on(owner, context).map(&:name))
61 | end
62 |
63 | def set_owner_tag_list_on(owner, context, new_list)
64 | add_custom_context(context)
65 |
66 | cache = cached_owned_tag_list_on(context)
67 |
68 | cache[owner] = ActsAsTaggableOn::TagList.from(new_list)
69 | end
70 |
71 | def reload(*args)
72 | self.class.tag_types.each do |context|
73 | instance_variable_set("@owned_#{context}_list", nil)
74 | end
75 |
76 | super(*args)
77 | end
78 |
79 | def save_owned_tags
80 | tagging_contexts.each do |context|
81 | cached_owned_tag_list_on(context).each do |owner, tag_list|
82 |
83 | # Find existing tags or create non-existing tags:
84 | tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
85 |
86 | # Tag objects for owned tags
87 | owned_tags = owner_tags_on(owner, context)
88 |
89 | # Tag maintenance based on whether preserving the created order of tags
90 | if self.class.preserve_tag_order?
91 | old_tags, new_tags = owned_tags - tags, tags - owned_tags
92 |
93 | shared_tags = owned_tags & tags
94 |
95 | if shared_tags.any? && tags[0...shared_tags.size] != shared_tags
96 | index = shared_tags.each_with_index { |_, i| break i unless shared_tags[i] == tags[i] }
97 |
98 | # Update arrays of tag objects
99 | old_tags |= owned_tags.from(index)
100 | new_tags |= owned_tags.from(index) & shared_tags
101 |
102 | # Order the array of tag objects to match the tag list
103 | new_tags = tags.map { |t| new_tags.find { |n| n.name.downcase == t.name.downcase } }.compact
104 | end
105 | else
106 | # Delete discarded tags and create new tags
107 | old_tags = owned_tags - tags
108 | new_tags = tags - owned_tags
109 | end
110 |
111 | # Find all taggings that belong to the taggable (self), are owned by the owner,
112 | # have the correct context, and are removed from the list.
113 | if old_tags.present?
114 | old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
115 | :tagger_type => owner.class.base_class.to_s, :tagger_id => owner.id,
116 | :tag_id => old_tags, :context => context)
117 | end
118 |
119 | # Destroy old taggings:
120 | if old_taggings.present?
121 | ActsAsTaggableOn::Tagging.destroy_all(:id => old_taggings.map(&:id))
122 | end
123 |
124 | # Create new taggings:
125 | new_tags.each do |tag|
126 | taggings.create!(:tag_id => tag.id, :context => context.to_s, :tagger => owner, :taggable => self)
127 | end
128 | end
129 | end
130 |
131 | true
132 | end
133 | end
134 | end
135 | end
136 |
--------------------------------------------------------------------------------
/spec/acts_as_taggable_on/tag_spec.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | require 'spec_helper'
3 |
4 | describe ActsAsTaggableOn::Tag do
5 | before(:each) do
6 | clean_database!
7 | @tag = ActsAsTaggableOn::Tag.new
8 | @user = TaggableModel.create(:name => "Pablo")
9 | end
10 |
11 | describe "named like any" do
12 | before(:each) do
13 | ActsAsTaggableOn::Tag.create(:name => "Awesome")
14 | ActsAsTaggableOn::Tag.create(:name => "awesome")
15 | ActsAsTaggableOn::Tag.create(:name => "epic")
16 | end
17 |
18 | it "should find both tags" do
19 | ActsAsTaggableOn::Tag.named_like_any(["awesome", "epic"]).should have(3).items
20 | end
21 | end
22 |
23 | describe "find or create by name" do
24 | before(:each) do
25 | @tag.name = "awesome"
26 | @tag.save
27 | end
28 |
29 | it "should find by name" do
30 | ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("awesome").should == @tag
31 | end
32 |
33 | it "should find by name case insensitive" do
34 | ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("AWESOME").should == @tag
35 | end
36 |
37 | it "should create by name" do
38 | lambda {
39 | ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("epic")
40 | }.should change(ActsAsTaggableOn::Tag, :count).by(1)
41 | end
42 | end
43 |
44 | unless ActsAsTaggableOn::Tag.using_sqlite?
45 | describe "find or create by unicode name" do
46 | before(:each) do
47 | @tag.name = "привет"
48 | @tag.save
49 | end
50 |
51 | it "should find by name" do
52 | ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("привет").should == @tag
53 | end
54 |
55 | it "should find by name case insensitive" do
56 | ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("ПРИВЕТ").should == @tag
57 | end
58 | end
59 | end
60 |
61 | describe "find or create all by any name" do
62 | before(:each) do
63 | @tag.name = "awesome"
64 | @tag.save
65 | end
66 |
67 | it "should find by name" do
68 | ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name("awesome").should == [@tag]
69 | end
70 |
71 | it "should find by name case insensitive" do
72 | ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name("AWESOME").should == [@tag]
73 | end
74 |
75 | it "should create by name" do
76 | lambda {
77 | ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name("epic")
78 | }.should change(ActsAsTaggableOn::Tag, :count).by(1)
79 | end
80 |
81 | it "should find or create by name" do
82 | lambda {
83 | ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name("awesome", "epic").map(&:name).should == ["awesome", "epic"]
84 | }.should change(ActsAsTaggableOn::Tag, :count).by(1)
85 | end
86 |
87 | it "should return an empty array if no tags are specified" do
88 | ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name([]).should == []
89 | end
90 | end
91 |
92 | it "should require a name" do
93 | @tag.valid?
94 |
95 | @tag.errors[:name].should == ["can't be blank"]
96 |
97 | @tag.name = "something"
98 | @tag.valid?
99 |
100 | @tag.errors[:name].should == []
101 | end
102 |
103 | it "should limit the name length to 255 or less characters" do
104 | @tag.name = "fgkgnkkgjymkypbuozmwwghblmzpqfsgjasflblywhgkwndnkzeifalfcpeaeqychjuuowlacmuidnnrkprgpcpybarbkrmziqihcrxirlokhnzfvmtzixgvhlxzncyywficpraxfnjptxxhkqmvicbcdcynkjvziefqzyndxkjmsjlvyvbwraklbalykyxoliqdlreeykuphdtmzfdwpphmrqvwvqffojkqhlzvinqajsxbszyvrqqyzusxranr"
105 | @tag.valid?
106 | @tag.errors[:name].should == ["is too long (maximum is 255 characters)"]
107 |
108 | @tag.name = "fgkgnkkgjymkypbuozmwwghblmzpqfsgjasflblywhgkwndnkzeifalfcpeaeqychjuuowlacmuidnnrkprgpcpybarbkrmziqihcrxirlokhnzfvmtzixgvhlxzncyywficpraxfnjptxxhkqmvicbcdcynkjvziefqzyndxkjmsjlvyvbwraklbalykyxoliqdlreeykuphdtmzfdwpphmrqvwvqffojkqhlzvinqajsxbszyvrqqyzusxran"
109 | @tag.valid?
110 | @tag.errors[:name].should == []
111 | end
112 |
113 | it "should equal a tag with the same name" do
114 | @tag.name = "awesome"
115 | new_tag = ActsAsTaggableOn::Tag.new(:name => "awesome")
116 | new_tag.should == @tag
117 | end
118 |
119 | it "should return its name when to_s is called" do
120 | @tag.name = "cool"
121 | @tag.to_s.should == "cool"
122 | end
123 |
124 | it "have named_scope named(something)" do
125 | @tag.name = "cool"
126 | @tag.save!
127 | ActsAsTaggableOn::Tag.named('cool').should include(@tag)
128 | end
129 |
130 | it "have named_scope named_like(something)" do
131 | @tag.name = "cool"
132 | @tag.save!
133 | @another_tag = ActsAsTaggableOn::Tag.create!(:name => "coolip")
134 | ActsAsTaggableOn::Tag.named_like('cool').should include(@tag, @another_tag)
135 | end
136 |
137 | describe "escape wildcard symbols in like requests" do
138 | before(:each) do
139 | @tag.name = "cool"
140 | @tag.save
141 | @another_tag = ActsAsTaggableOn::Tag.create!(:name => "coo%")
142 | @another_tag2 = ActsAsTaggableOn::Tag.create!(:name => "coolish")
143 | end
144 |
145 | it "return escaped result when '%' char present in tag" do
146 | ActsAsTaggableOn::Tag.named_like('coo%').should_not include(@tag)
147 | ActsAsTaggableOn::Tag.named_like('coo%').should include(@another_tag)
148 | end
149 |
150 | end
151 |
152 | describe "when using strict_case_match" do
153 | before do
154 | ActsAsTaggableOn.strict_case_match = true
155 | @tag.name = "awesome"
156 | @tag.save!
157 | end
158 |
159 | after do
160 | ActsAsTaggableOn.strict_case_match = false
161 | end
162 |
163 | it "should find by name" do
164 | ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("awesome").should == @tag
165 | end
166 |
167 | it "should find by name case sensitively" do
168 | expect {
169 | ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("AWESOME")
170 | }.to change(ActsAsTaggableOn::Tag, :count)
171 |
172 | ActsAsTaggableOn::Tag.last.name.should == "AWESOME"
173 | end
174 |
175 | it "should have a named_scope named(something) that matches exactly" do
176 | uppercase_tag = ActsAsTaggableOn::Tag.create(:name => "Cool")
177 | @tag.name = "cool"
178 | @tag.save!
179 |
180 | ActsAsTaggableOn::Tag.named('cool').should include(@tag)
181 | ActsAsTaggableOn::Tag.named('cool').should_not include(uppercase_tag)
182 | end
183 | end
184 |
185 | describe "name uniqeness validation" do
186 | let(:duplicate_tag) { ActsAsTaggableOn::Tag.new(:name => 'ror') }
187 |
188 | before { ActsAsTaggableOn::Tag.create(:name => 'ror') }
189 |
190 | context "when don't need unique names" do
191 | it "should not run uniqueness validation" do
192 | duplicate_tag.stub(:validates_name_uniqueness?).and_return(false)
193 | duplicate_tag.save
194 | duplicate_tag.should be_persisted
195 | end
196 | end
197 |
198 | context "when do need unique names" do
199 | it "should run uniqueness validation" do
200 | duplicate_tag.should_not be_valid
201 | end
202 |
203 | it "add error to name" do
204 | duplicate_tag.save
205 |
206 | duplicate_tag.should have(1).errors
207 | duplicate_tag.errors.messages[:name].should include('has already been taken')
208 | end
209 | end
210 | end
211 | end
212 |
--------------------------------------------------------------------------------
/spec/acts_as_taggable_on/single_table_inheritance_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "Single Table Inheritance" do
4 |
5 | before(:each) do
6 | clean_database!
7 | end
8 |
9 | let(:taggable) { TaggableModel.new(:name => "taggable model") }
10 |
11 | let(:inheriting_model) { InheritingTaggableModel.new(:name => "Inheriting Taggable Model") }
12 | let(:altered_inheriting) { AlteredInheritingTaggableModel.new(:name => "Altered Inheriting Model") }
13 |
14 | 1.upto(4) do |n|
15 | let(:"inheriting_#{n}") { InheritingTaggableModel.new(:name => "Inheriting Model #{n}") }
16 | end
17 |
18 | let(:student) { Student.create! }
19 |
20 | describe "tag contexts" do
21 | it "should pass on to STI-inherited models" do
22 | inheriting_model.should respond_to(:tag_list, :skill_list, :language_list)
23 | altered_inheriting.should respond_to(:tag_list, :skill_list, :language_list)
24 | end
25 |
26 | it "should pass on to altered STI models" do
27 | altered_inheriting.should respond_to(:part_list)
28 | end
29 | end
30 |
31 | context "matching contexts" do
32 |
33 | before do
34 | inheriting_1.offering_list = "one, two"
35 | inheriting_1.need_list = "one, two"
36 | inheriting_1.save!
37 |
38 | inheriting_2.need_list = "one, two"
39 | inheriting_2.save!
40 |
41 | inheriting_3.offering_list = "one, two"
42 | inheriting_3.save!
43 |
44 | inheriting_4.tag_list = "one, two, three, four"
45 | inheriting_4.save!
46 |
47 | taggable.need_list = "one, two"
48 | taggable.save!
49 | end
50 |
51 | it "should find objects with tags of matching contexts" do
52 | inheriting_1.find_matching_contexts(:offerings, :needs).should include(inheriting_2)
53 | inheriting_1.find_matching_contexts(:offerings, :needs).should_not include(inheriting_3)
54 | inheriting_1.find_matching_contexts(:offerings, :needs).should_not include(inheriting_4)
55 | inheriting_1.find_matching_contexts(:offerings, :needs).should_not include(taggable)
56 |
57 | inheriting_1.find_matching_contexts_for(TaggableModel, :offerings, :needs).should include(inheriting_2)
58 | inheriting_1.find_matching_contexts_for(TaggableModel, :offerings, :needs).should_not include(inheriting_3)
59 | inheriting_1.find_matching_contexts_for(TaggableModel, :offerings, :needs).should_not include(inheriting_4)
60 | inheriting_1.find_matching_contexts_for(TaggableModel, :offerings, :needs).should include(taggable)
61 | end
62 |
63 | it "should not include the object itself in the list of related objects with tags of matching contexts" do
64 | inheriting_1.find_matching_contexts(:offerings, :needs).should_not include(inheriting_1)
65 | inheriting_1.find_matching_contexts_for(InheritingTaggableModel, :offerings, :needs).should_not include(inheriting_1)
66 | inheriting_1.find_matching_contexts_for(TaggableModel, :offerings, :needs).should_not include(inheriting_1)
67 | end
68 | end
69 |
70 | context "find related tags" do
71 | before do
72 | inheriting_1.tag_list = "one, two"
73 | inheriting_1.save
74 |
75 | inheriting_2.tag_list = "three, four"
76 | inheriting_2.save
77 |
78 | inheriting_3.tag_list = "one, four"
79 | inheriting_3.save
80 |
81 | taggable.tag_list = "one, two, three, four"
82 | taggable.save
83 | end
84 |
85 | it "should find related objects based on tag names on context" do
86 | inheriting_1.find_related_tags.should include(inheriting_3)
87 | inheriting_1.find_related_tags.should_not include(inheriting_2)
88 | inheriting_1.find_related_tags.should_not include(taggable)
89 |
90 | inheriting_1.find_related_tags_for(TaggableModel).should include(inheriting_3)
91 | inheriting_1.find_related_tags_for(TaggableModel).should_not include(inheriting_2)
92 | inheriting_1.find_related_tags_for(TaggableModel).should include(taggable)
93 | end
94 |
95 | it "should not include the object itself in the list of related objects" do
96 | inheriting_1.find_related_tags.should_not include(inheriting_1)
97 | inheriting_1.find_related_tags_for(InheritingTaggableModel).should_not include(inheriting_1)
98 | inheriting_1.find_related_tags_for(TaggableModel).should_not include(inheriting_1)
99 | end
100 | end
101 |
102 | describe "tag list" do
103 | before do
104 | @inherited_same = InheritingTaggableModel.new(:name => "inherited same")
105 | @inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
106 | end
107 |
108 | it "should be able to save tags for inherited models" do
109 | inheriting_model.tag_list = "bob, kelso"
110 | inheriting_model.save
111 | InheritingTaggableModel.tagged_with("bob").first.should == inheriting_model
112 | end
113 |
114 | it "should find STI tagged models on the superclass" do
115 | inheriting_model.tag_list = "bob, kelso"
116 | inheriting_model.save
117 | TaggableModel.tagged_with("bob").first.should == inheriting_model
118 | end
119 |
120 | it "should be able to add on contexts only to some subclasses" do
121 | altered_inheriting.part_list = "fork, spoon"
122 | altered_inheriting.save
123 | InheritingTaggableModel.tagged_with("fork", :on => :parts).should be_empty
124 | AlteredInheritingTaggableModel.tagged_with("fork", :on => :parts).first.should == altered_inheriting
125 | end
126 |
127 | it "should have different tag_counts_on for inherited models" do
128 | inheriting_model.tag_list = "bob, kelso"
129 | inheriting_model.save!
130 | altered_inheriting.tag_list = "fork, spoon"
131 | altered_inheriting.save!
132 |
133 | InheritingTaggableModel.tag_counts_on(:tags, :order => 'tags.id').map(&:name).should == %w(bob kelso)
134 | AlteredInheritingTaggableModel.tag_counts_on(:tags, :order => 'tags.id').map(&:name).should == %w(fork spoon)
135 | TaggableModel.tag_counts_on(:tags, :order => 'tags.id').map(&:name).should == %w(bob kelso fork spoon)
136 | end
137 |
138 | it "should have different tags_on for inherited models" do
139 | inheriting_model.tag_list = "bob, kelso"
140 | inheriting_model.save!
141 | altered_inheriting.tag_list = "fork, spoon"
142 | altered_inheriting.save!
143 |
144 | InheritingTaggableModel.tags_on(:tags, :order => 'tags.id').map(&:name).should == %w(bob kelso)
145 | AlteredInheritingTaggableModel.tags_on(:tags, :order => 'tags.id').map(&:name).should == %w(fork spoon)
146 | TaggableModel.tags_on(:tags, :order => 'tags.id').map(&:name).should == %w(bob kelso fork spoon)
147 | end
148 |
149 | it 'should store same tag without validation conflict' do
150 | taggable.tag_list = 'one'
151 | taggable.save!
152 |
153 | inheriting_model.tag_list = 'one'
154 | inheriting_model.save!
155 |
156 | inheriting_model.update_attributes! :name => 'foo'
157 | end
158 | end
159 |
160 | describe "ownership" do
161 | it "should have taggings" do
162 | student.tag(taggable, :with=>'ruby,scheme', :on=>:tags)
163 | student.owned_taggings.should have(2).tags
164 | end
165 |
166 | it "should have tags" do
167 | student.tag(taggable, :with=>'ruby,scheme', :on=>:tags)
168 | student.owned_tags.should have(2).tags
169 | end
170 |
171 | it "should return tags for the inheriting tagger" do
172 | student.tag(taggable, :with => 'ruby, scheme', :on => :tags)
173 | taggable.tags_from(student).should match_array(%w(ruby scheme))
174 | end
175 |
176 | it "returns owner tags on the tagger" do
177 | student.tag(taggable, :with => 'ruby, scheme', :on => :tags)
178 | taggable.owner_tags_on(student, :tags).should have(2).tags
179 | end
180 |
181 | it "should scope objects returned by tagged_with by owners" do
182 | student.tag(taggable, :with => 'ruby, scheme', :on => :tags)
183 | TaggableModel.tagged_with(%w(ruby scheme), :owned_by => student).should have(1).tag
184 | end
185 | end
186 |
187 | end
188 |
--------------------------------------------------------------------------------
/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb:
--------------------------------------------------------------------------------
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 <<-RUBY, __FILE__, __LINE__ + 1
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 | RUBY
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 | def tags_on(context, options = {})
42 | all_tags(options.merge({:on => context.to_s}))
43 | end
44 |
45 | ##
46 | # Calculate the tag names.
47 | # To be used when you don't need tag counts and want to avoid the taggable joins.
48 | #
49 | # @param [Hash] options Options:
50 | # * :start_at - Restrict the tags to those created after a certain time
51 | # * :end_at - Restrict the tags to those created before a certain time
52 | # * :conditions - A piece of SQL conditions to add to the query. Note we don't join the taggable objects for performance reasons.
53 | # * :limit - The maximum number of tags to return
54 | # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
55 | # * :on - Scope the find to only include a certain context
56 | def all_tags(options = {})
57 | options.assert_valid_keys :start_at, :end_at, :conditions, :order, :limit, :on
58 |
59 | ## Generate conditions:
60 | options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
61 |
62 | start_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
63 | end_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
64 |
65 | taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?", base_class.name])
66 | taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
67 |
68 | tagging_conditions = [
69 | taggable_conditions,
70 | start_at_conditions,
71 | end_at_conditions
72 | ].compact.reverse
73 |
74 | tag_conditions = [
75 | options[:conditions]
76 | ].compact.reverse
77 |
78 | ## Generate scope:
79 | tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id")
80 | tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*").order(options[:order]).limit(options[:limit])
81 |
82 | # Joins and conditions
83 | tagging_conditions.each { |condition| tagging_scope = tagging_scope.where(condition) }
84 | tag_conditions.each { |condition| tag_scope = tag_scope.where(condition) }
85 |
86 | group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
87 |
88 | # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
89 | scoped_select = "#{table_name}.#{primary_key}"
90 | tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{select(scoped_select).to_sql})").group(group_columns)
91 |
92 | tag_scope = tag_scope.joins("JOIN (#{tagging_scope.to_sql}) AS #{ActsAsTaggableOn::Tagging.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id")
93 | tag_scope
94 | end
95 |
96 | ##
97 | # Calculate the tag counts for all tags.
98 | #
99 | # @param [Hash] options Options:
100 | # * :start_at - Restrict the tags to those created after a certain time
101 | # * :end_at - Restrict the tags to those created before a certain time
102 | # * :conditions - A piece of SQL conditions to add to the query
103 | # * :limit - The maximum number of tags to return
104 | # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
105 | # * :at_least - Exclude tags with a frequency less than the given value
106 | # * :at_most - Exclude tags with a frequency greater than the given value
107 | # * :on - Scope the find to only include a certain context
108 | def all_tag_counts(options = {})
109 | options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
110 |
111 | scope = {}
112 |
113 | ## Generate conditions:
114 | options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
115 |
116 | start_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
117 | end_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
118 |
119 | taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?", base_class.name])
120 | taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = ?", options[:id]]) if options[:id]
121 | taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
122 |
123 | tagging_conditions = [
124 | taggable_conditions,
125 | scope[:conditions],
126 | start_at_conditions,
127 | end_at_conditions
128 | ].compact.reverse
129 |
130 | tag_conditions = [
131 | options[:conditions]
132 | ].compact.reverse
133 |
134 | ## Generate joins:
135 | taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id"
136 | taggable_join << " AND #{table_name}.#{inheritance_column} = '#{name}'" unless descends_from_active_record? # Current model is STI descendant, so add type checking to the join condition
137 |
138 | tagging_joins = [
139 | taggable_join,
140 | scope[:joins]
141 | ].compact
142 |
143 | tag_joins = [
144 | ].compact
145 |
146 | ## Generate scope:
147 | tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count")
148 | tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
149 |
150 | # Joins and conditions
151 | tagging_joins.each { |join| tagging_scope = tagging_scope.joins(join) }
152 | tagging_conditions.each { |condition| tagging_scope = tagging_scope.where(condition) }
153 |
154 | tag_joins.each { |join| tag_scope = tag_scope.joins(join) }
155 | tag_conditions.each { |condition| tag_scope = tag_scope.where(condition) }
156 |
157 | # GROUP BY and HAVING clauses:
158 | at_least = sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) >= ?", options.delete(:at_least)]) if options[:at_least]
159 | at_most = sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) <= ?", options.delete(:at_most)]) if options[:at_most]
160 | having = ["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) > 0", at_least, at_most].compact.join(' AND ')
161 |
162 | group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
163 |
164 | unless options[:id]
165 | # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
166 | scoped_select = "#{table_name}.#{primary_key}"
167 | tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{select(scoped_select).to_sql})")
168 | end
169 |
170 | tagging_scope = tagging_scope.group(group_columns).having(having)
171 |
172 | tag_scope = tag_scope.joins("JOIN (#{tagging_scope.to_sql}) AS #{ActsAsTaggableOn::Tagging.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id")
173 | tag_scope
174 | end
175 | end
176 |
177 | module InstanceMethods
178 | def tag_counts_on(context, options={})
179 | self.class.tag_counts_on(context, options.merge(:id => id))
180 | end
181 | end
182 | end
183 | end
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ActsAsTaggableOn
2 | [](http://travis-ci.org/mbleigh/acts-as-taggable-on)
3 |
4 | This plugin was originally based on Acts as Taggable on Steroids by Jonathan Viney.
5 | It has evolved substantially since that point, but all credit goes to him for the
6 | initial tagging functionality that so many people have used.
7 |
8 | For instance, in a social network, a user might have tags that are called skills,
9 | interests, sports, and more. There is no real way to differentiate between tags and
10 | so an implementation of this type is not possible with acts as taggable on steroids.
11 |
12 | Enter Acts as Taggable On. Rather than tying functionality to a specific keyword
13 | (namely `tags`), acts as taggable on allows you to specify an arbitrary number of
14 | tag "contexts" that can be used locally or in combination in the same way steroids
15 | was used.
16 |
17 | ## Compatibility
18 |
19 | Versions 2.x are compatible with Ruby 1.8.7+ and Rails 3.
20 |
21 | Versions 2.4.1 and up are compatible with Rails 4 too (thanks to arabonradar and cwoodcox).
22 |
23 | Versions 3.x (currently unreleased) are compatible with Ruby 1.9.3+ and Rails 3 and 4.
24 |
25 | For an up-to-date roadmap, see https://github.com/mbleigh/acts-as-taggable-on/issues/milestones
26 |
27 | ## Installation
28 |
29 | To use it, add it to your Gemfile:
30 |
31 | ```ruby
32 | gem 'acts-as-taggable-on'
33 | ```
34 |
35 | and bundle:
36 |
37 | ```ruby
38 | bundle
39 | ```
40 |
41 | #### Post Installation
42 |
43 | ```shell
44 | rails generate acts_as_taggable_on:migration
45 | rake db:migrate
46 | ```
47 |
48 | ## Testing
49 |
50 | Acts As Taggable On uses RSpec for its test coverage. Inside the gem
51 | directory, you can run the specs with:
52 |
53 | ```shell
54 | bundle
55 | rake spec
56 | ```
57 |
58 | If you want, add a `.ruby-version` file in the project root (and use rbenv or RVM) to work on a specific version of Ruby.
59 |
60 | ## Usage
61 |
62 | ```ruby
63 | class User < ActiveRecord::Base
64 | # Alias for acts_as_taggable_on :tags
65 | acts_as_taggable
66 | acts_as_taggable_on :skills, :interests
67 | end
68 |
69 | @user = User.new(:name => "Bobby")
70 | @user.tag_list = "awesome, slick, hefty" # this should be familiar
71 | @user.skill_list = "joking, clowning, boxing" # but you can do it for any context!
72 |
73 | @user.tags # => [,,]
74 | @user.skills # => [,,]
75 | @user.skill_list # => ["joking","clowning","boxing"] as TagList
76 |
77 | @user.tag_list.remove("awesome") # remove a single tag
78 | @user.tag_list.remove("awesome, slick") # works with arrays too
79 | @user.tag_list.add("awesomer") # add a single tag. alias for <<
80 | @user.tag_list.add("awesomer, slicker") # also works with arrays
81 |
82 | User.skill_counts # => [,...]
83 | ```
84 |
85 | To preserve the order in which tags are created use `acts_as_ordered_taggable`:
86 |
87 | ```ruby
88 | class User < ActiveRecord::Base
89 | # Alias for acts_as_ordered_taggable_on :tags
90 | acts_as_ordered_taggable
91 | acts_as_ordered_taggable_on :skills, :interests
92 | end
93 |
94 | @user = User.new(:name => "Bobby")
95 | @user.tag_list = "east, south"
96 | @user.save
97 |
98 | @user.tag_list = "north, east, south, west"
99 | @user.save
100 |
101 | @user.reload
102 | @user.tag_list # => ["north", "east", "south", "west"]
103 | ```
104 |
105 | ### Finding Tagged Objects
106 |
107 | Acts As Taggable On uses scopes to create an association for tags.
108 | This way you can mix and match to filter down your results.
109 |
110 | ```ruby
111 | class User < ActiveRecord::Base
112 | acts_as_taggable_on :tags, :skills
113 | scope :by_join_date, order("created_at DESC")
114 | end
115 |
116 | User.tagged_with("awesome").by_join_date
117 | User.tagged_with("awesome").by_join_date.paginate(:page => params[:page], :per_page => 20)
118 |
119 | # Find a user with matching all tags, not just one
120 | User.tagged_with(["awesome", "cool"], :match_all => true)
121 |
122 | # Find a user with any of the tags:
123 | User.tagged_with(["awesome", "cool"], :any => true)
124 |
125 | # Find a user that not tags with awesome or cool:
126 | User.tagged_with(["awesome", "cool"], :exclude => true)
127 |
128 | # Find a user with any of tags based on context:
129 | User.tagged_with(['awesome, cool'], :on => :tags, :any => true).tagged_with(['smart', 'shy'], :on => :skills, :any => true)
130 | ```
131 |
132 | You can also use `:wild => true` option along with `:any` or `:exclude` option. It will looking for `%awesome%` and `%cool%` in sql.
133 |
134 | __Tip:__ `User.tagged_with([])` or '' will return `[]`, but not all records.
135 |
136 | ### Relationships
137 |
138 | You can find objects of the same type based on similar tags on certain contexts.
139 | Also, objects will be returned in descending order based on the total number of
140 | matched tags.
141 |
142 | ```ruby
143 | @bobby = User.find_by_name("Bobby")
144 | @bobby.skill_list # => ["jogging", "diving"]
145 |
146 | @frankie = User.find_by_name("Frankie")
147 | @frankie.skill_list # => ["hacking"]
148 |
149 | @tom = User.find_by_name("Tom")
150 | @tom.skill_list # => ["hacking", "jogging", "diving"]
151 |
152 | @tom.find_related_skills # => [,]
153 | @bobby.find_related_skills # => []
154 | @frankie.find_related_skills # => []
155 | ```
156 |
157 | ### Dynamic Tag Contexts
158 |
159 | In addition to the generated tag contexts in the definition, it is also possible
160 | to allow for dynamic tag contexts (this could be user generated tag contexts!)
161 |
162 | ```ruby
163 | @user = User.new(:name => "Bobby")
164 | @user.set_tag_list_on(:customs, "same, as, tag, list")
165 | @user.tag_list_on(:customs) # => ["same","as","tag","list"]
166 | @user.save
167 | @user.tags_on(:customs) # => [,...]
168 | @user.tag_counts_on(:customs)
169 | User.tagged_with("same", :on => :customs) # => [@user]
170 | ```
171 |
172 | ### Tag Ownership
173 |
174 | Tags can have owners:
175 |
176 | ```ruby
177 | class User < ActiveRecord::Base
178 | acts_as_tagger
179 | end
180 |
181 | class Photo < ActiveRecord::Base
182 | acts_as_taggable_on :locations
183 | end
184 |
185 | @some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations)
186 | @some_user.owned_taggings
187 | @some_user.owned_tags
188 | Photo.tagged_with("paris", :on => :locations, :owned_by => @some_user)
189 | @some_photo.locations_from(@some_user) # => ["paris", "normandy"]
190 | @some_photo.owner_tags_on(@some_user, :locations) # => [#...]
191 | @some_photo.owner_tags_on(nil, :locations) # => Ownerships equivalent to saying @some_photo.locations
192 | @some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations, :skip_save => true) #won't save @some_photo object
193 | ```
194 |
195 | ### Dirty objects
196 |
197 | ```ruby
198 | @bobby = User.find_by_name("Bobby")
199 | @bobby.skill_list # => ["jogging", "diving"]
200 |
201 | @bobby.skill_list_changed? #=> false
202 | @bobby.changes #=> {}
203 |
204 | @bobby.skill_list = "swimming"
205 | @bobby.changes.should == {"skill_list"=>["jogging, diving", ["swimming"]]}
206 | @bobby.skill_list_changed? #=> true
207 |
208 | @bobby.skill_list_change.should == ["jogging, diving", ["swimming"]]
209 | ```
210 |
211 | ### Tag cloud calculations
212 |
213 | To construct tag clouds, the frequency of each tag needs to be calculated.
214 | Because we specified `acts_as_taggable_on` on the `User` class, we can
215 | get a calculation of all the tag counts by using `User.tag_counts_on(:customs)`. But what if we wanted a tag count for
216 | an single user's posts? To achieve this we call tag_counts on the association:
217 |
218 | ```ruby
219 | User.find(:first).posts.tag_counts_on(:tags)
220 | ```
221 |
222 | A helper is included to assist with generating tag clouds.
223 |
224 | Here is an example that generates a tag cloud.
225 |
226 | Helper:
227 |
228 | ```ruby
229 | module PostsHelper
230 | include ActsAsTaggableOn::TagsHelper
231 | end
232 | ```
233 |
234 | Controller:
235 |
236 | ```ruby
237 | class PostController < ApplicationController
238 | def tag_cloud
239 | @tags = Post.tag_counts_on(:tags)
240 | end
241 | end
242 | ```
243 |
244 | View:
245 |
246 | ```erb
247 | <% tag_cloud(@tags, %w(css1 css2 css3 css4)) do |tag, css_class| %>
248 | <%= link_to tag.name, { :action => :tag, :id => tag.name }, :class => css_class %>
249 | <% end %>
250 | ```
251 |
252 | CSS:
253 |
254 | ```css
255 | .css1 { font-size: 1.0em; }
256 | .css2 { font-size: 1.2em; }
257 | .css3 { font-size: 1.4em; }
258 | .css4 { font-size: 1.6em; }
259 | ```
260 |
261 | ## Configuration
262 |
263 | If you would like to remove unused tag objects after removing taggings, add:
264 |
265 | ```ruby
266 | ActsAsTaggableOn.remove_unused_tags = true
267 | ```
268 |
269 | If you want force tags to be saved downcased:
270 |
271 | ```ruby
272 | ActsAsTaggableOn.force_lowercase = true
273 | ```
274 |
275 | If you want tags to be saved parametrized (you can redefine to_param as well):
276 |
277 | ```ruby
278 | ActsAsTaggableOn.force_parameterize = true
279 | ```
280 |
281 | If you would like tags to be case-sensitive and not use LIKE queries for creation:
282 |
283 | ```ruby
284 | ActsAsTaggableOn.strict_case_match = true
285 | ```
286 |
287 | If you want to change the default delimiter (it defaults to ','). You can also pass in an array of delimiters such as ([',', '|']):
288 |
289 | ```ruby
290 | ActsAsTaggableOn.delimiter = ','
291 | ```
292 |
293 | ## Contributors
294 |
295 | We have a long list of valued contributors. [Check them all](https://github.com/mbleigh/acts-as-taggable-on/contributors)
296 |
297 | ## Maintainer
298 |
299 | * [Joost Baaij](https://github.com/tilsammans)
300 |
301 | ## License
302 |
303 | See [LICENSE](https://github.com/mbleigh/acts-as-taggable-on/blob/master/LICENSE.md)
304 |
--------------------------------------------------------------------------------
/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "Acts As Taggable On" do
4 | before(:each) do
5 | clean_database!
6 | end
7 |
8 | it "should provide a class method 'taggable?' that is false for untaggable models" do
9 | UntaggableModel.should_not be_taggable
10 | end
11 |
12 | describe "Taggable Method Generation To Preserve Order" do
13 | before(:each) do
14 | clean_database!
15 | TaggableModel.tag_types = []
16 | TaggableModel.preserve_tag_order = false
17 | TaggableModel.acts_as_ordered_taggable_on(:ordered_tags)
18 | @taggable = TaggableModel.new(:name => "Bob Jones")
19 | end
20 |
21 | it "should respond 'true' to preserve_tag_order?" do
22 | @taggable.class.preserve_tag_order?.should be_true
23 | end
24 | end
25 |
26 | describe "Taggable Method Generation" do
27 | before(:each) do
28 | clean_database!
29 | TaggableModel.tag_types = []
30 | TaggableModel.acts_as_taggable_on(:tags, :languages, :skills, :needs, :offerings)
31 | @taggable = TaggableModel.new(:name => "Bob Jones")
32 | end
33 |
34 | it "should respond 'true' to taggable?" do
35 | @taggable.class.should be_taggable
36 | end
37 |
38 | it "should create a class attribute for tag types" do
39 | @taggable.class.should respond_to(:tag_types)
40 | end
41 |
42 | it "should create an instance attribute for tag types" do
43 | @taggable.should respond_to(:tag_types)
44 | end
45 |
46 | it "should have all tag types" do
47 | @taggable.tag_types.should == [:tags, :languages, :skills, :needs, :offerings]
48 | end
49 |
50 | it "should create a class attribute for preserve tag order" do
51 | @taggable.class.should respond_to(:preserve_tag_order?)
52 | end
53 |
54 | it "should create an instance attribute for preserve tag order" do
55 | @taggable.should respond_to(:preserve_tag_order?)
56 | end
57 |
58 | it "should respond 'false' to preserve_tag_order?" do
59 | @taggable.class.preserve_tag_order?.should be_false
60 | end
61 |
62 | it "should generate an association for each tag type" do
63 | @taggable.should respond_to(:tags, :skills, :languages)
64 | end
65 |
66 | it "should add tagged_with and tag_counts to singleton" do
67 | TaggableModel.should respond_to(:tagged_with, :tag_counts)
68 | end
69 |
70 | it "should generate a tag_list accessor/setter for each tag type" do
71 | @taggable.should respond_to(:tag_list, :skill_list, :language_list)
72 | @taggable.should respond_to(:tag_list=, :skill_list=, :language_list=)
73 | end
74 |
75 | it "should generate a tag_list accessor, that includes owned tags, for each tag type" do
76 | @taggable.should respond_to(:all_tags_list, :all_skills_list, :all_languages_list)
77 | end
78 | end
79 |
80 | describe "Reloading" do
81 | it "should save a model instantiated by Model.find" do
82 | taggable = TaggableModel.create!(:name => "Taggable")
83 | found_taggable = TaggableModel.find(taggable.id)
84 | found_taggable.save
85 | end
86 | end
87 |
88 | describe "Matching Contexts" do
89 | it "should find objects with tags of matching contexts" do
90 | taggable1 = TaggableModel.create!(:name => "Taggable 1")
91 | taggable2 = TaggableModel.create!(:name => "Taggable 2")
92 | taggable3 = TaggableModel.create!(:name => "Taggable 3")
93 |
94 | taggable1.offering_list = "one, two"
95 | taggable1.save!
96 |
97 | taggable2.need_list = "one, two"
98 | taggable2.save!
99 |
100 | taggable3.offering_list = "one, two"
101 | taggable3.save!
102 |
103 | taggable1.find_matching_contexts(:offerings, :needs).should include(taggable2)
104 | taggable1.find_matching_contexts(:offerings, :needs).should_not include(taggable3)
105 | end
106 |
107 | it "should find other related objects with tags of matching contexts" do
108 | taggable1 = TaggableModel.create!(:name => "Taggable 1")
109 | taggable2 = OtherTaggableModel.create!(:name => "Taggable 2")
110 | taggable3 = OtherTaggableModel.create!(:name => "Taggable 3")
111 |
112 | taggable1.offering_list = "one, two"
113 | taggable1.save
114 |
115 | taggable2.need_list = "one, two"
116 | taggable2.save
117 |
118 | taggable3.offering_list = "one, two"
119 | taggable3.save
120 |
121 | taggable1.find_matching_contexts_for(OtherTaggableModel, :offerings, :needs).should include(taggable2)
122 | taggable1.find_matching_contexts_for(OtherTaggableModel, :offerings, :needs).should_not include(taggable3)
123 | end
124 |
125 | it "should not include the object itself in the list of related objects with tags of matching contexts" do
126 | taggable1 = TaggableModel.create!(:name => "Taggable 1")
127 | taggable2 = TaggableModel.create!(:name => "Taggable 2")
128 |
129 | taggable1.offering_list = "one, two"
130 | taggable1.need_list = "one, two"
131 | taggable1.save
132 |
133 | taggable2.need_list = "one, two"
134 | taggable2.save
135 |
136 | taggable1.find_matching_contexts_for(TaggableModel, :offerings, :needs).should include(taggable2)
137 | taggable1.find_matching_contexts_for(TaggableModel, :offerings, :needs).should_not include(taggable1)
138 | end
139 |
140 | end
141 |
142 | describe 'Tagging Contexts' do
143 | it 'should eliminate duplicate tagging contexts ' do
144 | TaggableModel.acts_as_taggable_on(:skills, :skills)
145 | TaggableModel.tag_types.freq[:skills].should_not == 3
146 | end
147 |
148 | it "should not contain embedded/nested arrays" do
149 | TaggableModel.acts_as_taggable_on([:array], [:array])
150 | TaggableModel.tag_types.freq[[:array]].should == 0
151 | end
152 |
153 | it "should _flatten_ the content of arrays" do
154 | TaggableModel.acts_as_taggable_on([:array], [:array])
155 | TaggableModel.tag_types.freq[:array].should == 1
156 | end
157 |
158 | it "should not raise an error when passed nil" do
159 | lambda {
160 | TaggableModel.acts_as_taggable_on()
161 | }.should_not raise_error
162 | end
163 |
164 | it "should not raise an error when passed [nil]" do
165 | lambda {
166 | TaggableModel.acts_as_taggable_on([nil])
167 | }.should_not raise_error
168 | end
169 | end
170 |
171 | context 'when tagging context ends in an "s" when singular (ex. "status", "glass", etc.)' do
172 | describe 'caching' do
173 | before { @taggable = OtherCachedModel.new(:name => "John Smith") }
174 | subject { @taggable }
175 |
176 | it { should respond_to(:save_cached_tag_list) }
177 | its(:cached_language_list) { should be_blank }
178 | its(:cached_status_list) { should be_blank }
179 | its(:cached_glass_list) { should be_blank }
180 |
181 | context 'language taggings cache after update' do
182 | before { @taggable.update_attributes(:language_list => 'ruby, .net') }
183 | subject { @taggable }
184 |
185 | its(:language_list) { should == ['ruby', '.net']}
186 | its(:cached_language_list) { should == 'ruby, .net' } # passes
187 | its(:instance_variables) { should include((RUBY_VERSION < '1.9' ? '@language_list' : :@language_list)) }
188 | end
189 |
190 | context 'status taggings cache after update' do
191 | before { @taggable.update_attributes(:status_list => 'happy, married') }
192 | subject { @taggable }
193 |
194 | its(:status_list) { should == ['happy', 'married'] }
195 | its(:cached_status_list) { should == 'happy, married' } # fails
196 | its(:cached_status_list) { should_not == '' } # fails, is blank
197 | its(:instance_variables) { should include((RUBY_VERSION < '1.9' ? '@status_list' : :@status_list)) }
198 | its(:instance_variables) { should_not include((RUBY_VERSION < '1.9' ? '@statu_list' : :@statu_list)) } # fails, note: one "s"
199 |
200 | end
201 |
202 | context 'glass taggings cache after update' do
203 | before do
204 | @taggable.update_attributes(:glass_list => 'rectangle, aviator')
205 | end
206 |
207 | subject { @taggable }
208 | its(:glass_list) { should == ['rectangle', 'aviator'] }
209 | its(:cached_glass_list) { should == 'rectangle, aviator' } # fails
210 | its(:cached_glass_list) { should_not == '' } # fails, is blank
211 | if RUBY_VERSION < '1.9'
212 | its(:instance_variables) { should include('@glass_list') }
213 | its(:instance_variables) { should_not include('@glas_list') } # fails, note: one "s"
214 | else
215 | its(:instance_variables) { should include(:@glass_list) }
216 | its(:instance_variables) { should_not include(:@glas_list) } # fails, note: one "s"
217 | end
218 |
219 | end
220 | end
221 | end
222 |
223 | describe "taggings" do
224 | before(:each) do
225 | @taggable = TaggableModel.new(:name => "Art Kram")
226 | end
227 |
228 | it 'should return [] taggings' do
229 | @taggable.taggings.should == []
230 | end
231 | end
232 |
233 | describe "@@remove_unused_tags" do
234 | before do
235 | @taggable = TaggableModel.create(:name => "Bob Jones")
236 | @tag = ActsAsTaggableOn::Tag.create(:name => "awesome")
237 |
238 | @tagging = ActsAsTaggableOn::Tagging.create(:taggable => @taggable, :tag => @tag, :context => 'tags')
239 | end
240 |
241 | context "if set to true" do
242 | before do
243 | ActsAsTaggableOn.remove_unused_tags = true
244 | end
245 |
246 | it "should remove unused tags after removing taggings" do
247 | @tagging.destroy
248 | ActsAsTaggableOn::Tag.find_by_name("awesome").should be_nil
249 | end
250 | end
251 |
252 | context "if set to false" do
253 | before do
254 | ActsAsTaggableOn.remove_unused_tags = false
255 | end
256 |
257 | it "should not remove unused tags after removing taggings" do
258 | @tagging.destroy
259 | ActsAsTaggableOn::Tag.find_by_name("awesome").should == @tag
260 | end
261 | end
262 | end
263 |
264 | end
265 |
--------------------------------------------------------------------------------
/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb:
--------------------------------------------------------------------------------
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 |
17 | def initialize_acts_as_taggable_on_core
18 | include taggable_mixin
19 | tag_types.map(&:to_s).each do |tags_type|
20 | tag_type = tags_type.to_s.singularize
21 | context_taggings = "#{tag_type}_taggings".to_sym
22 | context_tags = tags_type.to_sym
23 | taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" : [])
24 |
25 | class_eval do
26 | # when preserving tag order, include order option so that for a 'tags' context
27 | # the associations tag_taggings & tags are always returned in created order
28 | has_many_with_compatibility context_taggings, :as => :taggable,
29 | :dependent => :destroy,
30 | :class_name => "ActsAsTaggableOn::Tagging",
31 | :order => taggings_order,
32 | :conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.context = (?)", tags_type],
33 | :include => :tag
34 |
35 | has_many_with_compatibility context_tags, :through => context_taggings,
36 | :source => :tag,
37 | :class_name => "ActsAsTaggableOn::Tag",
38 | :order => taggings_order
39 |
40 | end
41 |
42 | taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
43 | def #{tag_type}_list
44 | tag_list_on('#{tags_type}')
45 | end
46 |
47 | def #{tag_type}_list=(new_tags)
48 | set_tag_list_on('#{tags_type}', new_tags)
49 | end
50 |
51 | def all_#{tags_type}_list
52 | all_tags_list_on('#{tags_type}')
53 | end
54 | RUBY
55 | end
56 | end
57 |
58 | def taggable_on(preserve_tag_order, *tag_types)
59 | super(preserve_tag_order, *tag_types)
60 | initialize_acts_as_taggable_on_core
61 | end
62 |
63 | # all column names are necessary for PostgreSQL group clause
64 | def grouped_column_names_for(object)
65 | object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
66 | end
67 |
68 | ##
69 | # Return a scope of objects that are tagged with the specified tags.
70 | #
71 | # @param tags The tags that we want to query for
72 | # @param [Hash] options A hash of options to alter you query:
73 | # * :exclude - if set to true, return objects that are *NOT* tagged with the specified tags
74 | # * :any - if set to true, return objects that are tagged with *ANY* of the specified tags
75 | # * :match_all - if set to true, return objects that are *ONLY* tagged with the specified tags
76 | # * :owned_by - return objects that are *ONLY* owned by the owner
77 | #
78 | # Example:
79 | # User.tagged_with("awesome", "cool") # Users that are tagged with awesome and cool
80 | # User.tagged_with("awesome", "cool", :exclude => true) # Users that are not tagged with awesome or cool
81 | # User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
82 | # User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
83 | # User.tagged_with("awesome", "cool", :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
84 | def tagged_with(tags, options = {})
85 | tag_list = ActsAsTaggableOn::TagList.from(tags)
86 | empty_result = where("1 = 0")
87 |
88 | return empty_result if tag_list.empty?
89 |
90 | joins = []
91 | conditions = []
92 | having = []
93 | select_clause = []
94 |
95 | context = options.delete(:on)
96 | owned_by = options.delete(:owned_by)
97 | alias_base_name = undecorated_table_name.gsub('.','_')
98 | quote = ActsAsTaggableOn::Tag.using_postgresql? ? '"' : ''
99 |
100 | if options.delete(:exclude)
101 | if options.delete(:wild)
102 | tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ? ESCAPE '!'", "%#{escape_like(t)}%"]) }.join(" OR ")
103 | else
104 | tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
105 | end
106 |
107 | conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{ActsAsTaggableOn::Tagging.table_name}.taggable_id FROM #{ActsAsTaggableOn::Tagging.table_name} JOIN #{ActsAsTaggableOn::Tag.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
108 |
109 | if owned_by
110 | joins << "JOIN #{ActsAsTaggableOn::Tagging.table_name}" +
111 | " ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
112 | " AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)}" +
113 | " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{owned_by.id}" +
114 | " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s)}"
115 | end
116 |
117 | elsif options.delete(:any)
118 | # get tags, drop out if nothing returned (we need at least one)
119 | tags = if options.delete(:wild)
120 | ActsAsTaggableOn::Tag.named_like_any(tag_list)
121 | else
122 | ActsAsTaggableOn::Tag.named_any(tag_list)
123 | end
124 |
125 | return empty_result unless tags.length > 0
126 |
127 | # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
128 | # avoid ambiguous column name
129 | taggings_context = context ? "_#{context}" : ''
130 |
131 | taggings_alias = adjust_taggings_alias(
132 | "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:name).join('_'))}"
133 | )
134 |
135 | tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
136 | " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
137 | " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
138 | tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
139 |
140 | # don't need to sanitize sql, map all ids and join with OR logic
141 | conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{t.id}" }.join(" OR ")
142 | select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one?
143 |
144 | if owned_by
145 | tagging_join << " AND " +
146 | sanitize_sql([
147 | "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
148 | owned_by.id,
149 | owned_by.class.base_class.to_s
150 | ])
151 | end
152 |
153 | joins << tagging_join
154 | else
155 | tags = ActsAsTaggableOn::Tag.named_any(tag_list)
156 |
157 | return empty_result unless tags.length == tag_list.length
158 |
159 | tags.each do |tag|
160 | taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}")
161 | tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
162 | " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
163 | " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
164 | " AND #{taggings_alias}.tag_id = #{tag.id}"
165 |
166 | tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
167 |
168 | if owned_by
169 | tagging_join << " AND " +
170 | sanitize_sql([
171 | "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
172 | owned_by.id,
173 | owned_by.class.base_class.to_s
174 | ])
175 | end
176 |
177 | joins << tagging_join
178 | end
179 | end
180 |
181 | taggings_alias, tags_alias = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
182 |
183 | if options.delete(:match_all)
184 | joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
185 | " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
186 | " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
187 |
188 |
189 | group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
190 | group = group_columns
191 | having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
192 | end
193 |
194 | select(select_clause) \
195 | .joins(joins.join(" ")) \
196 | .where(conditions.join(" AND ")) \
197 | .group(group) \
198 | .having(having) \
199 | .order(options[:order]) \
200 | .readonly(false)
201 | end
202 |
203 | def is_taggable?
204 | true
205 | end
206 |
207 | def adjust_taggings_alias(taggings_alias)
208 | if taggings_alias.size > 75
209 | taggings_alias = 'taggings_alias_' + Digest::SHA1.hexdigest(taggings_alias)
210 | end
211 | taggings_alias
212 | end
213 |
214 | def taggable_mixin
215 | @taggable_mixin ||= Module.new
216 | end
217 | end
218 |
219 | module InstanceMethods
220 | # all column names are necessary for PostgreSQL group clause
221 | def grouped_column_names_for(object)
222 | self.class.grouped_column_names_for(object)
223 | end
224 |
225 | def custom_contexts
226 | @custom_contexts ||= []
227 | end
228 |
229 | def is_taggable?
230 | self.class.is_taggable?
231 | end
232 |
233 | def add_custom_context(value)
234 | custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
235 | end
236 |
237 | def cached_tag_list_on(context)
238 | self["cached_#{context.to_s.singularize}_list"]
239 | end
240 |
241 | def tag_list_cache_set_on(context)
242 | variable_name = "@#{context.to_s.singularize}_list"
243 | instance_variable_defined?(variable_name) && !instance_variable_get(variable_name).nil?
244 | end
245 |
246 | def tag_list_cache_on(context)
247 | variable_name = "@#{context.to_s.singularize}_list"
248 | if instance_variable_get(variable_name)
249 | instance_variable_get(variable_name)
250 | elsif cached_tag_list_on(context) && self.class.caching_tag_list_on?(context)
251 | instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(cached_tag_list_on(context)))
252 | else
253 | instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
254 | end
255 | end
256 |
257 | def tag_list_on(context)
258 | add_custom_context(context)
259 | tag_list_cache_on(context)
260 | end
261 |
262 | def all_tags_list_on(context)
263 | variable_name = "@all_#{context.to_s.singularize}_list"
264 | return instance_variable_get(variable_name) if instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
265 |
266 | instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze)
267 | end
268 |
269 | ##
270 | # Returns all tags of a given context
271 | def all_tags_on(context)
272 | tag_table_name = ActsAsTaggableOn::Tag.table_name
273 | tagging_table_name = ActsAsTaggableOn::Tagging.table_name
274 |
275 | opts = ["#{tagging_table_name}.context = ?", context.to_s]
276 | scope = base_tags.where(opts)
277 |
278 | if ActsAsTaggableOn::Tag.using_postgresql?
279 | group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
280 | scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
281 | else
282 | scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
283 | end.to_a
284 | end
285 |
286 | ##
287 | # Returns all tags that are not owned of a given context
288 | def tags_on(context)
289 | scope = base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s])
290 | # when preserving tag order, return tags in created order
291 | # if we added the order to the association this would always apply
292 | scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
293 | scope
294 | end
295 |
296 | def set_tag_list_on(context, new_list)
297 | add_custom_context(context)
298 |
299 | variable_name = "@#{context.to_s.singularize}_list"
300 | process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
301 |
302 | instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
303 | end
304 |
305 | def tagging_contexts
306 | custom_contexts + self.class.tag_types.map(&:to_s)
307 | end
308 |
309 | def process_dirty_object(context,new_list)
310 | value = new_list.is_a?(Array) ? new_list.join(', ') : new_list
311 | attrib = "#{context.to_s.singularize}_list"
312 |
313 | if changed_attributes.include?(attrib)
314 | # The attribute already has an unsaved change.
315 | old = changed_attributes[attrib]
316 | changed_attributes.delete(attrib) if (old.to_s == value.to_s)
317 | else
318 | old = tag_list_on(context).to_s
319 | changed_attributes[attrib] = old if (old.to_s != value.to_s)
320 | end
321 | end
322 |
323 | def reload(*args)
324 | self.class.tag_types.each do |context|
325 | instance_variable_set("@#{context.to_s.singularize}_list", nil)
326 | instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
327 | end
328 |
329 | super(*args)
330 | end
331 |
332 | def save_tags
333 | tagging_contexts.each do |context|
334 | next unless tag_list_cache_set_on(context)
335 | # List of currently assigned tag names
336 | tag_list = tag_list_cache_on(context).uniq
337 |
338 | # Find existing tags or create non-existing tags:
339 | tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
340 |
341 | # Tag objects for currently assigned tags
342 | current_tags = tags_on(context)
343 |
344 | # Tag maintenance based on whether preserving the created order of tags
345 | if self.class.preserve_tag_order?
346 | old_tags, new_tags = current_tags - tags, tags - current_tags
347 |
348 | shared_tags = current_tags & tags
349 |
350 | if shared_tags.any? && tags[0...shared_tags.size] != shared_tags
351 | index = shared_tags.each_with_index { |_, i| break i unless shared_tags[i] == tags[i] }
352 |
353 | # Update arrays of tag objects
354 | old_tags |= current_tags[index...current_tags.size]
355 | new_tags |= current_tags[index...current_tags.size] & shared_tags
356 |
357 | # Order the array of tag objects to match the tag list
358 | new_tags = tags.map do |t|
359 | new_tags.find { |n| n.name.downcase == t.name.downcase }
360 | end.compact
361 | end
362 | else
363 | # Delete discarded tags and create new tags
364 | old_tags = current_tags - tags
365 | new_tags = tags - current_tags
366 | end
367 |
368 | # Find taggings to remove:
369 | if old_tags.present?
370 | old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil, :context => context.to_s, :tag_id => old_tags)
371 | end
372 |
373 | # Destroy old taggings:
374 | if old_taggings.present?
375 | ActsAsTaggableOn::Tagging.destroy_all "#{ActsAsTaggableOn::Tagging.primary_key}".to_sym => old_taggings.map(&:id)
376 | end
377 |
378 | # Create new taggings:
379 | new_tags.each do |tag|
380 | taggings.create!(:tag_id => tag.id, :context => context.to_s, :taggable => self)
381 | end
382 | end
383 |
384 | true
385 | end
386 | end
387 | end
388 | end
389 |
--------------------------------------------------------------------------------
/spec/acts_as_taggable_on/taggable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "Taggable To Preserve Order" do
4 | before(:each) do
5 | clean_database!
6 | @taggable = OrderedTaggableModel.new(:name => "Bob Jones")
7 | end
8 |
9 | it "should have tag types" do
10 | [:tags, :colours].each do |type|
11 | OrderedTaggableModel.tag_types.should include type
12 | end
13 |
14 | @taggable.tag_types.should == OrderedTaggableModel.tag_types
15 | end
16 |
17 | it "should have tag associations" do
18 | [:tags, :colours].each do |type|
19 | @taggable.respond_to?(type).should be_true
20 | @taggable.respond_to?("#{type.to_s.singularize}_taggings").should be_true
21 | end
22 | end
23 |
24 | # it "should have tag associations ordered by id" do
25 | # [:tags, :colours].each do |type|
26 | # OrderedTaggableModel.reflect_on_association(type).options[:order].should include('id')
27 | # OrderedTaggableModel.reflect_on_association("#{type.to_s.singularize}_taggings".to_sym).options[:order].should include('id')
28 | # end
29 | # end
30 |
31 | it "should have tag methods" do
32 | [:tags, :colours].each do |type|
33 | @taggable.respond_to?("#{type.to_s.singularize}_list").should be_true
34 | @taggable.respond_to?("#{type.to_s.singularize}_list=").should be_true
35 | @taggable.respond_to?("all_#{type.to_s}_list").should be_true
36 | end
37 | end
38 |
39 | it "should return tag list in the order the tags were created" do
40 | # create
41 | @taggable.tag_list = "rails, ruby, css"
42 | @taggable.instance_variable_get("@tag_list").instance_of?(ActsAsTaggableOn::TagList).should be_true
43 |
44 | lambda {
45 | @taggable.save
46 | }.should change(ActsAsTaggableOn::Tag, :count).by(3)
47 |
48 | @taggable.reload
49 | @taggable.tag_list.should == %w(rails ruby css)
50 |
51 | # update
52 | @taggable.tag_list = "pow, ruby, rails"
53 | @taggable.save
54 |
55 | @taggable.reload
56 | @taggable.tag_list.should == %w(pow ruby rails)
57 |
58 | # update with no change
59 | @taggable.tag_list = "pow, ruby, rails"
60 | @taggable.save
61 |
62 | @taggable.reload
63 | @taggable.tag_list.should == %w(pow ruby rails)
64 |
65 | # update to clear tags
66 | @taggable.tag_list = ""
67 | @taggable.save
68 |
69 | @taggable.reload
70 | @taggable.tag_list.should == []
71 | end
72 |
73 | it "should return tag objects in the order the tags were created" do
74 | # create
75 | @taggable.tag_list = "pow, ruby, rails"
76 | @taggable.instance_variable_get("@tag_list").instance_of?(ActsAsTaggableOn::TagList).should be_true
77 |
78 | lambda {
79 | @taggable.save
80 | }.should change(ActsAsTaggableOn::Tag, :count).by(3)
81 |
82 | @taggable.reload
83 | @taggable.tags.map{|t| t.name}.should == %w(pow ruby rails)
84 |
85 | # update
86 | @taggable.tag_list = "rails, ruby, css, pow"
87 | @taggable.save
88 |
89 | @taggable.reload
90 | @taggable.tags.map{|t| t.name}.should == %w(rails ruby css pow)
91 | end
92 |
93 | it "should return tag objects in tagging id order" do
94 | # create
95 | @taggable.tag_list = "pow, ruby, rails"
96 | @taggable.save
97 |
98 | @taggable.reload
99 | ids = @taggable.tags.map{|t| t.taggings.first.id}
100 | ids.should == ids.sort
101 |
102 | # update
103 | @taggable.tag_list = "rails, ruby, css, pow"
104 | @taggable.save
105 |
106 | @taggable.reload
107 | ids = @taggable.tags.map{|t| t.taggings.first.id}
108 | ids.should == ids.sort
109 | end
110 | end
111 |
112 | describe "Taggable" do
113 | before(:each) do
114 | clean_database!
115 | @taggable = TaggableModel.new(:name => "Bob Jones")
116 | @taggables = [@taggable, TaggableModel.new(:name => "John Doe")]
117 | end
118 |
119 | it "should have tag types" do
120 | [:tags, :languages, :skills, :needs, :offerings].each do |type|
121 | TaggableModel.tag_types.should include type
122 | end
123 |
124 | @taggable.tag_types.should == TaggableModel.tag_types
125 | end
126 |
127 | it "should have tag_counts_on" do
128 | TaggableModel.tag_counts_on(:tags).should be_empty
129 |
130 | @taggable.tag_list = ["awesome", "epic"]
131 | @taggable.save
132 |
133 | TaggableModel.tag_counts_on(:tags).length.should == 2
134 | @taggable.tag_counts_on(:tags).length.should == 2
135 | end
136 |
137 | it "should have tags_on" do
138 | TaggableModel.tags_on(:tags).should be_empty
139 |
140 | @taggable.tag_list = ["awesome", "epic"]
141 | @taggable.save
142 |
143 | TaggableModel.tags_on(:tags).length.should == 2
144 | @taggable.tags_on(:tags).length.should == 2
145 | end
146 |
147 | it "should return [] right after create" do
148 | blank_taggable = TaggableModel.new(:name => "Bob Jones")
149 | blank_taggable.tag_list.should == []
150 | end
151 |
152 | it "should be able to create tags" do
153 | @taggable.skill_list = "ruby, rails, css"
154 | @taggable.instance_variable_get("@skill_list").instance_of?(ActsAsTaggableOn::TagList).should be_true
155 |
156 | lambda {
157 | @taggable.save
158 | }.should change(ActsAsTaggableOn::Tag, :count).by(3)
159 |
160 | @taggable.reload
161 | @taggable.skill_list.sort.should == %w(ruby rails css).sort
162 | end
163 |
164 | it "should be able to create tags through the tag list directly" do
165 | @taggable.tag_list_on(:test).add("hello")
166 | @taggable.tag_list_cache_on(:test).should_not be_empty
167 | @taggable.tag_list_on(:test).should == ["hello"]
168 |
169 | @taggable.save
170 | @taggable.save_tags
171 |
172 | @taggable.reload
173 | @taggable.tag_list_on(:test).should == ["hello"]
174 | end
175 |
176 | it "should differentiate between contexts" do
177 | @taggable.skill_list = "ruby, rails, css"
178 | @taggable.tag_list = "ruby, bob, charlie"
179 | @taggable.save
180 | @taggable.reload
181 | @taggable.skill_list.should include("ruby")
182 | @taggable.skill_list.should_not include("bob")
183 | end
184 |
185 | it "should be able to remove tags through list alone" do
186 | @taggable.skill_list = "ruby, rails, css"
187 | @taggable.save
188 | @taggable.reload
189 | @taggable.should have(3).skills
190 | @taggable.skill_list = "ruby, rails"
191 | @taggable.save
192 | @taggable.reload
193 | @taggable.should have(2).skills
194 | end
195 |
196 | it "should be able to select taggables by subset of tags using ActiveRelation methods" do
197 | @taggables[0].tag_list = "bob"
198 | @taggables[1].tag_list = "charlie"
199 | @taggables[0].skill_list = "ruby"
200 | @taggables[1].skill_list = "css"
201 | @taggables.each{|taggable| taggable.save}
202 |
203 | @found_taggables_by_tag = TaggableModel.joins(:tags).where(:tags => {:name => ["bob"]})
204 | @found_taggables_by_skill = TaggableModel.joins(:skills).where(:tags => {:name => ["ruby"]})
205 |
206 | @found_taggables_by_tag.should include @taggables[0]
207 | @found_taggables_by_tag.should_not include @taggables[1]
208 | @found_taggables_by_skill.should include @taggables[0]
209 | @found_taggables_by_skill.should_not include @taggables[1]
210 | end
211 |
212 | it "should be able to find by tag" do
213 | @taggable.skill_list = "ruby, rails, css"
214 | @taggable.save
215 |
216 | TaggableModel.tagged_with("ruby").first.should == @taggable
217 | end
218 |
219 | it "should be able to find by tag with context" do
220 | @taggable.skill_list = "ruby, rails, css"
221 | @taggable.tag_list = "bob, charlie"
222 | @taggable.save
223 |
224 | TaggableModel.tagged_with("ruby").first.should == @taggable
225 | TaggableModel.tagged_with("ruby, css").first.should == @taggable
226 | TaggableModel.tagged_with("bob", :on => :skills).first.should_not == @taggable
227 | TaggableModel.tagged_with("bob", :on => :tags).first.should == @taggable
228 | end
229 |
230 | it "should not care about case" do
231 | bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby")
232 | frank = TaggableModel.create(:name => "Frank", :tag_list => "Ruby")
233 |
234 | ActsAsTaggableOn::Tag.all.size.should == 1
235 | TaggableModel.tagged_with("ruby").to_a.should == TaggableModel.tagged_with("Ruby").to_a
236 | end
237 |
238 | it "should be able to get tag counts on model as a whole" do
239 | bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
240 | frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
241 | charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
242 | TaggableModel.tag_counts.should_not be_empty
243 | TaggableModel.skill_counts.should_not be_empty
244 | end
245 |
246 | it "should be able to get all tag counts on model as whole" do
247 | bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
248 | frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
249 | charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
250 |
251 | TaggableModel.all_tag_counts.should_not be_empty
252 | TaggableModel.all_tag_counts(:order => 'tags.id').first.count.should == 3 # ruby
253 | end
254 |
255 | it "should be able to get all tags on model as whole" do
256 | bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
257 | frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
258 | charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
259 |
260 | TaggableModel.all_tags.should_not be_empty
261 | TaggableModel.all_tags(:order => 'tags.id').first.name.should == "ruby"
262 | end
263 |
264 | it "should be able to use named scopes to chain tag finds by any tags by context" do
265 | bob = TaggableModel.create(:name => "Bob", :need_list => "rails", :offering_list => "c++")
266 | frank = TaggableModel.create(:name => "Frank", :need_list => "css", :offering_list => "css")
267 | steve = TaggableModel.create(:name => 'Steve', :need_list => "c++", :offering_list => "java")
268 |
269 | # Let's only find those who need rails or css and are offering c++ or java
270 | TaggableModel.tagged_with(['rails, css'], :on => :needs, :any => true).tagged_with(['c++', 'java'], :on => :offerings, :any => true).to_a.should == [bob]
271 | end
272 |
273 | it "should not return read-only records" do
274 | TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
275 | TaggableModel.tagged_with("ruby").first.should_not be_readonly
276 | end
277 |
278 | it "should be able to get scoped tag counts" do
279 | bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
280 | frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
281 | charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
282 |
283 | TaggableModel.tagged_with("ruby").tag_counts(:order => 'tags.id').first.count.should == 2 # ruby
284 | TaggableModel.tagged_with("ruby").skill_counts.first.count.should == 1 # ruby
285 | end
286 |
287 | it "should be able to get all scoped tag counts" do
288 | bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
289 | frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
290 | charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
291 |
292 | TaggableModel.tagged_with("ruby").all_tag_counts(:order => 'tags.id').first.count.should == 3 # ruby
293 | end
294 |
295 | it "should be able to get all scoped tags" do
296 | bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
297 | frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
298 | charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
299 |
300 | TaggableModel.tagged_with("ruby").all_tags(:order => 'tags.id').first.name.should == "ruby"
301 | end
302 |
303 | it 'should only return tag counts for the available scope' do
304 | bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
305 | frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
306 | charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby, java")
307 |
308 | TaggableModel.tagged_with('rails').all_tag_counts.should have(3).items
309 | TaggableModel.tagged_with('rails').all_tag_counts.any? { |tag| tag.name == 'java' }.should be_false
310 |
311 | # Test specific join syntaxes:
312 | frank.untaggable_models.create!
313 | TaggableModel.tagged_with('rails').joins(:untaggable_models).all_tag_counts.should have(2).items
314 | TaggableModel.tagged_with('rails').joins(:untaggable_models => :taggable_model).all_tag_counts.should have(2).items
315 | TaggableModel.tagged_with('rails').joins([:untaggable_models]).all_tag_counts.should have(2).items
316 | end
317 |
318 | it 'should only return tags for the available scope' do
319 | bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
320 | frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
321 | charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby, java")
322 |
323 | TaggableModel.tagged_with('rails').all_tags.should have(3).items
324 | TaggableModel.tagged_with('rails').all_tags.any? { |tag| tag.name == 'java' }.should be_false
325 |
326 | # Test specific join syntaxes:
327 | frank.untaggable_models.create!
328 | TaggableModel.tagged_with('rails').joins(:untaggable_models).all_tags.should have(2).items
329 | TaggableModel.tagged_with('rails').joins({ :untaggable_models => :taggable_model }).all_tags.should have(2).items
330 | TaggableModel.tagged_with('rails').joins([:untaggable_models]).all_tags.should have(2).items
331 | end
332 |
333 | it "should be able to set a custom tag context list" do
334 | bob = TaggableModel.create(:name => "Bob")
335 | bob.set_tag_list_on(:rotors, "spinning, jumping")
336 | bob.tag_list_on(:rotors).should == ["spinning","jumping"]
337 | bob.save
338 | bob.reload
339 | bob.tags_on(:rotors).should_not be_empty
340 | end
341 |
342 | it "should be able to find tagged" do
343 | bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive", :skill_list => "ruby, rails, css")
344 | frank = TaggableModel.create(:name => "Frank", :tag_list => "weaker, depressed, inefficient", :skill_list => "ruby, rails, css")
345 | steve = TaggableModel.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, ruby')
346 |
347 | TaggableModel.tagged_with("ruby", :order => 'taggable_models.name').to_a.should == [bob, frank, steve]
348 | TaggableModel.tagged_with("ruby, rails", :order => 'taggable_models.name').to_a.should == [bob, frank]
349 | TaggableModel.tagged_with(["ruby", "rails"], :order => 'taggable_models.name').to_a.should == [bob, frank]
350 | end
351 |
352 | it "should be able to find tagged with quotation marks" do
353 | bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive, 'I love the ,comma,'")
354 | TaggableModel.tagged_with("'I love the ,comma,'").should include(bob)
355 | end
356 |
357 | it "should be able to find tagged with invalid tags" do
358 | bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive")
359 | TaggableModel.tagged_with("sad, happier").should_not include(bob)
360 | end
361 |
362 | it "should be able to find tagged with any tag" do
363 | bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive", :skill_list => "ruby, rails, css")
364 | frank = TaggableModel.create(:name => "Frank", :tag_list => "weaker, depressed, inefficient", :skill_list => "ruby, rails, css")
365 | steve = TaggableModel.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, ruby')
366 |
367 | TaggableModel.tagged_with(["ruby", "java"], :order => 'taggable_models.name', :any => true).to_a.should == [bob, frank, steve]
368 | TaggableModel.tagged_with(["c++", "fitter"], :order => 'taggable_models.name', :any => true).to_a.should == [bob, steve]
369 | TaggableModel.tagged_with(["depressed", "css"], :order => 'taggable_models.name', :any => true).to_a.should == [bob, frank]
370 | end
371 |
372 | context "wild: true" do
373 | it "should use params as wildcards" do
374 | bob = TaggableModel.create(:name => "Bob", :tag_list => "bob, tricia")
375 | frank = TaggableModel.create(:name => "Frank", :tag_list => "bobby, jim")
376 | steve = TaggableModel.create(:name => "Steve", :tag_list => "john, patricia")
377 | jim = TaggableModel.create(:name => "Jim", :tag_list => "jim, steve")
378 |
379 |
380 | TaggableModel.tagged_with(["bob", "tricia"], :wild => true, :any => true).to_a.sort_by{|o| o.id}.should == [bob, frank, steve]
381 | TaggableModel.tagged_with(["bob", "tricia"], :wild => true, :exclude => true).to_a.should == [jim]
382 | end
383 | end
384 |
385 | it "should be able to find tagged on a custom tag context" do
386 | bob = TaggableModel.create(:name => "Bob")
387 | bob.set_tag_list_on(:rotors, "spinning, jumping")
388 | bob.tag_list_on(:rotors).should == ["spinning","jumping"]
389 | bob.save
390 |
391 | TaggableModel.tagged_with("spinning", :on => :rotors).to_a.should == [bob]
392 | end
393 |
394 | it "should be able to use named scopes to chain tag finds" do
395 | bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive", :skill_list => "ruby, rails, css")
396 | frank = TaggableModel.create(:name => "Frank", :tag_list => "weaker, depressed, inefficient", :skill_list => "ruby, rails, css")
397 | steve = TaggableModel.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, python')
398 |
399 | # Let's only find those productive Rails developers
400 | TaggableModel.tagged_with('rails', :on => :skills, :order => 'taggable_models.name').to_a.should == [bob, frank]
401 | TaggableModel.tagged_with('happier', :on => :tags, :order => 'taggable_models.name').to_a.should == [bob, steve]
402 | TaggableModel.tagged_with('rails', :on => :skills).tagged_with('happier', :on => :tags).to_a.should == [bob]
403 | TaggableModel.tagged_with('rails').tagged_with('happier', :on => :tags).to_a.should == [bob]
404 | end
405 |
406 | it "should be able to find tagged with only the matching tags" do
407 | bob = TaggableModel.create(:name => "Bob", :tag_list => "lazy, happier")
408 | frank = TaggableModel.create(:name => "Frank", :tag_list => "fitter, happier, inefficient")
409 | steve = TaggableModel.create(:name => 'Steve', :tag_list => "fitter, happier")
410 |
411 | TaggableModel.tagged_with("fitter, happier", :match_all => true).to_a.should == [steve]
412 | end
413 |
414 | it "should be able to find tagged with some excluded tags" do
415 | bob = TaggableModel.create(:name => "Bob", :tag_list => "happier, lazy")
416 | frank = TaggableModel.create(:name => "Frank", :tag_list => "happier")
417 | steve = TaggableModel.create(:name => 'Steve', :tag_list => "happier")
418 |
419 | TaggableModel.tagged_with("lazy", :exclude => true).to_a.should == [frank, steve]
420 | end
421 |
422 | it "should return an empty scope for empty tags" do
423 | TaggableModel.tagged_with('').should == []
424 | TaggableModel.tagged_with(' ').should == []
425 | TaggableModel.tagged_with(nil).should == []
426 | TaggableModel.tagged_with([]).should == []
427 | end
428 |
429 | it "should not create duplicate taggings" do
430 | bob = TaggableModel.create(:name => "Bob")
431 | lambda {
432 | bob.tag_list << "happier"
433 | bob.tag_list << "happier"
434 | bob.save
435 | }.should change(ActsAsTaggableOn::Tagging, :count).by(1)
436 | end
437 |
438 | describe "Associations" do
439 | before(:each) do
440 | @taggable = TaggableModel.create(:tag_list => "awesome, epic")
441 | end
442 |
443 | it "should not remove tags when creating associated objects" do
444 | @taggable.untaggable_models.create!
445 | @taggable.reload
446 | @taggable.tag_list.should have(2).items
447 | end
448 | end
449 |
450 | describe "grouped_column_names_for method" do
451 | it "should return all column names joined for Tag GROUP clause" do
452 | @taggable.grouped_column_names_for(ActsAsTaggableOn::Tag).should == "tags.id, tags.name"
453 | end
454 |
455 | it "should return all column names joined for TaggableModel GROUP clause" do
456 | @taggable.grouped_column_names_for(TaggableModel).should == "taggable_models.id, taggable_models.name, taggable_models.type"
457 | end
458 |
459 | it "should return all column names joined for NonStandardIdTaggableModel GROUP clause" do
460 | @taggable.grouped_column_names_for(TaggableModel).should == "taggable_models.#{TaggableModel.primary_key}, taggable_models.name, taggable_models.type"
461 | end
462 | end
463 |
464 | describe "NonStandardIdTaggable" do
465 | before(:each) do
466 | clean_database!
467 | @taggable = NonStandardIdTaggableModel.new(:name => "Bob Jones")
468 | @taggables = [@taggable, NonStandardIdTaggableModel.new(:name => "John Doe")]
469 | end
470 |
471 | it "should have tag types" do
472 | [:tags, :languages, :skills, :needs, :offerings].each do |type|
473 | NonStandardIdTaggableModel.tag_types.should include type
474 | end
475 |
476 | @taggable.tag_types.should == NonStandardIdTaggableModel.tag_types
477 | end
478 |
479 | it "should have tag_counts_on" do
480 | NonStandardIdTaggableModel.tag_counts_on(:tags).should be_empty
481 |
482 | @taggable.tag_list = ["awesome", "epic"]
483 | @taggable.save
484 |
485 | NonStandardIdTaggableModel.tag_counts_on(:tags).length.should == 2
486 | @taggable.tag_counts_on(:tags).length.should == 2
487 | end
488 |
489 | it "should have tags_on" do
490 | NonStandardIdTaggableModel.tags_on(:tags).should be_empty
491 |
492 | @taggable.tag_list = ["awesome", "epic"]
493 | @taggable.save
494 |
495 | NonStandardIdTaggableModel.tags_on(:tags).length.should == 2
496 | @taggable.tags_on(:tags).length.should == 2
497 | end
498 |
499 | it "should be able to create tags" do
500 | @taggable.skill_list = "ruby, rails, css"
501 | @taggable.instance_variable_get("@skill_list").instance_of?(ActsAsTaggableOn::TagList).should be_true
502 |
503 | lambda {
504 | @taggable.save
505 | }.should change(ActsAsTaggableOn::Tag, :count).by(3)
506 |
507 | @taggable.reload
508 | @taggable.skill_list.sort.should == %w(ruby rails css).sort
509 | end
510 |
511 | it "should be able to create tags through the tag list directly" do
512 | @taggable.tag_list_on(:test).add("hello")
513 | @taggable.tag_list_cache_on(:test).should_not be_empty
514 | @taggable.tag_list_on(:test).should == ["hello"]
515 |
516 | @taggable.save
517 | @taggable.save_tags
518 |
519 | @taggable.reload
520 | @taggable.tag_list_on(:test).should == ["hello"]
521 | end
522 | end
523 |
524 | describe "Dirty Objects" do
525 | context "with un-contexted tags" do
526 | before(:each) do
527 | @taggable = TaggableModel.create(:tag_list => "awesome, epic")
528 | end
529 |
530 | context "when tag_list changed" do
531 | before(:each) do
532 | @taggable.changes.should == {}
533 | @taggable.tag_list = 'one'
534 | end
535 |
536 | it 'should show changes of dirty object' do
537 | @taggable.changes.should == {"tag_list"=>["awesome, epic", ["one"]]}
538 | end
539 |
540 | it 'flags tag_list as changed' do
541 | @taggable.tag_list_changed?.should be_true
542 | end
543 |
544 | it 'preserves original value' do
545 | @taggable.tag_list_was.should == "awesome, epic"
546 | end
547 |
548 | it 'shows what the change was' do
549 | @taggable.tag_list_change.should == ["awesome, epic", ["one"]]
550 | end
551 | end
552 |
553 | context 'when tag_list is the same' do
554 | before(:each) do
555 | @taggable.tag_list = "awesome, epic"
556 | end
557 |
558 | it 'is not flagged as changed' do
559 | @taggable.tag_list_changed?.should be_false
560 | end
561 |
562 | it 'does not show any changes to the taggable item' do
563 | @taggable.changes.should == {}
564 | end
565 | end
566 | end
567 |
568 | context "with context tags" do
569 | before(:each) do
570 | @taggable = TaggableModel.create(:language_list => "awesome, epic")
571 | end
572 |
573 | context "when language_list changed" do
574 | before(:each) do
575 | @taggable.changes.should == {}
576 | @taggable.language_list = 'one'
577 | end
578 |
579 | it 'should show changes of dirty object' do
580 | @taggable.changes.should == {"language_list"=>["awesome, epic", ["one"]]}
581 | end
582 |
583 | it 'flags language_list as changed' do
584 | @taggable.language_list_changed?.should be_true
585 | end
586 |
587 | it 'preserves original value' do
588 | @taggable.language_list_was.should == "awesome, epic"
589 | end
590 |
591 | it 'shows what the change was' do
592 | @taggable.language_list_change.should == ["awesome, epic", ["one"]]
593 | end
594 |
595 | it 'shows what the changes were' do
596 | @taggable.language_list_changes.should == ["awesome, epic", ["one"]]
597 | end
598 | end
599 |
600 | context 'when language_list is the same' do
601 | before(:each) do
602 | @taggable.language_list = "awesome, epic"
603 | end
604 |
605 | it 'is not flagged as changed' do
606 | @taggable.language_list_changed?.should be_false
607 | end
608 |
609 | it 'does not show any changes to the taggable item' do
610 | @taggable.changes.should == {}
611 | end
612 | end
613 | end
614 | end
615 |
616 | describe "Autogenerated methods" do
617 | it "should be overridable" do
618 | TaggableModel.create(:tag_list=>'woo').tag_list_submethod_called.should be_true
619 | end
620 | end
621 | end
622 |
623 |
624 |
--------------------------------------------------------------------------------