From 1c8bcc58d410db6d7eb5f1629813f08f78f47fa1 Mon Sep 17 00:00:00 2001 From: hukl Date: Mon, 2 Feb 2009 23:25:15 +0100 Subject: add acts_as_taggable_on_steroids to replaces custom flagging facilities --- .../20090202222400_acts_as_taggable_migration.rb | 26 ++ .../plugins/acts_as_taggable_on_steroids/CHANGELOG | 167 ++++++++++ .../acts_as_taggable_on_steroids/MIT-LICENSE | 20 ++ vendor/plugins/acts_as_taggable_on_steroids/README | 153 +++++++++ .../plugins/acts_as_taggable_on_steroids/Rakefile | 22 ++ .../acts_as_taggable_migration_generator.rb | 11 + .../templates/migration.rb | 26 ++ .../plugins/acts_as_taggable_on_steroids/init.rb | 1 + .../lib/acts_as_taggable.rb | 214 +++++++++++++ .../acts_as_taggable_on_steroids/lib/tag.rb | 26 ++ .../lib/tag_counts_extension.rb | 3 + .../acts_as_taggable_on_steroids/lib/tag_list.rb | 91 ++++++ .../acts_as_taggable_on_steroids/lib/tagging.rb | 12 + .../lib/tags_helper.rb | 13 + .../test/abstract_unit.rb | 97 ++++++ .../test/acts_as_taggable_test.rb | 347 +++++++++++++++++++++ .../test/fixtures/magazine.rb | 3 + .../test/fixtures/magazines.yml | 7 + .../test/fixtures/photo.rb | 8 + .../test/fixtures/photos.yml | 24 ++ .../test/fixtures/post.rb | 7 + .../test/fixtures/posts.yml | 34 ++ .../test/fixtures/special_post.rb | 2 + .../test/fixtures/subscription.rb | 4 + .../test/fixtures/subscriptions.yml | 3 + .../test/fixtures/taggings.yml | 149 +++++++++ .../test/fixtures/tags.yml | 19 ++ .../test/fixtures/user.rb | 7 + .../test/fixtures/users.yml | 7 + .../test/tag_list_test.rb | 106 +++++++ .../acts_as_taggable_on_steroids/test/tag_test.rb | 34 ++ .../test/tagging_test.rb | 13 + .../test/tags_helper_test.rb | 28 ++ 33 files changed, 1684 insertions(+) create mode 100644 db/migrate/20090202222400_acts_as_taggable_migration.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/CHANGELOG create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/MIT-LICENSE create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/README create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/Rakefile create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/acts_as_taggable_migration_generator.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/templates/migration.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/init.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/lib/acts_as_taggable.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/lib/tag.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/lib/tag_counts_extension.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/lib/tag_list.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/lib/tagging.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/lib/tags_helper.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/abstract_unit.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/acts_as_taggable_test.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/magazine.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/magazines.yml create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photo.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photos.yml create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/post.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/posts.yml create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/special_post.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/subscription.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/subscriptions.yml create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/taggings.yml create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/tags.yml create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/user.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/users.yml create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/tag_list_test.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/tag_test.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/tagging_test.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/tags_helper_test.rb diff --git a/db/migrate/20090202222400_acts_as_taggable_migration.rb b/db/migrate/20090202222400_acts_as_taggable_migration.rb new file mode 100644 index 0000000..ea0c2cc --- /dev/null +++ b/db/migrate/20090202222400_acts_as_taggable_migration.rb @@ -0,0 +1,26 @@ +class ActsAsTaggableMigration < ActiveRecord::Migration + def self.up + create_table :tags do |t| + t.column :name, :string + end + + create_table :taggings do |t| + t.column :tag_id, :integer + t.column :taggable_id, :integer + + # You should make sure that the column created is + # long enough to store the required class names. + t.column :taggable_type, :string + + t.column :created_at, :datetime + end + + add_index :taggings, :tag_id + add_index :taggings, [:taggable_id, :taggable_type] + end + + def self.down + drop_table :taggings + drop_table :tags + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/CHANGELOG b/vendor/plugins/acts_as_taggable_on_steroids/CHANGELOG new file mode 100644 index 0000000..f314bd9 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/CHANGELOG @@ -0,0 +1,167 @@ +[13 March 08] + +* Added helper methods for will_paginate compatibility (Clinton R. Nixon) +* Fixed :conditions in tag_counts to accept array to sanitize (Clinton R. Nixon) + +[07 March 08] + +* Added support for regexp delimiter (Matt Aimonetti) + +[30 Jan 08] + +* Fix Tag.destroy_unused on Rails 2.0. + +[23 October 2007] + +* Make find_options_for_tag_counts and find_options_for_tagged_with dup their options. + +* Apply conditions properly in find_options_for_tag_counts. + +* Fix tag_cloud when no tags are present. + +[22 October 2007] + +* Fix find_tagged_with using :match_all and :include. + +* Use inner joins instead of left outer joins. + +[15 October 2007] + +* Make find_tagged_with correctly apply :conditions + +* Add Tag.destroy_unused option. + +[11 October 2007] + +* Make tag_counts work correctly with STI. + +[3 October 2007] + +* Improve documentation. + +* Fix TagsHelper and test. + +[2 October 2007] + +* Remove TagList.parse, use TagList.from instead. + +* Add :parse option to TagList#new, TagList#add, and TagList#remove. + + tag_list = TagList.new("One, Two", :parse => true) # ["One", "Two"] + + tag_list # ["One", "Two"] + tag_list.add("Three, Four", :parse => true) # ["One", "Two", "Three", "Four"] + +* Remove TagList#names. + +[29 September 2007] + +* Add TagsHelper to assist with generating tag clouds and provide a simple example. + +[27 September 2007] + +* Add #tag_counts method to get tag counts for a specific object's tags. + +* BACKWARDS INCOMPATIBILITY: Rename #find_options_for_tagged_with to #find_options_for_find_tagged_with + +[17 September 2007] + +* Fix clearing of cached tag list when all tags removed. + +[12 September 2007] + +* Make the TagList class inherit from Array. + +* Deprecate obsolete TagList#names. + +[6 September 2007] + +* Add TagList#include? and TagList#empty? + +[26 August 2006] + +* Remove deprecated Tag.delimiter. Use TagList.delimiter instead. + +[25 August 2007] + +* Make tag_counts work with has_many :through + +[23 August 2007] + +* Make search comparisons case-insensitive across different databases. [Moisés Machado] + +* Improve compatiblity with STI. [Moisés Machado] + +[25 July 2007] + +* Respect custom table names for the Tag and Tagging classes. + +* Fix the :exclude option for find_tagged_with + +[17 July 2007] + +* Make the migration work on edge rails + +[8 July 2007] + +* find_options_for_tagged_with should not alter its arguments + +[1 July 2007] + +* Fix incorrect tagging when the case of the tag list is changed. + +* Fix deprecated Tag.delimiter accessor. + +[23 June 2007] + +* Add validation to Tag model. + +* find_options_for_tagged_with should always return a hash. + +* find_tagged_with passing in no tags should return an empty array. + +* Improve compatibility with PostgreSQL. + +[21 June 2007] + +* Remove extra .rb from generated migration file name. + +[15 June 2007] + +* Introduce TagList class. + +* Various cleanups and improvements. + +* Use TagList.delimiter now, not Tag.delimiter. Tag.delimiter will be removed at some stage. + +[11 June 2007] + +* Restructure the creation of the options for find_tagged_with [Thijs Cadier] + +* Add an example migration with a generator. + +* Add caching. + +* Fix compatibility with Ruby < 1.8.6 + +[23 April 2007] + +* Make tag_list to respect Tag.delimiter + +[31 March 2007] + +* Add Tag.delimiter accessor to change how tags are parsed. + +* Fix :include => :tags when used with find_tagged_with + +[7 March 2007] + +* Fix tag_counts for SQLServer [Brad Young] + +[21 Feb 2007] + +* Use scoping instead of TagCountsExtension [Michael Schuerig] + +[7 Jan 2007] + +* Add :match_all to find_tagged_with [Michael Sheakoski] diff --git a/vendor/plugins/acts_as_taggable_on_steroids/MIT-LICENSE b/vendor/plugins/acts_as_taggable_on_steroids/MIT-LICENSE new file mode 100644 index 0000000..602bda2 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2006 Jonathan Viney + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/plugins/acts_as_taggable_on_steroids/README b/vendor/plugins/acts_as_taggable_on_steroids/README new file mode 100644 index 0000000..73b88fe --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/README @@ -0,0 +1,153 @@ += acts_as_taggable_on_steroids + +NOT THE OFFICIAL REPO, I just imported this project to get few bug fixed when I was using it for a project in early 2008. + +USE AT YOUR OWN RISK + + +If you find this plugin useful, please consider a donation to show your support! + + http://www.paypal.com/cgi-bin/webscr?cmd=_send-money + + Email address: jonathan.viney@gmail.com + +== Instructions + +This plugin is based on acts_as_taggable by DHH but includes extras +such as tests, smarter tag assignment, and tag cloud calculations. + +== Installation + + ruby script/plugin install http://svn.viney.net.nz/things/rails/plugins/acts_as_taggable_on_steroids + +== Usage + +=== Prepare database + +Generate and apply the migration: + + ruby script/generate acts_as_taggable_migration + rake db:migrate + +=== Basic tagging + +Let's suppose users have many posts and we want those posts to have tags. +The first step is to add +acts_as_taggable+ to the Post class: + + class Post < ActiveRecord::Base + acts_as_taggable + + belongs_to :user + end + +We can now use the tagging methods provided by acts_as_taggable, #tag_list and #tag_list=. Both these +methods work like regular attribute accessors. + + p = Post.find(:first) + p.tag_list # [] + p.tag_list = "Funny, Silly" + p.save + p.tag_list # ["Funny", "Silly"] + +You can also add or remove arrays of tags. + + p.tag_list.add("Great", "Awful") + p.tag_list.remove("Funny") + +=== Finding tagged objects + +To retrieve objects tagged with a certain tag, use find_tagged_with. + + Post.find_tagged_with('Funny, Silly') + +By default, find_tagged_with will find objects that have any of the given tags. To +find only objects that are tagged with all the given tags, use match_all. + + Post.find_tagged_with('Funny, Silly', :match_all => true) + +See ActiveRecord::Acts::Taggable::InstanceMethods for more methods and options. + +=== Tag cloud calculations + +To construct tag clouds, the frequency of each tag needs to be calculated. +Because we specified +acts_as_taggable+ on the Post class, we can +get a calculation of all the tag counts by using Post.tag_counts. But what if we wanted a tag count for +an single user's posts? To achieve this we call tag_counts on the association: + + User.find(:first).posts.tag_counts + +A helper is included to assist with generating tag clouds. Include it in your helper file: + + module ApplicationHelper + include TagsHelper + end + +Here is an example that generates a tag cloud. + +Controller: + + class PostController < ApplicationController + def tag_cloud + @tags = Post.tag_counts + end + end + +View: + <% tag_cloud @tags, %w(css1 css2 css3 css4) do |tag, css_class| %> + <%= link_to tag.name, { :action => :tag, :id => tag.name }, :class => css_class %> + <% end %> + +CSS: + + .css1 { font-size: 1.0em; } + .css2 { font-size: 1.2em; } + .css3 { font-size: 1.4em; } + .css4 { font-size: 1.6em; } + +=== Caching + +It is useful to cache the list of tags to reduce the number of queries executed. To do this, +add a column named cached_tag_list to the model which is being tagged. The column should be long enough to hold +the full tag list and must have a default value of null, not an empty string. + + class CachePostTagList < ActiveRecord::Migration + def self.up + add_column :posts, :cached_tag_list, :string + end + end + + class Post < ActiveRecord::Base + acts_as_taggable + + # The caching column defaults to cached_tag_list, but can be changed: + # + # set_cached_tag_list_column_name "my_caching_column_name" + end + +The details of the caching are handled for you. Just continue to use the tag_list accessor as you normally would. +Note that the cached tag list will not be updated if you directly create Tagging objects or manually append to the +tags or taggings associations. To update the cached tag list you should call save_cached_tag_list manually. + +=== Delimiter + +If you want to change the delimiter used to parse and present tags, set TagList.delimiter. +For example, to use spaces instead of commas, add the following to config/environment.rb: + + TagList.delimiter = " " + +You can also use a regexp as delimiter: + + TagList.delimiter = /,|;/ + +The above code would parse the string and use ',' and ';' as delimiters. + +=== Unused tags + +Set Tag.destroy_unused to remove tags when they are no longer being +used to tag any objects. Defaults to false. + + Tag.destroy_unused = true + +=== Other + +Problems, comments, and suggestions all welcome. jonathan.viney@gmail.com diff --git a/vendor/plugins/acts_as_taggable_on_steroids/Rakefile b/vendor/plugins/acts_as_taggable_on_steroids/Rakefile new file mode 100644 index 0000000..d2c0003 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/Rakefile @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the acts_as_taggable_on_steroids plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the acts_as_taggable_on_steroids plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'Acts As Taggable On Steroids' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/acts_as_taggable_migration_generator.rb b/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/acts_as_taggable_migration_generator.rb new file mode 100644 index 0000000..be9b39c --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/acts_as_taggable_migration_generator.rb @@ -0,0 +1,11 @@ +class ActsAsTaggableMigrationGenerator < Rails::Generator::Base + def manifest + record do |m| + m.migration_template 'migration.rb', 'db/migrate' + end + end + + def file_name + "acts_as_taggable_migration" + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/templates/migration.rb b/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/templates/migration.rb new file mode 100644 index 0000000..ea0c2cc --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/templates/migration.rb @@ -0,0 +1,26 @@ +class ActsAsTaggableMigration < ActiveRecord::Migration + def self.up + create_table :tags do |t| + t.column :name, :string + end + + create_table :taggings do |t| + t.column :tag_id, :integer + t.column :taggable_id, :integer + + # You should make sure that the column created is + # long enough to store the required class names. + t.column :taggable_type, :string + + t.column :created_at, :datetime + end + + add_index :taggings, :tag_id + add_index :taggings, [:taggable_id, :taggable_type] + end + + def self.down + drop_table :taggings + drop_table :tags + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/init.rb b/vendor/plugins/acts_as_taggable_on_steroids/init.rb new file mode 100644 index 0000000..64505b9 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/init.rb @@ -0,0 +1 @@ +require File.dirname(__FILE__) + '/lib/acts_as_taggable' diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/acts_as_taggable.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/acts_as_taggable.rb new file mode 100644 index 0000000..d537889 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/acts_as_taggable.rb @@ -0,0 +1,214 @@ +module ActiveRecord #:nodoc: + module Acts #:nodoc: + module Taggable #:nodoc: + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def acts_as_taggable + has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag + has_many :tags, :through => :taggings + + before_save :save_cached_tag_list + after_save :save_tags + + include ActiveRecord::Acts::Taggable::InstanceMethods + extend ActiveRecord::Acts::Taggable::SingletonMethods + + alias_method_chain :reload, :tag_list + end + + def cached_tag_list_column_name + "cached_tag_list" + end + + def set_cached_tag_list_column_name(value = nil, &block) + define_attr_method :cached_tag_list_column_name, value, &block + end + end + + module SingletonMethods + # Pass either a tag string, or an array of strings or tags + # + # Options: + # :exclude - Find models that are not tagged with the given tags + # :match_all - Find models that match all of the given tags, not just one + # :conditions - A piece of SQL conditions to add to the query + def find_tagged_with(*args) + options = find_options_for_find_tagged_with(*args) + options.blank? ? [] : find(:all, options) + end + + # will_paginate's method_missing function wants to hit + # find_all_tagged_with if you call paginate_tagged_with, which is + # obviously suboptimal + def find_all_tagged_with(*args) + find_tagged_with(*args) + end + + def find_options_for_find_tagged_with(tags, options = {}) + tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags) + options = options.dup + + return {} if tags.empty? + + conditions = [] + conditions << sanitize_sql(options.delete(:conditions)) if options[:conditions] + + taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags" + + if options.delete(:exclude) + conditions << <<-END + #{table_name}.id NOT IN + (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} + INNER JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id + WHERE #{tags_condition(tags)} AND #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)}) + END + else + if options.delete(:match_all) + conditions << <<-END + (SELECT COUNT(*) FROM #{Tagging.table_name} + INNER JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id + WHERE #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)} AND + taggable_id = #{table_name}.id AND + #{tags_condition(tags)}) = #{tags.size} + END + else + conditions << tags_condition(tags, tags_alias) + end + end + + { :select => "DISTINCT #{table_name}.*", + :joins => "INNER JOIN #{Tagging.table_name} #{taggings_alias} ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key} AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)} " + + "INNER JOIN #{Tag.table_name} #{tags_alias} ON #{tags_alias}.id = #{taggings_alias}.tag_id", + :conditions => conditions.join(" AND ") + }.reverse_merge!(options) + end + + # Calculate the tag counts for all tags. + # + # Options: + # :start_at - Restrict the tags to those created after a certain time + # :end_at - Restrict the tags to those created before a certain time + # :conditions - A piece of SQL conditions to add to the query + # :limit - The maximum number of tags to return + # :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc' + # :at_least - Exclude tags with a frequency less than the given value + # :at_most - Exclude tags with a frequency greater than the given value + def tag_counts(options = {}) + Tag.find(:all, find_options_for_tag_counts(options)) + end + + # Find how many objects are tagged with a certain tag. + def count_by_tag(tag_name) + counts = tag_counts(:conditions => "tags.name = #{quote_value(tag_name)}") + counts[0].respond_to?(:count) ? counts[0].count : 0 + end + + def find_options_for_tag_counts(options = {}) + options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit + options = options.dup + + scope = scope(:find) + start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at] + end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at] + + conditions = [ + "#{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)}", + sanitize_sql(options.delete(:conditions)), + scope && scope[:conditions], + start_at, + end_at + ] + + conditions << type_condition unless descends_from_active_record? + conditions.compact! + conditions = conditions.join(' AND ') + + joins = ["INNER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"] + joins << "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id" + joins << scope[:joins] if scope && scope[:joins] + + at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least] + at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most] + having = [at_least, at_most].compact.join(' AND ') + group_by = "#{Tag.table_name}.id, #{Tag.table_name}.name HAVING COUNT(*) > 0" + group_by << " AND #{having}" unless having.blank? + + { :select => "#{Tag.table_name}.id, #{Tag.table_name}.name, COUNT(*) AS count", + :joins => joins.join(" "), + :conditions => conditions, + :group => group_by + }.reverse_merge!(options) + end + + def caching_tag_list? + column_names.include?(cached_tag_list_column_name) + end + + private + def tags_condition(tags, table_name = Tag.table_name) + condition = tags.map { |t| sanitize_sql(["#{table_name}.name LIKE ?", t]) }.join(" OR ") + "(" + condition + ")" + end + end + + module InstanceMethods + def tag_list + return @tag_list if @tag_list + + if self.class.caching_tag_list? and !(cached_value = send(self.class.cached_tag_list_column_name)).nil? + @tag_list = TagList.from(cached_value) + else + @tag_list = TagList.new(*tags.map(&:name)) + end + end + + def tag_list=(value) + @tag_list = TagList.from(value) + end + + def save_cached_tag_list + if self.class.caching_tag_list? + self[self.class.cached_tag_list_column_name] = tag_list.to_s + end + end + + def save_tags + return unless @tag_list + + new_tag_names = @tag_list - tags.map(&:name) + old_tags = tags.reject { |tag| @tag_list.include?(tag.name) } + + self.class.transaction do + if old_tags.any? + taggings.find(:all, :conditions => ["tag_id IN (?)", old_tags.map(&:id)]).each(&:destroy) + taggings.reset + end + + new_tag_names.each do |new_tag_name| + tags << Tag.find_or_create_with_like_by_name(new_tag_name) + end + end + + true + end + + # Calculate the tag counts for the tags used by this model. + # + # The possible options are the same as the tag_counts class method, excluding :conditions. + def tag_counts(options = {}) + self.class.tag_counts({ :conditions => self.class.send(:tags_condition, tag_list) }.reverse_merge!(options)) + end + + def reload_with_tag_list(*args) #:nodoc: + @tag_list = nil + reload_without_tag_list(*args) + end + end + end + end +end + +ActiveRecord::Base.send(:include, ActiveRecord::Acts::Taggable) diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/tag.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag.rb new file mode 100644 index 0000000..91859d5 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag.rb @@ -0,0 +1,26 @@ +class Tag < ActiveRecord::Base + has_many :taggings + + validates_presence_of :name + validates_uniqueness_of :name + + cattr_accessor :destroy_unused + self.destroy_unused = false + + # LIKE is used for cross-database case-insensitivity + def self.find_or_create_with_like_by_name(name) + find(:first, :conditions => ["name LIKE ?", name]) || create(:name => name) + end + + def ==(object) + super || (object.is_a?(Tag) && name == object.name) + end + + def to_s + name + end + + def count + read_attribute(:count).to_i + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_counts_extension.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_counts_extension.rb new file mode 100644 index 0000000..a1d13b8 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_counts_extension.rb @@ -0,0 +1,3 @@ +# Deprecated +module TagCountsExtension #:nodoc: +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_list.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_list.rb new file mode 100644 index 0000000..01325b8 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_list.rb @@ -0,0 +1,91 @@ +class TagList < Array + cattr_accessor :delimiter + self.delimiter = ',' + + def initialize(*args) + add(*args) + end + + # Add tags to the tag_list. Duplicate or blank tags will be ignored. + # + # tag_list.add("Fun", "Happy") + # + # Use the :parse option to add an unparsed tag string. + # + # tag_list.add("Fun, Happy", :parse => true) + def add(*names) + extract_and_apply_options!(names) + concat(names) + clean! + self + end + + # Remove specific tags from the tag_list. + # + # tag_list.remove("Sad", "Lonely") + # + # Like #add, the :parse option can be used to remove multiple tags in a string. + # + # tag_list.remove("Sad, Lonely", :parse => true) + def remove(*names) + extract_and_apply_options!(names) + delete_if { |name| names.include?(name) } + self + end + + # Transform the tag_list into a tag string suitable for edting in a form. + # The tags are joined with TagList.delimiter and quoted if necessary. + # + # tag_list = TagList.new("Round", "Square,Cube") + # tag_list.to_s # 'Round, "Square,Cube"' + def to_s + clean! + + list = map do |name| + if delimiter.is_a?(Regexp) + name.match(delimiter) ? "\"#{name}\"" : name + else + name.include?(delimiter) ? "\"#{name}\"" : name + end + end + + list.join( delimiter.is_a?(Regexp) ? "#{delimiter.source.match(/[^\\\[\]\*\?\{\}\.\|]/)[0]} " : (delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ") ) + end + + private + # Remove whitespace, duplicates, and blanks. + def clean! + reject!(&:blank?) + map!(&:strip) + uniq! + end + + def extract_and_apply_options!(args) + options = args.last.is_a?(Hash) ? args.pop : {} + options.assert_valid_keys :parse + + if options[:parse] + args.map! { |a| self.class.from(a) } + end + + args.flatten! + end + + class << self + # Returns a new TagList using the given tag string. + # + # tag_list = TagList.from("One , Two, Three") + # tag_list # ["One", "Two", "Three"] + def from(string) + returning new do |tag_list| + string = string.to_s.gsub('.', '').dup + + # Parse the quoted tags + string.gsub!(/"(.*?)"\s*#{delimiter}?\s*/) { tag_list << $1; "" } + string.gsub!(/'(.*?)'\s*#{delimiter}?\s*/) { tag_list << $1; "" } + + tag_list.add(string.split(delimiter)) + end + end + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/tagging.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/tagging.rb new file mode 100644 index 0000000..87bc44d --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/tagging.rb @@ -0,0 +1,12 @@ +class Tagging < ActiveRecord::Base #:nodoc: + belongs_to :tag + belongs_to :taggable, :polymorphic => true + + def after_destroy + if Tag.destroy_unused + if tag.taggings.count.zero? + tag.destroy + end + end + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/tags_helper.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/tags_helper.rb new file mode 100644 index 0000000..d5644b7 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/tags_helper.rb @@ -0,0 +1,13 @@ +module TagsHelper + # See the README for an example using tag_cloud. + def tag_cloud(tags, classes) + return if tags.empty? + + max_count = tags.sort_by(&:count).last.count.to_f + + tags.each do |tag| + index = ((tag.count / max_count) * (classes.size - 1)).round + yield tag, classes[index] + end + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/abstract_unit.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/abstract_unit.rb new file mode 100644 index 0000000..42e277a --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/abstract_unit.rb @@ -0,0 +1,97 @@ +require 'test/unit' + +begin + require File.dirname(__FILE__) + '/../../../../config/environment' +rescue LoadError + require 'rubygems' + gem 'activerecord' + gem 'actionpack' + require 'active_record' + require 'action_controller' +end + +# Search for fixtures first +fixture_path = File.dirname(__FILE__) + '/fixtures/' +Dependencies.load_paths.insert(0, fixture_path) + +require 'active_record/fixtures' + +require File.dirname(__FILE__) + '/../lib/acts_as_taggable' +require_dependency File.dirname(__FILE__) + '/../lib/tag_list' +require_dependency File.dirname(__FILE__) + '/../lib/tags_helper' + +ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + '/debug.log') +ActiveRecord::Base.configurations = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) +ActiveRecord::Base.establish_connection(ENV['DB'] || 'mysql') + +load(File.dirname(__FILE__) + '/schema.rb') + +Test::Unit::TestCase.fixture_path = fixture_path + +class Test::Unit::TestCase #:nodoc: + self.use_transactional_fixtures = true + self.use_instantiated_fixtures = false + + def assert_equivalent(expected, actual, message = nil) + if expected.first.is_a?(ActiveRecord::Base) + assert_equal expected.sort_by(&:id), actual.sort_by(&:id), message + else + assert_equal expected.sort, actual.sort, message + end + end + + def assert_tag_counts(tags, expected_values) + # Map the tag fixture names to real tag names + expected_values = expected_values.inject({}) do |hash, (tag, count)| + hash[tags(tag).name] = count + hash + end + + tags.each do |tag| + value = expected_values.delete(tag.name) + + assert_not_nil value, "Expected count for #{tag.name} was not provided" + assert_equal value, tag.count, "Expected value of #{value} for #{tag.name}, but was #{tag.count}" + end + + unless expected_values.empty? + assert false, "The following tag counts were not present: #{expected_values.inspect}" + end + end + + def assert_queries(num = 1) + $query_count = 0 + yield + ensure + assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed." + end + + def assert_no_queries(&block) + assert_queries(0, &block) + end + + # From Rails trunk + def assert_difference(expressions, difference = 1, message = nil, &block) + expression_evaluations = [expressions].flatten.collect{|expression| lambda { eval(expression, block.binding) } } + + original_values = expression_evaluations.inject([]) { |memo, expression| memo << expression.call } + yield + expression_evaluations.each_with_index do |expression, i| + assert_equal original_values[i] + difference, expression.call, message + end + end + + def assert_no_difference(expressions, message = nil, &block) + assert_difference expressions, 0, message, &block + end +end + +ActiveRecord::Base.connection.class.class_eval do + def execute_with_counting(sql, name = nil, &block) + $query_count ||= 0 + $query_count += 1 + execute_without_counting(sql, name, &block) + end + + alias_method_chain :execute, :counting +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/acts_as_taggable_test.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/acts_as_taggable_test.rb new file mode 100644 index 0000000..90ea3c4 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/acts_as_taggable_test.rb @@ -0,0 +1,347 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +class ActsAsTaggableOnSteroidsTest < Test::Unit::TestCase + fixtures :tags, :taggings, :posts, :users, :photos, :subscriptions, :magazines + + def test_find_tagged_with + assert_equivalent [posts(:jonathan_sky), posts(:sam_flowers)], Post.find_tagged_with('"Very good"') + assert_equal Post.find_tagged_with('"Very good"'), Post.find_tagged_with(['Very good']) + assert_equal Post.find_tagged_with('"Very good"'), Post.find_tagged_with([tags(:good)]) + + assert_equivalent [photos(:jonathan_dog), photos(:sam_flower), photos(:sam_sky)], Photo.find_tagged_with('Nature') + assert_equal Photo.find_tagged_with('Nature'), Photo.find_tagged_with(['Nature']) + assert_equal Photo.find_tagged_with('Nature'), Photo.find_tagged_with([tags(:nature)]) + + assert_equivalent [photos(:jonathan_bad_cat), photos(:jonathan_dog), photos(:jonathan_questioning_dog)], Photo.find_tagged_with('"Crazy animal" Bad') + assert_equal Photo.find_tagged_with('"Crazy animal" Bad'), Photo.find_tagged_with(['Crazy animal', 'Bad']) + assert_equal Photo.find_tagged_with('"Crazy animal" Bad'), Photo.find_tagged_with([tags(:animal), tags(:bad)]) + end + + def test_find_tagged_with_nothing + assert_equal [], Post.find_tagged_with("") + assert_equal [], Post.find_tagged_with([]) + end + + def test_find_tagged_with_nonexistant_tags + assert_equal [], Post.find_tagged_with('ABCDEFG') + assert_equal [], Photo.find_tagged_with(['HIJKLM']) + assert_equal [], Photo.find_tagged_with([Tag.new(:name => 'unsaved tag')]) + end + + def test_find_tagged_with_match_all + assert_equivalent [photos(:jonathan_dog)], Photo.find_tagged_with('Crazy animal, "Nature"', :match_all => true) + end + + def test_find_tagged_with_match_all_and_include + assert_equivalent [posts(:jonathan_sky), posts(:sam_flowers)], Post.find_tagged_with(['Very good', 'Nature'], :match_all => true, :include => :tags) + end + + def test_find_tagged_with_conditions + assert_equal [], Post.find_tagged_with('"Very good", Nature', :conditions => '1=0') + end + + def test_find_tagged_with_duplicates_options_hash + options = { :conditions => '1=1' }.freeze + assert_nothing_raised { Post.find_tagged_with("Nature", options) } + end + + def test_find_tagged_with_exclusions + assert_equivalent [photos(:jonathan_questioning_dog), photos(:jonathan_bad_cat)], Photo.find_tagged_with("Nature", :exclude => true) + assert_equivalent [posts(:jonathan_grass), posts(:jonathan_rain), posts(:jonathan_cloudy), posts(:jonathan_still_cloudy)], Post.find_tagged_with("'Very good', Bad", :exclude => true) + end + + def test_find_options_for_find_tagged_with_no_tags_returns_empty_hash + assert_equal Hash.new, Post.find_options_for_find_tagged_with("") + assert_equal Hash.new, Post.find_options_for_find_tagged_with([nil]) + end + + def test_find_options_for_find_tagged_with_leaves_arguments_unchanged + original_tags = photos(:jonathan_questioning_dog).tags.dup + Photo.find_options_for_find_tagged_with(photos(:jonathan_questioning_dog).tags) + assert_equal original_tags, photos(:jonathan_questioning_dog).tags + end + + def test_find_options_for_find_tagged_with_respects_custom_table_name + Tagging.table_name = "categorisations" + Tag.table_name = "categories" + + options = Photo.find_options_for_find_tagged_with("Hello") + + assert_no_match(/ taggings /, options[:joins]) + assert_no_match(/ tags /, options[:joins]) + + assert_match(/ categorisations /, options[:joins]) + assert_match(/ categories /, options[:joins]) + ensure + Tagging.table_name = "taggings" + Tag.table_name = "tags" + end + + def test_include_tags_on_find_tagged_with + assert_nothing_raised do + Photo.find_tagged_with('Nature', :include => :tags) + Photo.find_tagged_with("Nature", :include => { :taggings => :tag }) + end + end + + def test_basic_tag_counts_on_class + assert_tag_counts Post.tag_counts, :good => 2, :nature => 7, :question => 1, :bad => 1 + assert_tag_counts Photo.tag_counts, :good => 1, :nature => 3, :question => 1, :bad => 1, :animal => 3 + end + + def test_tag_counts_on_class_with_date_conditions + assert_tag_counts Post.tag_counts(:start_at => Date.new(2006, 8, 4)), :good => 1, :nature => 5, :question => 1, :bad => 1 + assert_tag_counts Post.tag_counts(:end_at => Date.new(2006, 8, 6)), :good => 1, :nature => 4, :question => 1 + assert_tag_counts Post.tag_counts(:start_at => Date.new(2006, 8, 5), :end_at => Date.new(2006, 8, 10)), :good => 1, :nature => 4, :bad => 1 + + assert_tag_counts Photo.tag_counts(:start_at => Date.new(2006, 8, 12), :end_at => Date.new(2006, 8, 19)), :good => 1, :nature => 2, :bad => 1, :question => 1, :animal => 3 + end + + def test_tag_counts_on_class_with_frequencies + assert_tag_counts Photo.tag_counts(:at_least => 2), :nature => 3, :animal => 3 + assert_tag_counts Photo.tag_counts(:at_most => 2), :good => 1, :question => 1, :bad => 1 + end + + def test_tag_counts_on_class_with_frequencies_and_conditions + assert_tag_counts Photo.tag_counts(:at_least => 2, :conditions => '1=1'), :nature => 3, :animal => 3 + end + + def test_tag_counts_duplicates_options_hash + options = { :at_least => 2, :conditions => '1=1' }.freeze + assert_nothing_raised { Photo.tag_counts(options) } + end + + def test_tag_counts_with_limit + assert_equal 2, Photo.tag_counts(:limit => 2).size + assert_equal 1, Post.tag_counts(:at_least => 4, :limit => 2).size + end + + def test_tag_counts_with_limit_and_order + assert_equal [tags(:nature), tags(:good)], Post.tag_counts(:order => 'count desc', :limit => 2) + end + + def test_tag_counts_on_association + assert_tag_counts users(:jonathan).posts.tag_counts, :good => 1, :nature => 5, :question => 1 + assert_tag_counts users(:sam).posts.tag_counts, :good => 1, :nature => 2, :bad => 1 + + assert_tag_counts users(:jonathan).photos.tag_counts, :animal => 3, :nature => 1, :question => 1, :bad => 1 + assert_tag_counts users(:sam).photos.tag_counts, :nature => 2, :good => 1 + end + + def test_tag_counts_on_association_with_options + assert_equal [], users(:jonathan).posts.tag_counts(:conditions => '1=0') + assert_tag_counts users(:jonathan).posts.tag_counts(:at_most => 2), :good => 1, :question => 1 + end + + def test_tag_counts_on_has_many_through + assert_tag_counts users(:jonathan).magazines.tag_counts, :good => 1 + end + + def test_tag_counts_respects_custom_table_names + Tagging.table_name = "categorisations" + Tag.table_name = "categories" + + options = Photo.find_options_for_tag_counts(:start_at => 2.weeks.ago, :end_at => Date.today) + sql = options.values.join(' ') + + assert_no_match /taggings/, sql + assert_no_match /tags/, sql + + assert_match /categorisations/, sql + assert_match /categories/, sql + ensure + Tagging.table_name = "taggings" + Tag.table_name = "tags" + end + + def test_tag_list_reader + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list + assert_equivalent ["Bad", "Crazy animal"], photos(:jonathan_bad_cat).tag_list + end + + def test_reassign_tag_list + assert_equivalent ["Nature", "Question"], posts(:jonathan_rain).tag_list + posts(:jonathan_rain).taggings.reload + + # Only an update of the posts table should be executed + assert_queries 1 do + posts(:jonathan_rain).update_attributes!(:tag_list => posts(:jonathan_rain).tag_list.to_s) + end + + assert_equivalent ["Nature", "Question"], posts(:jonathan_rain).tag_list + end + + def test_new_tags + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list + posts(:jonathan_sky).update_attributes!(:tag_list => "#{posts(:jonathan_sky).tag_list}, One, Two") + assert_equivalent ["Very good", "Nature", "One", "Two"], posts(:jonathan_sky).tag_list + end + + def test_remove_tag + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list + posts(:jonathan_sky).update_attributes!(:tag_list => "Nature") + assert_equivalent ["Nature"], posts(:jonathan_sky).tag_list + end + + def test_change_case_of_tags + original_tag_names = photos(:jonathan_questioning_dog).tag_list + photos(:jonathan_questioning_dog).update_attributes!(:tag_list => photos(:jonathan_questioning_dog).tag_list.to_s.upcase) + + # The new tag list is not uppercase becuase the AR finders are not case-sensitive + # and find the old tags when re-tagging with the uppercase tags. + assert_equivalent original_tag_names, photos(:jonathan_questioning_dog).reload.tag_list + end + + def test_remove_and_add_tag + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list + posts(:jonathan_sky).update_attributes!(:tag_list => "Nature, Beautiful") + assert_equivalent ["Nature", "Beautiful"], posts(:jonathan_sky).tag_list + end + + def test_tags_not_saved_if_validation_fails + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list + assert !posts(:jonathan_sky).update_attributes(:tag_list => "One, Two", :text => "") + assert_equivalent ["Very good", "Nature"], Post.find(posts(:jonathan_sky).id).tag_list + end + + def test_tag_list_accessors_on_new_record + p = Post.new(:text => 'Test') + + assert p.tag_list.blank? + p.tag_list = "One, Two" + assert_equal "One, Two", p.tag_list.to_s + end + + def test_clear_tag_list_with_nil + p = photos(:jonathan_questioning_dog) + + assert !p.tag_list.blank? + assert p.update_attributes(:tag_list => nil) + assert p.tag_list.blank? + + assert p.reload.tag_list.blank? + end + + def test_clear_tag_list_with_string + p = photos(:jonathan_questioning_dog) + + assert !p.tag_list.blank? + assert p.update_attributes(:tag_list => ' ') + assert p.tag_list.blank? + + assert p.reload.tag_list.blank? + end + + def test_tag_list_reset_on_reload + p = photos(:jonathan_questioning_dog) + assert !p.tag_list.blank? + p.tag_list = nil + assert p.tag_list.blank? + assert !p.reload.tag_list.blank? + end + + def test_instance_tag_counts + assert_tag_counts posts(:jonathan_sky).tag_counts, :good => 2, :nature => 7 + end + + def test_tag_list_populated_when_cache_nil + assert_nil posts(:jonathan_sky).cached_tag_list + posts(:jonathan_sky).save! + assert_equal posts(:jonathan_sky).tag_list.to_s, posts(:jonathan_sky).cached_tag_list + end + + def test_cached_tag_list_used + posts(:jonathan_sky).save! + posts(:jonathan_sky).reload + + assert_no_queries do + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list + end + end + + def test_cached_tag_list_not_used + # Load fixture and column information + posts(:jonathan_sky).taggings(:reload) + + assert_queries 1 do + # Tags association will be loaded + posts(:jonathan_sky).tag_list + end + end + + def test_cached_tag_list_updated + assert_nil posts(:jonathan_sky).cached_tag_list + posts(:jonathan_sky).save! + assert_equivalent ["Very good", "Nature"], TagList.from(posts(:jonathan_sky).cached_tag_list) + posts(:jonathan_sky).update_attributes!(:tag_list => "None") + + assert_equal 'None', posts(:jonathan_sky).cached_tag_list + assert_equal 'None', posts(:jonathan_sky).reload.cached_tag_list + end + + def test_clearing_cached_tag_list + # Generate the cached tag list + posts(:jonathan_sky).save! + + posts(:jonathan_sky).update_attributes!(:tag_list => "") + assert_equal "", posts(:jonathan_sky).cached_tag_list + end + + def test_find_tagged_with_using_sti + special_post = SpecialPost.create!(:text => "Test", :tag_list => "Random") + + assert_equal [special_post], SpecialPost.find_tagged_with("Random") + assert Post.find_tagged_with("Random").include?(special_post) + end + + def test_tag_counts_using_sti + SpecialPost.create!(:text => "Test", :tag_list => "Nature") + + assert_tag_counts SpecialPost.tag_counts, :nature => 1 + end + + def test_case_insensitivity + assert_difference "Tag.count", 1 do + Post.create!(:text => "Test", :tag_list => "one") + Post.create!(:text => "Test", :tag_list => "One") + end + + assert_equal Post.find_tagged_with("Nature"), Post.find_tagged_with("nature") + end + + def test_tag_not_destroyed_when_unused + posts(:jonathan_sky).tag_list.add("Random") + posts(:jonathan_sky).save! + + assert_no_difference 'Tag.count' do + posts(:jonathan_sky).tag_list.remove("Random") + posts(:jonathan_sky).save! + end + end + + def test_tag_destroyed_when_unused + Tag.destroy_unused = true + + posts(:jonathan_sky).tag_list.add("Random") + posts(:jonathan_sky).save! + + assert_difference 'Tag.count', -1 do + posts(:jonathan_sky).tag_list.remove("Random") + posts(:jonathan_sky).save! + end + ensure + Tag.destroy_unused = false + end +end + +class ActsAsTaggableOnSteroidsFormTest < Test::Unit::TestCase + fixtures :tags, :taggings, :posts, :users, :photos + + include ActionView::Helpers::FormHelper + + def test_tag_list_contents + fields_for :post, posts(:jonathan_sky) do |f| + assert_match /Very good, Nature/, f.text_field(:tag_list) + end + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/magazine.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/magazine.rb new file mode 100644 index 0000000..554afe4 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/magazine.rb @@ -0,0 +1,3 @@ +class Magazine < ActiveRecord::Base + acts_as_taggable +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/magazines.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/magazines.yml new file mode 100644 index 0000000..044ce6d --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/magazines.yml @@ -0,0 +1,7 @@ +ruby: + id: 1 + name: Ruby + +rails: + id: 2 + name: Rails \ No newline at end of file diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photo.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photo.rb new file mode 100644 index 0000000..224957f --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photo.rb @@ -0,0 +1,8 @@ +class Photo < ActiveRecord::Base + acts_as_taggable + + belongs_to :user +end + +class SpecialPhoto < Photo +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photos.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photos.yml new file mode 100644 index 0000000..25a4118 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photos.yml @@ -0,0 +1,24 @@ +jonathan_dog: + id: 1 + user_id: 1 + title: A small dog + +jonathan_questioning_dog: + id: 2 + user_id: 1 + title: What does this dog want? + +jonathan_bad_cat: + id: 3 + user_id: 1 + title: Bad cat + +sam_flower: + id: 4 + user_id: 2 + title: Flower + +sam_sky: + id: 5 + user_id: 2 + title: Sky diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/post.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/post.rb new file mode 100644 index 0000000..bee100a --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/post.rb @@ -0,0 +1,7 @@ +class Post < ActiveRecord::Base + acts_as_taggable + + belongs_to :user + + validates_presence_of :text +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/posts.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/posts.yml new file mode 100644 index 0000000..79f50f5 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/posts.yml @@ -0,0 +1,34 @@ +jonathan_sky: + id: 1 + user_id: 1 + text: The sky is particularly blue today + +jonathan_grass: + id: 2 + user_id: 1 + text: The grass seems very green + +jonathan_rain: + id: 3 + user_id: 1 + text: Why does the rain fall? + +jonathan_cloudy: + id: 4 + user_id: 1 + text: Is it cloudy? + +jonathan_still_cloudy: + id: 5 + user_id: 1 + text: Is it still cloudy? + +sam_ground: + id: 6 + user_id: 2 + text: The ground is looking too brown + +sam_flowers: + id: 7 + user_id: 2 + text: Why are the flowers dead? \ No newline at end of file diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/special_post.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/special_post.rb new file mode 100644 index 0000000..366a0d5 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/special_post.rb @@ -0,0 +1,2 @@ +class SpecialPost < Post +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/subscription.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/subscription.rb new file mode 100644 index 0000000..e975cb7 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/subscription.rb @@ -0,0 +1,4 @@ +class Subscription < ActiveRecord::Base + belongs_to :user + belongs_to :magazine +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/subscriptions.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/subscriptions.yml new file mode 100644 index 0000000..1b5e68a --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/subscriptions.yml @@ -0,0 +1,3 @@ +jonathan_rails: + user_id: 1 + magazine_id: 1 \ No newline at end of file diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/taggings.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/taggings.yml new file mode 100644 index 0000000..01e599b --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/taggings.yml @@ -0,0 +1,149 @@ +# Posts +jonathan_sky_good: + id: 1 + tag_id: 1 + taggable_id: 1 + taggable_type: Post + created_at: 2006-08-01 + +jonathan_sky_nature: + id: 2 + tag_id: 3 + taggable_id: 1 + taggable_type: Post + created_at: 2006-08-02 + +jonathan_grass_nature: + id: 3 + tag_id: 3 + taggable_id: 2 + taggable_type: Post + created_at: 2006-08-03 + +jonathan_rain_question: + id: 4 + tag_id: 4 + taggable_id: 3 + taggable_type: Post + created_at: 2006-08-04 + +jonathan_rain_nature: + id: 5 + tag_id: 3 + taggable_id: 3 + taggable_type: Post + created_at: 2006-08-05 + +jonathan_cloudy_nature: + id: 6 + tag_id: 3 + taggable_id: 4 + taggable_type: Post + created_at: 2006-08-06 + +jonathan_still_cloudy_nature: + id: 7 + tag_id: 3 + taggable_id: 5 + taggable_type: Post + created_at: 2006-08-07 + +sam_ground_nature: + id: 8 + tag_id: 3 + taggable_id: 6 + taggable_type: Post + created_at: 2006-08-08 + +sam_ground_bad: + id: 9 + tag_id: 2 + taggable_id: 6 + taggable_type: Post + created_at: 2006-08-09 + +sam_flowers_good: + id: 10 + tag_id: 1 + taggable_id: 7 + taggable_type: Post + created_at: 2006-08-10 + +sam_flowers_nature: + id: 11 + tag_id: 3 + taggable_id: 7 + taggable_type: Post + created_at: 2006-08-11 + +# Photos +jonathan_dog_animal: + id: 12 + tag_id: 5 + taggable_id: 1 + taggable_type: Photo + created_at: 2006-08-12 + +jonathan_dog_nature: + id: 13 + tag_id: 3 + taggable_id: 1 + taggable_type: Photo + created_at: 2006-08-13 + +jonathan_questioning_dog_animal: + id: 14 + tag_id: 5 + taggable_id: 2 + taggable_type: Photo + created_at: 2006-08-14 + +jonathan_questioning_dog_question: + id: 15 + tag_id: 4 + taggable_id: 2 + taggable_type: Photo + created_at: 2006-08-15 + +jonathan_bad_cat_bad: + id: 16 + tag_id: 2 + taggable_id: 3 + taggable_type: Photo + created_at: 2006-08-16 + +jonathan_bad_cat_animal: + id: 17 + tag_id: 5 + taggable_id: 3 + taggable_type: Photo + created_at: 2006-08-17 + +sam_flower_nature: + id: 18 + tag_id: 3 + taggable_id: 4 + taggable_type: Photo + created_at: 2006-08-18 + +sam_flower_good: + id: 19 + tag_id: 1 + taggable_id: 4 + taggable_type: Photo + created_at: 2006-08-19 + +sam_sky_nature: + id: 20 + tag_id: 3 + taggable_id: 5 + taggable_type: Photo + created_at: 2006-08-20 + +# Magazines +ruby_good: + id: 50 + tag_id: 1 + taggable_id: 1 + taggable_type: Magazine + created_at: 2007-08-25 diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/tags.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/tags.yml new file mode 100644 index 0000000..b8f8367 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/tags.yml @@ -0,0 +1,19 @@ +good: + id: 1 + name: Very good + +bad: + id: 2 + name: Bad + +nature: + id: 3 + name: Nature + +question: + id: 4 + name: Question + +animal: + id: 5 + name: Crazy animal \ No newline at end of file diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/user.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/user.rb new file mode 100644 index 0000000..8c0f787 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/user.rb @@ -0,0 +1,7 @@ +class User < ActiveRecord::Base + has_many :posts + has_many :photos + + has_many :subscriptions + has_many :magazines, :through => :subscriptions +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/users.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/users.yml new file mode 100644 index 0000000..da94fea --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/users.yml @@ -0,0 +1,7 @@ +jonathan: + id: 1 + name: Jonathan + +sam: + id: 2 + name: Sam diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/tag_list_test.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/tag_list_test.rb new file mode 100644 index 0000000..d0d135c --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/tag_list_test.rb @@ -0,0 +1,106 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +class TagListTest < Test::Unit::TestCase + def test_from_leaves_string_unchanged + tags = '"One ", Two' + original = tags.dup + TagList.from(tags) + assert_equal tags, original + end + + def test_from_single_name + assert_equal %w(Fun), TagList.from("Fun") + assert_equal %w(Fun), TagList.from('"Fun"') + end + + def test_from_blank + assert_equal [], TagList.from(nil) + assert_equal [], TagList.from("") + end + + def test_from_single_quoted_tag + assert_equal ['with, comma'], TagList.from('"with, comma"') + end + + def test_spaces_do_not_delineate + assert_equal ['A B', 'C'], TagList.from('A B, C') + end + + def test_from_multiple_tags + assert_equivalent %w(Alpha Beta Delta Gamma), TagList.from("Alpha, Beta, Delta, Gamma") + end + + def test_from_multiple_tags_with_quotes + assert_equivalent %w(Alpha Beta Delta Gamma), TagList.from('Alpha, "Beta", Gamma , "Delta"') + end + + def test_from_with_single_quotes + assert_equivalent ['A B', 'C'], TagList.from("'A B', C") + end + + def test_from_multiple_tags_with_quote_and_commas + assert_equivalent ['Alpha, Beta', 'Delta', 'Gamma, something'], TagList.from('"Alpha, Beta", Delta, "Gamma, something"') + end + + def test_from_removes_white_space + assert_equivalent %w(Alpha Beta), TagList.from('" Alpha ", "Beta "') + assert_equivalent %w(Alpha Beta), TagList.from(' Alpha, Beta ') + end + + def test_from_removes_dots + assert_equivalent %w(Alpha Beta), TagList.from('Alpha., Beta') + assert_equivalent %w(Alpha Beta), TagList.from('Alpha, Be.ta') + end + + def test_alternative_delimiter + TagList.delimiter = " " + + assert_equal %w(One Two), TagList.from("One Two") + assert_equal ['One two', 'three', 'four'], TagList.from('"One two" three four') + ensure + TagList.delimiter = "," + end + + def test_duplicate_tags_removed + assert_equal %w(One), TagList.from("One, One") + end + + def test_to_s_with_commas + assert_equal "Question, Crazy Animal", TagList.new("Question", "Crazy Animal").to_s + end + + def test_to_s_with_alternative_delimiter + TagList.delimiter = " " + + assert_equal '"Crazy Animal" Question', TagList.new("Crazy Animal", "Question").to_s + ensure + TagList.delimiter = "," + end + + def test_add + tag_list = TagList.new("One") + assert_equal %w(One), tag_list + + assert_equal %w(One Two), tag_list.add("Two") + assert_equal %w(One Two Three), tag_list.add(["Three"]) + end + + def test_remove + tag_list = TagList.new("One", "Two") + assert_equal %w(Two), tag_list.remove("One") + assert_equal %w(), tag_list.remove(["Two"]) + end + + def test_new_with_parsing + assert_equal %w(One Two), TagList.new("One, Two", :parse => true) + end + + def test_add_with_parsing + assert_equal %w(One Two), TagList.new.add("One, Two", :parse => true) + end + + def test_remove_with_parsing + tag_list = TagList.from("Three, Four, Five") + assert_equal %w(Four), tag_list.remove("Three, Five", :parse => true) + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/tag_test.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/tag_test.rb new file mode 100644 index 0000000..4ca577d --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/tag_test.rb @@ -0,0 +1,34 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +class TagTest < Test::Unit::TestCase + fixtures :tags, :taggings, :users, :photos, :posts + + def test_name_required + t = Tag.create + assert_match /blank/, t.errors[:name].to_s + end + + def test_name_unique + t = Tag.create!(:name => "My tag") + duplicate = t.clone + + assert !duplicate.save + assert_match /taken/, duplicate.errors[:name].to_s + end + + def test_taggings + assert_equivalent [taggings(:jonathan_sky_good), taggings(:sam_flowers_good), taggings(:sam_flower_good), taggings(:ruby_good)], tags(:good).taggings + assert_equivalent [taggings(:sam_ground_bad), taggings(:jonathan_bad_cat_bad)], tags(:bad).taggings + end + + def test_to_s + assert_equal tags(:good).name, tags(:good).to_s + end + + def test_equality + assert_equal tags(:good), tags(:good) + assert_equal Tag.find(1), Tag.find(1) + assert_equal Tag.new(:name => 'A'), Tag.new(:name => 'A') + assert_not_equal Tag.new(:name => 'A'), Tag.new(:name => 'B') + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/tagging_test.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/tagging_test.rb new file mode 100644 index 0000000..172b8e2 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/tagging_test.rb @@ -0,0 +1,13 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +class TaggingTest < Test::Unit::TestCase + fixtures :tags, :taggings, :posts + + def test_tag + assert_equal tags(:good), taggings(:jonathan_sky_good).tag + end + + def test_taggable + assert_equal posts(:jonathan_sky), taggings(:jonathan_sky_good).taggable + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/tags_helper_test.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/tags_helper_test.rb new file mode 100644 index 0000000..9c3ce53 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/tags_helper_test.rb @@ -0,0 +1,28 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +class TagsHelperTest < Test::Unit::TestCase + fixtures :tags, :taggings, :posts + + include TagsHelper + + def test_tag_cloud + cloud_elements = [] + + tag_cloud Post.tag_counts, %w(css1 css2 css3 css4) do |tag, css_class| + cloud_elements << [tag, css_class] + end + + assert_equal [ + [tags(:good), "css2"], + [tags(:bad), "css1"], + [tags(:nature), "css4"], + [tags(:question), "css1"] + ], cloud_elements + end + + def test_tag_cloud_when_no_tags + tag_cloud SpecialPost.tag_counts, %w(css1) do + assert false, "tag_cloud should not yield" + end + end +end -- cgit v1.3