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 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,4 @@
1
+ ---
2
+ :minor: 1
3
+ :patch: 0
4
+ :major: 0
@@ -0,0 +1,7 @@
1
+ class IsTaggableMigrationGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => 'is_taggable_migration'
5
+ end
6
+ end
7
+ end
@@ -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
@@ -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,4 @@
1
+ class Tagging < ActiveRecord::Base
2
+ belongs_to :tag
3
+ belongs_to :taggable, :polymorphic => true
4
+ end
@@ -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
@@ -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
+