bdimcheff-is_taggable 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +62 -0
- data/VERSION.yml +4 -0
- data/generators/is_taggable_migration/is_taggable_migration_generator.rb +7 -0
- data/generators/is_taggable_migration/templates/migration.rb +24 -0
- data/lib/is_taggable.rb +103 -0
- data/lib/tag.rb +15 -0
- data/lib/tagging.rb +4 -0
- data/test/is_taggable_test.rb +90 -0
- data/test/tag_test.rb +35 -0
- data/test/tagging_test.rb +38 -0
- data/test/test_helper.rb +48 -0
- metadata +68 -0
data/README.rdoc
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
= is_taggable
|
2
|
+
|
3
|
+
At last, a short and sweet tagging implementation that you can easily modify and extend.
|
4
|
+
|
5
|
+
Most of the plugins out there are on steroids or messing directly with SQL, a known gateway drug.
|
6
|
+
|
7
|
+
We wanted a more sober plugin that would handle new functionality without breaking a sweat. Some plugins had minimal or no tests *gasp*. They were so messed up that operating would likely cause internal bleeding.
|
8
|
+
|
9
|
+
So, we crafted the plugin we needed with the functionality we were looking for: tag kinds. It's small and healthy. So, it should be a good base to build on.
|
10
|
+
|
11
|
+
== Usage
|
12
|
+
|
13
|
+
After generating the migration:
|
14
|
+
|
15
|
+
$ script/generate is_taggable_migration
|
16
|
+
$ rake db:migrate
|
17
|
+
|
18
|
+
All you need is the 'is_taggable' declaration in your models:
|
19
|
+
|
20
|
+
class User < ActiveRecord::Base
|
21
|
+
is_taggable :tags, :languages
|
22
|
+
end
|
23
|
+
|
24
|
+
In your forms, add a text fields for "tag_list" and/or "language_list" (matching the example model above):
|
25
|
+
|
26
|
+
<%= f.text_field :tag_list %>
|
27
|
+
|
28
|
+
Calling is_taggable with any arguments defaults to a tag_list. Instantiating our polyglot user is easy:
|
29
|
+
|
30
|
+
User.new :tag_list => "rails, giraffesoft", :language_list => "english, french, spanish, latin, esperanto, tlhIngan Hol"
|
31
|
+
|
32
|
+
A comma is the default tag separator, but this can be easily changed:
|
33
|
+
|
34
|
+
IsTaggable::TagList.delimiter = " "
|
35
|
+
|
36
|
+
You can also set options on the tags. The only option currently supported is :fixed. If you set :fixed => true, tags will not be added to a taggable model unless you explicitly create a Tag record. This is for tagging with fixed vocabularies.
|
37
|
+
|
38
|
+
class User < ActiveRecord::Base
|
39
|
+
is_taggable :tags, :fixed => true
|
40
|
+
end
|
41
|
+
|
42
|
+
== Get it
|
43
|
+
|
44
|
+
$ sudo gem install bdimcheff-is_taggable -s http://gems.github.com
|
45
|
+
|
46
|
+
As a rails gem dependency:
|
47
|
+
|
48
|
+
config.gem 'bdimcheff-is_taggable', :lib => 'is_taggable'
|
49
|
+
|
50
|
+
Or get the source from github:
|
51
|
+
|
52
|
+
$ git clone git://github.com/bdimcheff/is_taggable.git
|
53
|
+
|
54
|
+
(or fork it at http://github.com/bdimcheff/is_taggable)
|
55
|
+
|
56
|
+
== Credits
|
57
|
+
|
58
|
+
is_taggable was created, and is maintained by Daniel Haran and James Golick. Brandon Dimcheff added fixed tag vocabulary support.
|
59
|
+
|
60
|
+
== License
|
61
|
+
|
62
|
+
is_taggable is available under the MIT License
|
data/VERSION.yml
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
class IsTaggableMigration < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :tags do |t|
|
4
|
+
t.string :name, :default => ''
|
5
|
+
t.string :kind, :default => ''
|
6
|
+
end
|
7
|
+
|
8
|
+
create_table :taggings do |t|
|
9
|
+
t.integer :tag_id
|
10
|
+
|
11
|
+
t.string :taggable_type, :default => ''
|
12
|
+
t.integer :taggable_id
|
13
|
+
end
|
14
|
+
|
15
|
+
add_index :tags, [:name, :kind]
|
16
|
+
add_index :taggings, :tag_id
|
17
|
+
add_index :taggings, [:taggable_id, :taggable_type]
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.down
|
21
|
+
drop_table :taggings
|
22
|
+
drop_table :tags
|
23
|
+
end
|
24
|
+
end
|
data/lib/is_taggable.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
path = File.expand_path(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH << path unless $LOAD_PATH.include?(path)
|
3
|
+
require 'tag'
|
4
|
+
require 'tagging'
|
5
|
+
|
6
|
+
module IsTaggable
|
7
|
+
class TagList < Array
|
8
|
+
cattr_accessor :delimiter
|
9
|
+
@@delimiter = ','
|
10
|
+
|
11
|
+
def initialize(list)
|
12
|
+
list = list.is_a?(Array) ? list : list.split(@@delimiter).collect(&:strip).reject(&:blank?)
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
join(', ')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ActiveRecordExtension
|
22
|
+
def is_taggable(*kinds)
|
23
|
+
class_inheritable_accessor :tag_kinds, :tag_options
|
24
|
+
default_options = { :fixed => false }
|
25
|
+
self.tag_options = if kinds[-1].is_a?(Hash)
|
26
|
+
default_options.merge(kinds.delete_at(-1))
|
27
|
+
else
|
28
|
+
default_options
|
29
|
+
end
|
30
|
+
self.tag_kinds = kinds.map(&:to_s).map(&:singularize)
|
31
|
+
self.tag_kinds << :tag if kinds.empty?
|
32
|
+
|
33
|
+
include IsTaggable::TaggableMethods
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module TaggableMethods
|
38
|
+
def self.included(klass)
|
39
|
+
klass.class_eval do
|
40
|
+
include IsTaggable::TaggableMethods::InstanceMethods
|
41
|
+
|
42
|
+
has_many :taggings, :as => :taggable, :dependent => :destroy
|
43
|
+
has_many :tags, :through => :taggings
|
44
|
+
after_save :save_tags
|
45
|
+
|
46
|
+
tag_kinds.each do |k|
|
47
|
+
define_method("#{k}_list") { get_tag_list(k) }
|
48
|
+
define_method("#{k}_list=") { |new_list| set_tag_list(k, new_list) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
module InstanceMethods
|
54
|
+
def set_tag_list(kind, list)
|
55
|
+
tag_list = TagList.new(list)
|
56
|
+
instance_variable_set(tag_list_name_for_kind(kind), tag_list)
|
57
|
+
end
|
58
|
+
|
59
|
+
def get_tag_list(kind)
|
60
|
+
set_tag_list(kind, tags.of_kind(kind).map(&:name)) if tag_list_instance_variable(kind).nil?
|
61
|
+
tag_list_instance_variable(kind)
|
62
|
+
end
|
63
|
+
|
64
|
+
protected
|
65
|
+
def tag_list_name_for_kind(kind)
|
66
|
+
"@#{kind}_list"
|
67
|
+
end
|
68
|
+
|
69
|
+
def tag_list_instance_variable(kind)
|
70
|
+
instance_variable_get(tag_list_name_for_kind(kind))
|
71
|
+
end
|
72
|
+
|
73
|
+
def save_tags
|
74
|
+
tag_kinds.each do |tag_kind|
|
75
|
+
delete_unused_tags(tag_kind)
|
76
|
+
add_new_tags(tag_kind)
|
77
|
+
set_tag_list(tag_kind, tags.of_kind(tag_kind).map(&:name))
|
78
|
+
end
|
79
|
+
|
80
|
+
taggings.each(&:save)
|
81
|
+
end
|
82
|
+
|
83
|
+
def delete_unused_tags(tag_kind)
|
84
|
+
tags.of_kind(tag_kind).each { |t| tags.delete(t) unless get_tag_list(tag_kind).include?(t.name) }
|
85
|
+
end
|
86
|
+
|
87
|
+
def add_new_tags(tag_kind)
|
88
|
+
tag_names = tags.of_kind(tag_kind).map(&:name)
|
89
|
+
get_tag_list(tag_kind).each do |tag_name|
|
90
|
+
if tag_options[:fixed]
|
91
|
+
tag = Tag.with_name_like_and_kind(tag_name, tag_kind) unless tag_names.include?(tag_name)
|
92
|
+
else
|
93
|
+
tag = Tag.find_or_initialize_with_name_like_and_kind(tag_name, tag_kind) unless tag_names.include?(tag_name)
|
94
|
+
end
|
95
|
+
|
96
|
+
tags << tag if tag
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
ActiveRecord::Base.send(:extend, IsTaggable::ActiveRecordExtension)
|
data/lib/tag.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
class Tag < ActiveRecord::Base
|
2
|
+
class << self
|
3
|
+
def find_or_initialize_with_name_like_and_kind(name, kind)
|
4
|
+
with_name_like_and_kind(name, kind).first || new(:name => name, :kind => kind)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
has_many :taggings, :dependent => :destroy
|
9
|
+
|
10
|
+
validates_presence_of :name
|
11
|
+
validates_uniqueness_of :name, :scope => :kind
|
12
|
+
|
13
|
+
named_scope :with_name_like_and_kind, lambda { |name, kind| { :conditions => ["name like ? AND kind = ?", name, kind] } }
|
14
|
+
named_scope :of_kind, lambda { |kind| { :conditions => {:kind => kind} } }
|
15
|
+
end
|
data/lib/tagging.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
Expectations do
|
4
|
+
expect Tag do
|
5
|
+
Post.new.tags.build
|
6
|
+
end
|
7
|
+
|
8
|
+
expect Tagging do
|
9
|
+
Post.new.taggings.build
|
10
|
+
end
|
11
|
+
|
12
|
+
expect ["is_taggable", "has 'tags' by default"] do
|
13
|
+
n = Comment.new :tag_list => "is_taggable, has 'tags' by default"
|
14
|
+
n.tag_list
|
15
|
+
end
|
16
|
+
|
17
|
+
expect ["one", "two"] do
|
18
|
+
IsTaggable::TagList.delimiter = " "
|
19
|
+
n = Comment.new :tag_list => "one two"
|
20
|
+
IsTaggable::TagList.delimiter = "," # puts things back to avoid breaking following tests
|
21
|
+
n.tag_list
|
22
|
+
end
|
23
|
+
|
24
|
+
expect ["something cool", "something else cool"] do
|
25
|
+
p = Post.new :tag_list => "something cool, something else cool"
|
26
|
+
p.tag_list
|
27
|
+
end
|
28
|
+
|
29
|
+
expect ["something cool", "something new"] do
|
30
|
+
p = Post.new :tag_list => "something cool, something else cool"
|
31
|
+
p.save!
|
32
|
+
p.tag_list = "something cool, something new"
|
33
|
+
p.save!
|
34
|
+
p.tags.reload
|
35
|
+
p.instance_variable_set("@tag_list", nil)
|
36
|
+
p.tag_list
|
37
|
+
end
|
38
|
+
|
39
|
+
expect ["english", "french"] do
|
40
|
+
p = Post.new :language_list => "english, french"
|
41
|
+
p.save!
|
42
|
+
p.tags.reload
|
43
|
+
p.instance_variable_set("@language_list", nil)
|
44
|
+
p.language_list
|
45
|
+
end
|
46
|
+
|
47
|
+
expect ["english", "french"] do
|
48
|
+
p = Post.new :language_list => "english, french"
|
49
|
+
p.language_list
|
50
|
+
end
|
51
|
+
|
52
|
+
expect "english, french" do
|
53
|
+
p = Post.new :language_list => "english, french"
|
54
|
+
p.language_list.to_s
|
55
|
+
end
|
56
|
+
|
57
|
+
# added - should clean up strings with arbitrary spaces around commas
|
58
|
+
expect ["spaces","should","not","matter"] do
|
59
|
+
p = Post.new
|
60
|
+
p.tag_list = "spaces,should, not,matter"
|
61
|
+
p.save!
|
62
|
+
p.tags.reload
|
63
|
+
p.tag_list
|
64
|
+
end
|
65
|
+
|
66
|
+
expect ["blank","topics","should be ignored"] do
|
67
|
+
p = Post.new
|
68
|
+
p.tag_list = "blank, topics, should be ignored, "
|
69
|
+
p.save!
|
70
|
+
p.tags.reload
|
71
|
+
p.tag_list
|
72
|
+
end
|
73
|
+
|
74
|
+
expect 2 do
|
75
|
+
p = Post.new :language_list => "english, french"
|
76
|
+
p.save!
|
77
|
+
p.tags.length
|
78
|
+
end
|
79
|
+
|
80
|
+
expect ["foo", "bar"] do
|
81
|
+
Tag.create(:name => 'foo', :kind => 'category')
|
82
|
+
Tag.create(:name => 'bar', :kind => 'category')
|
83
|
+
|
84
|
+
p = Page.new
|
85
|
+
p.category_list = "foo, bar, baz"
|
86
|
+
p.save!
|
87
|
+
p.tags.reload
|
88
|
+
p.category_list
|
89
|
+
end
|
90
|
+
end
|
data/test/tag_test.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
Expectations do
|
4
|
+
expect Tagging do
|
5
|
+
Tag.new.taggings.proxy_reflection.klass
|
6
|
+
end
|
7
|
+
|
8
|
+
expect Tag.new(:name => "duplicate").not.to.be.valid? do
|
9
|
+
Tag.create!(:name => "duplicate")
|
10
|
+
end
|
11
|
+
|
12
|
+
expect Tag.new(:name => "not dup").to.be.valid? do
|
13
|
+
Tag.create!(:name => "not dup", :kind => "something")
|
14
|
+
end
|
15
|
+
|
16
|
+
expect Tag.new.not.to.be.valid?
|
17
|
+
expect String do
|
18
|
+
t = Tag.new
|
19
|
+
t.valid?
|
20
|
+
t.errors[:name]
|
21
|
+
end
|
22
|
+
|
23
|
+
expect Tag.create!(:name => "iamawesome", :kind => "awesomestuff") do
|
24
|
+
Tag.find_or_initialize_with_name_like_and_kind("iaMawesome", "awesomestuff")
|
25
|
+
end
|
26
|
+
|
27
|
+
expect true do
|
28
|
+
Tag.create!(:name => "iamawesome", :kind => "stuff")
|
29
|
+
Tag.find_or_initialize_with_name_like_and_kind("iaMawesome", "otherstuff").new_record?
|
30
|
+
end
|
31
|
+
|
32
|
+
expect Tag.create!(:kind => "language", :name => "french") do
|
33
|
+
Tag.of_kind("language").first
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
Expectations do
|
4
|
+
expect Tag do
|
5
|
+
t = Tagging.new :tag => Tag.new(:name => 'some_tag')
|
6
|
+
t.tag
|
7
|
+
end
|
8
|
+
|
9
|
+
expect Post do
|
10
|
+
t = Tagging.new :taggable => Post.new
|
11
|
+
t.taggable
|
12
|
+
end
|
13
|
+
|
14
|
+
expect 2 do
|
15
|
+
2.times { Post.create(:tag_list => "interesting") }
|
16
|
+
Tag.find_by_name("interesting").taggings.count
|
17
|
+
end
|
18
|
+
|
19
|
+
expect 1 do
|
20
|
+
p1 = Post.create(:tag_list => "witty")
|
21
|
+
p2 = Post.create(:tag_list => "witty")
|
22
|
+
|
23
|
+
p2.destroy
|
24
|
+
Tag.find_by_name("witty").taggings.count
|
25
|
+
end
|
26
|
+
|
27
|
+
expect 2 do
|
28
|
+
p1 = Post.create(:tag_list => "smart, pretty")
|
29
|
+
p1.taggings.count
|
30
|
+
end
|
31
|
+
|
32
|
+
expect 1 do
|
33
|
+
p1 = Post.create(:tag_list => "mildly, inappropriate")
|
34
|
+
|
35
|
+
Tag.find_by_name('inappropriate').destroy
|
36
|
+
p1.taggings.count
|
37
|
+
end
|
38
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'activerecord'
|
3
|
+
require File.dirname(__FILE__)+'/../lib/is_taggable'
|
4
|
+
require 'expectations'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
ActiveRecord::Base.configurations = {'sqlite3' => {:adapter => 'sqlite3', :database => ':memory:'}}
|
8
|
+
ActiveRecord::Base.establish_connection('sqlite3')
|
9
|
+
|
10
|
+
ActiveRecord::Base.logger = Logger.new(STDERR)
|
11
|
+
ActiveRecord::Base.logger.level = Logger::WARN
|
12
|
+
|
13
|
+
ActiveRecord::Schema.define(:version => 0) do
|
14
|
+
create_table :comments do |t|
|
15
|
+
end
|
16
|
+
|
17
|
+
create_table :posts do |t|
|
18
|
+
t.string :title, :default => ''
|
19
|
+
end
|
20
|
+
|
21
|
+
create_table :pages do |t|
|
22
|
+
t.string :title, :default => ''
|
23
|
+
end
|
24
|
+
|
25
|
+
create_table :tags do |t|
|
26
|
+
t.string :name, :default => ''
|
27
|
+
t.string :kind, :default => ''
|
28
|
+
end
|
29
|
+
|
30
|
+
create_table :taggings do |t|
|
31
|
+
t.integer :tag_id
|
32
|
+
|
33
|
+
t.string :taggable_type, :default => ''
|
34
|
+
t.integer :taggable_id
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Post < ActiveRecord::Base
|
39
|
+
is_taggable :tags, :languages
|
40
|
+
end
|
41
|
+
|
42
|
+
class Comment < ActiveRecord::Base
|
43
|
+
is_taggable
|
44
|
+
end
|
45
|
+
|
46
|
+
class Page < ActiveRecord::Base
|
47
|
+
is_taggable :categories, :fixed => true
|
48
|
+
end
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bdimcheff-is_taggable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Daniel Haran
|
8
|
+
- James Golick
|
9
|
+
- GiraffeSoft Inc.
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
|
14
|
+
date: 2009-02-16 00:00:00 -08:00
|
15
|
+
default_executable:
|
16
|
+
dependencies: []
|
17
|
+
|
18
|
+
description:
|
19
|
+
email: chebuctonian@mgmail.com
|
20
|
+
executables: []
|
21
|
+
|
22
|
+
extensions: []
|
23
|
+
|
24
|
+
extra_rdoc_files: []
|
25
|
+
|
26
|
+
files:
|
27
|
+
- README.rdoc
|
28
|
+
- VERSION.yml
|
29
|
+
- generators/is_taggable_migration
|
30
|
+
- generators/is_taggable_migration/is_taggable_migration_generator.rb
|
31
|
+
- generators/is_taggable_migration/templates
|
32
|
+
- generators/is_taggable_migration/templates/migration.rb
|
33
|
+
- lib/is_taggable.rb
|
34
|
+
- lib/tag.rb
|
35
|
+
- lib/tagging.rb
|
36
|
+
- test/is_taggable_test.rb
|
37
|
+
- test/tag_test.rb
|
38
|
+
- test/tagging_test.rb
|
39
|
+
- test/test_helper.rb
|
40
|
+
has_rdoc: true
|
41
|
+
homepage: http://github.com/giraffesoft/is_taggable
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options:
|
44
|
+
- --inline-source
|
45
|
+
- --charset=UTF-8
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
version:
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
requirements: []
|
61
|
+
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 1.2.0
|
64
|
+
signing_key:
|
65
|
+
specification_version: 2
|
66
|
+
summary: tagging that doesn't want to be on steroids. it's skinny and happy to stay that way.
|
67
|
+
test_files: []
|
68
|
+
|