This repository has been archived by the owner on Dec 21, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 1e813ae
Showing
23 changed files
with
2,492 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
= acts_as_taggable_on_steroids | ||
|
||
This plugin is based on acts_as_taggable by DHH but includes extras | ||
such as tests, smarter tag assignment, and tag cloud calculations. | ||
|
||
Thanks to www.fanacious.com for allowing this plugin to be released. Please check out | ||
their site to show your support. | ||
|
||
== Resources | ||
|
||
Install | ||
* script/plugin install http://svn.viney.net.nz/things/rails/plugins/acts_as_taggable_on_steroids | ||
|
||
== Usage | ||
|
||
=== Basic tagging | ||
|
||
Using the examples from the tests, let's suppose we have users that have many posts and we want those | ||
posts to be able to be tagged by the user. | ||
|
||
As usual, we 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, <tt>tag_list</tt> and <tt>tag_list=</tt>. Both these | ||
methods work like regular attribute accessors. | ||
|
||
p = Post.find(:first) | ||
p.tag_list # "" | ||
p.tag_list = "Funny, Silly" | ||
p.save | ||
p.reload.tag_list # "Funny, Silly" | ||
|
||
=== Tag cloud calculations | ||
|
||
To construct tag clouds, the frequency of each tag needs to be calculated. | ||
Because we specified +acts_as_taggable+ on the <tt>Post</tt> class, we can | ||
get a calculation of all the tag counts by using <tt>Post.tag_counts</tt>. But what if we wanted a tag count for | ||
an individual user's posts? To achieve this we extend the <tt>:posts</tt> association like so: | ||
|
||
class User < ActiveRecord::Base | ||
has_many :posts, :extend => TagCountsExtension | ||
end | ||
|
||
This extension now allows us to do the following: | ||
|
||
User.find(:first).posts.tag_counts | ||
|
||
This can be used to construct a tag cloud for the posts of an individual user. The +TagCountsExtension+ should | ||
only be used on associations that have +acts_as_taggable+ defined. | ||
|
||
Problems, comments, and suggestions all welcome. jonathan.viney@gmail.com | ||
|
||
== Credits | ||
|
||
www.fanacious.com |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
require File.dirname(__FILE__) + '/lib/acts_as_taggable' | ||
|
||
require File.dirname(__FILE__) + '/lib/tagging' | ||
require File.dirname(__FILE__) + '/lib/tag' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
module ActiveRecord | ||
module Acts #:nodoc: | ||
module Taggable #:nodoc: | ||
def self.included(base) | ||
base.extend(ClassMethods) | ||
end | ||
|
||
module ClassMethods | ||
def acts_as_taggable(options = {}) | ||
has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag | ||
has_many :tags, :through => :taggings | ||
|
||
after_save :save_tags | ||
|
||
include ActiveRecord::Acts::Taggable::InstanceMethods | ||
extend ActiveRecord::Acts::Taggable::SingletonMethods | ||
|
||
alias_method :reload_without_tag_list, :reload | ||
alias_method :reload, :reload_with_tag_list | ||
end | ||
end | ||
|
||
module SingletonMethods | ||
# Pass either a tag string, or an array of strings or tags | ||
def find_tagged_with(tags, options = {}) | ||
tags = Tag.parse(tags) if tags.is_a?(String) | ||
return [] if tags.empty? | ||
tags.map!(&:to_s) | ||
|
||
conditions = sanitize_sql(['tags.name in (?)', tags]) | ||
conditions << "and #{sanitize_sql(options.delete(:conditions))}" if options[:conditions] | ||
|
||
find(:all, { :select => "DISTINCT #{table_name}.*", | ||
:joins => "left outer join taggings on taggings.taggable_id = #{table_name}.#{primary_key} and taggings.taggable_type = '#{name}' " + | ||
"left outer join tags on tags.id = taggings.tag_id", | ||
:conditions => conditions }.merge(options)) | ||
end | ||
|
||
# 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 then the given value | ||
def tag_counts(options = {}) | ||
options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit | ||
|
||
start_at = sanitize_sql(['taggings.created_at >= ?', options[:start_at]]) if options[:start_at] | ||
end_at = sanitize_sql(['taggings.created_at <= ?', options[:end_at]]) if options[:end_at] | ||
options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions] | ||
|
||
conditions = [options[:conditions], start_at, end_at].compact.join(' and ') | ||
|
||
at_least = sanitize_sql(['count >= ?', options[:at_least]]) if options[:at_least] | ||
at_most = sanitize_sql(['count <= ?', options[:at_most]]) if options[:at_most] | ||
having = [at_least, at_most].compact.join(' and ') | ||
|
||
order = "order by #{options[:order]}" if options[:order] | ||
limit = sanitize_sql(['limit ?', options[:limit]]) if options[:limit] | ||
|
||
Tag.find_by_sql <<-END | ||
select tags.id, tags.name, count(*) as count | ||
from tags left outer join taggings on tags.id = taggings.tag_id | ||
left outer join #{table_name} on #{table_name}.id = taggings.taggable_id | ||
where taggings.taggable_type = "#{name}" | ||
#{"and #{conditions}" unless conditions.blank?} | ||
group by tags.id | ||
having count(*) > 0 #{"and #{having}" unless having.blank?} | ||
#{order} | ||
#{limit} | ||
END | ||
end | ||
end | ||
|
||
module InstanceMethods | ||
attr_writer :tag_list | ||
|
||
def tag_list | ||
defined?(@tag_list) ? @tag_list : read_tags | ||
end | ||
|
||
def save_tags | ||
if defined?(@tag_list) | ||
write_tags(@tag_list) | ||
remove_tag_list | ||
end | ||
end | ||
|
||
def write_tags(list) | ||
new_tag_names = Tag.parse(list) | ||
old_tagging_ids = [] | ||
|
||
Tag.transaction do | ||
taggings.each do |tagging| | ||
index = new_tag_names.index(tagging.tag.name) | ||
index ? new_tag_names.delete_at(index) : old_tagging_ids << tagging.id | ||
end | ||
|
||
Tagging.delete_all(['id in (?)', old_tagging_ids]) unless old_tagging_ids.empty? | ||
|
||
# Create any new tags/taggings | ||
new_tag_names.each do |name| | ||
Tag.find_or_create_by_name(name).tag(self) | ||
end | ||
|
||
taggings.reset | ||
tags.reset | ||
end | ||
true | ||
end | ||
|
||
def read_tags | ||
tags.collect do |tag| | ||
tag.name.include?(',') ? "\"#{tag.name}\"" : tag.name | ||
end.join(', ') | ||
end | ||
|
||
def reload_with_tag_list(*args) | ||
remove_tag_list | ||
reload_without_tag_list(*args) | ||
end | ||
|
||
private | ||
def remove_tag_list | ||
remove_instance_variable(:@tag_list) if defined?(@tag_list) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
|
||
ActiveRecord::Base.send(:include, ActiveRecord::Acts::Taggable) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
class Tag < ActiveRecord::Base | ||
has_many :taggings | ||
|
||
def self.parse(list) | ||
tags = [] | ||
|
||
return tags if list.blank? | ||
list = list.dup | ||
|
||
# Parse the quoted tags | ||
list.gsub!(/"(.*?)"\s*,?\s*/) { tags << $1; "" } | ||
|
||
# Strip whitespace and remove blank tags | ||
(tags + list.split(',')).map!(&:strip).delete_if(&:blank?) | ||
end | ||
|
||
# A list of all the objects tagged with this tag | ||
def tagged | ||
taggings.collect(&:taggable) | ||
end | ||
|
||
# Tag a taggable with this tag | ||
def tag(taggable) | ||
Tagging.create :tag_id => id, :taggable => taggable | ||
taggings.reset | ||
end | ||
|
||
def ==(object) | ||
super || (object.is_a?(Tag) && name == object.name) | ||
end | ||
|
||
def to_s | ||
name | ||
end | ||
|
||
def count | ||
@attributes['count'].to_i if @attributes.include?('count') | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
module TagCountsExtension | ||
# Options will be passed to the tag_counts method on the association's class | ||
def tag_counts(options = {}) | ||
load_target | ||
return [] if target.blank? | ||
|
||
key_condition = "#{@reflection.table_name}.#{@reflection.primary_key_name} = #{target.first.send(@reflection.primary_key_name)}" | ||
|
||
options[:conditions] = if options[:conditions] | ||
sanitize_sql(options[:conditions]) + " and #{key_condition}" | ||
else | ||
key_condition | ||
end | ||
|
||
@reflection.klass.tag_counts(options) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
class Tagging < ActiveRecord::Base | ||
belongs_to :tag | ||
belongs_to :taggable, :polymorphic => true | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
require 'test/unit' | ||
|
||
begin | ||
require File.dirname(__FILE__) + '/../../../../config/boot' | ||
require 'active_record' | ||
rescue LoadError | ||
require 'rubygems' | ||
require_gem 'activerecord' | ||
end | ||
|
||
require 'active_record/fixtures' | ||
|
||
require File.dirname(__FILE__) + '/../lib/acts_as_taggable' | ||
require File.dirname(__FILE__) + '/../lib/tag' | ||
require File.dirname(__FILE__) + '/../lib/tagging' | ||
require File.dirname(__FILE__) + '/../lib/tag_counts_extension' | ||
|
||
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) | ||
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + '/debug.log') | ||
ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'mysql']) | ||
|
||
load(File.dirname(__FILE__) + '/schema.rb') | ||
|
||
Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + '/fixtures/' | ||
$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path) | ||
|
||
class Test::Unit::TestCase #:nodoc: | ||
def create_fixtures(*table_names) | ||
if block_given? | ||
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield } | ||
else | ||
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) | ||
end | ||
end | ||
|
||
# Turn off transactional fixtures if you're working with MyISAM tables in MySQL | ||
self.use_transactional_fixtures = true | ||
|
||
# Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david) | ||
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" if value.nil? | ||
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 | ||
end |
Oops, something went wrong.