gnugeek-is_taggable 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,6 @@
1
+ [master]
2
+
3
+ *
4
+
5
+ [0.0.1]
6
+ * (05 Feb 2009) Gemified
data/README.rdoc ADDED
@@ -0,0 +1,85 @@
1
+ = Is Taggable
2
+
3
+ Rails plugin for working easily with tagging stuff in your application.
4
+
5
+ Stores _tags_ in separate table and implements a polymorphic interface _taggable_ for attaching tags to ActiveRecord objects.
6
+
7
+ Comes with a generator for database migrations and autocompleting tags in forms (with the <tt>--with-autocompleter</tt> option)
8
+
9
+ See examples below.
10
+
11
+
12
+ == Usage
13
+
14
+ Install the plugin by cloning the repository and running
15
+
16
+ script/generate is_taggable .
17
+
18
+ To generate migration tables. The dot _is_ necessary.
19
+
20
+
21
+ == Example
22
+
23
+ In your _Article_ model:
24
+
25
+ class Article < ActiveRecord::Base
26
+ is_taggable :tags
27
+ end
28
+
29
+ Then:
30
+
31
+ >> Tag.all
32
+ Tag Load (0.3ms) SELECT * FROM `tags`
33
+ => [#<Tag id: 1, name: "one", kind: "tag", ...>, #<Tag id: 2, name: "two", kind: "tag", ...>]
34
+
35
+ >> a = Article.first
36
+ Article Load (0.4ms) SELECT * FROM `articles` LIMIT 1
37
+ => #<Article id: 1, title: ....>
38
+
39
+ >> a.tag_list
40
+ Article Load (0.4ms) SELECT * FROM `articles` LIMIT 1
41
+ Tag Load (0.5ms) SELECT `tags`.* FROM `tags` INNER JOIN taggings ON tags.id = taggings.tag_id WHERE ((`taggings`.taggable_type = 'Article') AND (`taggings`.taggable_id = 1)) AND (`tags`.`kind` = 'tag')
42
+ => ["one"]
43
+
44
+ >> a.tag_list = ['one', 'two']
45
+ => ["one", "two"]
46
+
47
+ >> a.save
48
+ ...
49
+ Tagging Create (0.3ms) INSERT INTO `taggings` (`updated_at`, `tag_id`, `taggable_type`, `taggable_id`, `created_at`) VALUES('2008-12-18 16:51:50', 2, 'Article', 1, '2008-12-18 16:51:50')
50
+ ...
51
+ => true
52
+
53
+ >> Article.find_all_tagged_with 'one'
54
+ Article Load (1.9ms) SELECT * FROM `articles`
55
+ Tagging Load (66.6ms) SELECT `taggings`.* FROM `taggings` WHERE (`taggings`.`taggable_id` IN (1,2,3) and `taggings`.`taggable_type` = 'Article')
56
+ Tag Load (0.4ms) SELECT * FROM `tags` WHERE (`tags`.`id` IN (1,2))
57
+ => [#<Article id: 1, title: "Lorem...", ...>]
58
+
59
+ >> Article.find_all_tagged_with ['one', 'two']
60
+ Article Load (2.1ms) SELECT * FROM `articles`
61
+ Tagging Load (2.9ms) SELECT `taggings`.* FROM `taggings` WHERE (`taggings`.`taggable_id` IN (1,2,3) and `taggings`.`taggable_type` = 'Article')
62
+ Tag Load (0.3ms) SELECT * FROM `tags` WHERE (`tags`.`id` IN (1,2))
63
+ => [#<Article id: 1, title: "Lorem...", ...>]
64
+
65
+ >> Article.first.find_tagged_alike
66
+ SQL (0.5ms) SELECT count(*) AS count_all FROM `tags` INNER JOIN taggings ON tags.id = taggings.tag_id WHERE ((`taggings`.taggable_type = 'Article') AND (`taggings`.taggable_id = 1))
67
+ Article Load (1.5ms) SELECT * FROM `articles` WHERE (id != '1')
68
+ Tagging Load (2.9ms) SELECT `taggings`.* FROM `taggings` WHERE (`taggings`.`taggable_id` IN (1,2,3) and `taggings`.`taggable_type` = 'Article')
69
+ Tag Load (0.3ms) SELECT * FROM `tags` WHERE (`tags`.`id` IN (1,2,3))
70
+ Tag Load (0.4ms) SELECT `tags`.* FROM `tags` INNER JOIN taggings ON tags.id = taggings.tag_id WHERE ((`taggings`.taggable_type = 'Article') AND (`taggings`.taggable_id = 1)) AND (`tags`.`kind` = 'tag')
71
+ => [#<Article id: 201, title: "Another...", ...>]
72
+
73
+
74
+ When you run generator with the <tt>--with-autocompleter</tt> option, also a controller and view are generated.
75
+ You can use it like this then:
76
+
77
+ <% form_for(@my_object) do |f| %>
78
+ <%= f.text_area :tag_list %>
79
+ <%= taggable_autocompleter_for('my_object_tag_list') %>
80
+ <% end %>
81
+
82
+ Tags matching input will be then added to your input/textarea. Assumes you separate tags with commas (,).
83
+
84
+
85
+ Copyright (c) 2008 James Golick & Karel Minarik, released under the MIT license
@@ -0,0 +1,14 @@
1
+ Description:
2
+ Generate migrations for +is_taggable+ plugin
3
+
4
+ Example:
5
+ ./script/generate is_taggable .
6
+
7
+ This will create:
8
+ db/migrate/XXXXXXXXXX_create_taggables.rb
9
+
10
+ If the --with-autocomplete options is passed, it will generate this as well:
11
+ create app/controllers/taggable_controller.rb
12
+ create app/views/taggable
13
+ create app/views/taggable/autocomplete.js.erb
14
+ create app/helpers/taggable_helper.rb
@@ -0,0 +1,35 @@
1
+ class IsTaggableGenerator < Rails::Generator::NamedBase
2
+
3
+ def manifest
4
+ record do |m|
5
+
6
+ # * Generate migration
7
+ m.migration_template 'migration.rb', "db/migrate", {
8
+ :assigns => { :migration_name => 'CreateTaggables' },
9
+ :migration_file_name => 'create_taggables'
10
+ }
11
+
12
+ # * Generate controller and view for Scriptaculous autocompleter
13
+ if options[:with_autocompleter]
14
+ m.file 'taggable_controller.rb', 'app/controllers/taggable_controller.rb', :collision => :skip
15
+ m.directory 'app/views/taggable'
16
+ m.file 'autocomplete.js.erb', 'app/views/taggable/autocomplete.js.erb', :collision => :skip
17
+ end
18
+
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def banner
25
+ "\nUsage: script/generate is_taggable . [options]\n\n"
26
+ end
27
+
28
+ def add_options!(opt)
29
+ opt.separator ''
30
+ opt.separator 'Options:'
31
+ opt.on("--with-autocompleter",
32
+ "Generate controller and view for autocompleting tags") { |v| options[:with_autocompleter] = v }
33
+ end
34
+
35
+ end
@@ -0,0 +1,5 @@
1
+ <ul>
2
+ <%- @autocomplete_tags.each do |tag| -%>
3
+ <li id="<%= tag.id %>"><%= tag.name %></li>
4
+ <%- end -%>
5
+ </ul>
@@ -0,0 +1,31 @@
1
+ # = Migration for the is_taggable plugin
2
+ #
3
+ class CreateTaggables < ActiveRecord::Migration
4
+ def self.up
5
+
6
+ create_table :tags do |t|
7
+ t.string :name, :default => ''
8
+ t.string :kind, :default => ''
9
+ t.timestamps
10
+ end
11
+
12
+ create_table :taggings do |t|
13
+ t.integer :tag_id
14
+ # Interface +taggable+
15
+ t.string :taggable_type
16
+ t.integer :taggable_id
17
+ t.timestamps
18
+ end
19
+
20
+ # Add indices
21
+ add_index :tags, :name
22
+ add_index :tags, [:name, :kind], :name => "name_and_kind"
23
+ add_index :taggings, [:taggable_type, :taggable_id], :name => "taggable_interface_index"
24
+
25
+ end
26
+
27
+ def self.down
28
+ drop_table :tags
29
+ drop_table :taggings
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ # = Return autocomplete results for the Tag class from is_taggable plugin
2
+ # Add before_filters, copy it somewhere else, etc, as you like.
3
+ # Just don't forget the <tt>app/views/taggable/autocomplete.js.erb</tt> file as well
4
+ #
5
+ class TaggableController < ApplicationController
6
+
7
+ # Return autocomplete results
8
+ def autocomplete
9
+ @autocomplete_tags = Tag.all( :conditions => ['name LIKE ?', "#{params[:autocomplete_tags]}%"], :order => 'name', :limit => 50)
10
+ respond_to do |format|
11
+ format.js { render :template => "autocomplete", :layout => false }
12
+ end
13
+ end
14
+
15
+ end
@@ -0,0 +1,32 @@
1
+ # = Encapsulating Script.aculo.us <tt>Ajax.Autocompleter</tt> method
2
+ # Over-ride the CSS in your stylesheets with a more specific selector, like <tt>.my-page div.autocomplete { color: red }</tt>
3
+
4
+ module TaggableHelper
5
+
6
+ # Returns <tt><div></tt>, CSS and JavaScript code needed for autocompleting tags
7
+ # See http://github.com/madrobby/scriptaculous/wikis/ajax-autocompleter
8
+ def taggable_autocompleter_for(field_name, options={})
9
+ authenticity_token_param = protect_against_forgery? ? "authenticity_token=#{form_authenticity_token}" : '' # Beware of tests :)
10
+ autocompleter=<<HTML
11
+ <style type="text/css" media="screen">
12
+ div.autocomplete {
13
+ position:absolute; width:250px;
14
+ background-color: white;
15
+ border:1px solid #888; margin:0; padding:0; }
16
+ div.autocomplete ul { list-style-type:none; margin:0; padding:0; }
17
+ div.autocomplete ul li.selected { background-color: #ecf4fe;}
18
+ div.autocomplete ul li { display: block; margin: 0; padding: 0.2em; height: 1em; cursor: pointer; }
19
+ </style>
20
+ <div id="#{field_name}_autocomplete_choices" class="autocomplete"></div>
21
+ <script type="text/javascript">
22
+ new Ajax.Autocompleter("#{field_name}",
23
+ "#{field_name}_autocomplete_choices",
24
+ "/taggable/autocomplete?#{authenticity_token_param}",
25
+ { paramName: 'autocomplete_tag', tokens: ',' }
26
+ );
27
+ </script>
28
+ HTML
29
+ return autocompleter
30
+ end
31
+
32
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'is_taggable'
@@ -0,0 +1,112 @@
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
+ def to_s
9
+ join(', ')
10
+ end
11
+ end
12
+
13
+ module ActiveRecordExtension
14
+ def is_taggable(*kinds)
15
+ class_inheritable_accessor :tag_kinds
16
+ self.tag_kinds = kinds.map(&:to_s).map(&:singularize)
17
+ self.tag_kinds << :tag if kinds.empty?
18
+
19
+ include IsTaggable::TaggableMethods
20
+ end
21
+ end
22
+
23
+ module TaggableMethods
24
+ def self.included(klass)
25
+ klass.class_eval do
26
+ include IsTaggable::TaggableMethods::InstanceMethods
27
+
28
+ has_many :taggings, :as => :taggable
29
+ has_many :tags, :through => :taggings
30
+ after_save :save_tags
31
+
32
+ tag_kinds.each do |k|
33
+ define_method("#{k}_list") { get_tag_list(k) }
34
+ define_method("#{k}_list=") { |new_list| set_tag_list(k, new_list) }
35
+ end
36
+
37
+ # Find all records tagged with a +'tag'+ or ['tag one', 'tag two']
38
+ # Pass either String for single tag or Array for multiple tags
39
+ # TODO : Add option all x any
40
+ def self.find_all_tagged_with(tag_or_tags, conditions=[])
41
+ return [] if tag_or_tags.nil? || tag_or_tags.empty?
42
+ case tag_or_tags
43
+ when Array, IsTaggable::TagList
44
+ all(:include => ['tags', 'taggings'], :conditions => conditions ).select { |record| tag_or_tags.all? { |tag| record.tags.map(&:name).include?(tag) } } || []
45
+ else
46
+ all(:include => ['tags', 'taggings'], :conditions => conditions).select { |record| record.tags.map(&:name).include?(tag_or_tags) } || []
47
+ end
48
+ end
49
+
50
+ def self.find_all_ids_tagged_with(tag_or_tags, conditions=[])
51
+ find_all_tagged_with(tag_or_tags, conditions).map(&:id)
52
+ end
53
+
54
+ # Find all records tagged with the same tags as current object,
55
+ # *excluding* the current object (for things like "Related articles")
56
+ # TODO : Add option all x any
57
+ # TODO : Remove hardcoded +tag_list+ kind of tags, could be any kind
58
+ def find_tagged_alike
59
+ return [] if self.tags.empty?
60
+ self.class.all(:include => ['tags', 'taggings'],
61
+ :conditions => ["id != '?'", self.id]).
62
+ select { |record| self.tag_list.all? { |tag| record.tags.map(&:name).include?(tag) } } || []
63
+ end
64
+
65
+ end
66
+ end
67
+
68
+ module InstanceMethods
69
+ def set_tag_list(kind, list)
70
+ list.gsub!(/ *, */,',') unless list.is_a?(Array)
71
+ tag_list = TagList.new(list.is_a?(Array) ? list : list.split(','))
72
+ instance_variable_set(tag_list_name_for_kind(kind), tag_list)
73
+ end
74
+
75
+ def get_tag_list(kind)
76
+ set_tag_list(kind, tags.of_kind(kind).map(&:name)) if tag_list_instance_variable(kind).nil?
77
+ tag_list_instance_variable(kind)
78
+ end
79
+
80
+ protected
81
+ def tag_list_name_for_kind(kind)
82
+ "@#{kind}_list"
83
+ end
84
+
85
+ def tag_list_instance_variable(kind)
86
+ instance_variable_get(tag_list_name_for_kind(kind))
87
+ end
88
+
89
+ def save_tags
90
+ tag_kinds.each do |tag_kind|
91
+ delete_unused_tags(tag_kind)
92
+ add_new_tags(tag_kind)
93
+ end
94
+
95
+ taggings.each(&:save)
96
+ end
97
+
98
+ def delete_unused_tags(tag_kind)
99
+ tags.of_kind(tag_kind).each { |t| tags.delete(t) unless get_tag_list(tag_kind).include?(t.name) }
100
+ end
101
+
102
+ def add_new_tags(tag_kind)
103
+ tag_names = tags.of_kind(tag_kind).map(&:name)
104
+ get_tag_list(tag_kind).each do |tag_name|
105
+ tags << Tag.find_or_initialize_with_name_like_and_kind(tag_name, tag_kind) unless tag_names.include?(tag_name)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ ActiveRecord::Base.send(:extend, IsTaggable::ActiveRecordExtension)
data/lib/tag.rb ADDED
@@ -0,0 +1,20 @@
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
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
+ named_scope :unique_by_name_for_kind, lambda { |kind| { :conditions => {:kind => kind}, :group => 'id,name,kind,created_at,updated_at' } }
16
+
17
+ def self.unique_tag_list_by_kind(kind)
18
+ unique_by_name_for_kind('available_service').map(&:name)
19
+ end
20
+ end
data/lib/tagging.rb ADDED
@@ -0,0 +1,3 @@
1
+ class Tagging < ActiveRecord::Base
2
+ belongs_to :tag
3
+ end
@@ -0,0 +1,59 @@
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 ["something cool", "something else cool"] do
13
+ p = Post.new :tag_list => "something cool, something else cool"
14
+ p.tag_list
15
+ end
16
+
17
+ expect ["something cool", "something new"] do
18
+ p = Post.new :tag_list => "something cool, something else cool"
19
+ p.save!
20
+ p.tag_list = "something cool, something new"
21
+ p.save!
22
+ p.tags.reload
23
+ p.instance_variable_set("@tag_list", nil)
24
+ p.tag_list
25
+ end
26
+
27
+ expect ["english", "french"] do
28
+ p = Post.new :language_list => "english, french"
29
+ p.save!
30
+ p.tags.reload
31
+ p.instance_variable_set("@language_list", nil)
32
+ p.language_list
33
+ end
34
+
35
+ expect ["english", "french"] do
36
+ p = Post.new :language_list => "english, french"
37
+ p.language_list
38
+ end
39
+
40
+ expect "english, french" do
41
+ p = Post.new :language_list => "english, french"
42
+ p.language_list.to_s
43
+ end
44
+
45
+ # added - should clean up strings with arbitrary spaces around commas
46
+ expect ["spaces","should","not","matter"] do
47
+ p = Post.new
48
+ p.tag_list = "spaces,should, not,matter"
49
+ p.save!
50
+ p.tags.reload
51
+ p.tag_list
52
+ end
53
+
54
+ expect 2 do
55
+ p = Post.new :language_list => "english, french"
56
+ p.save!
57
+ p.tags.length
58
+ end
59
+ end
data/test/tag_test.rb ADDED
@@ -0,0 +1,36 @@
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
+
36
+ end
@@ -0,0 +1,8 @@
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
+ end
@@ -0,0 +1,34 @@
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 :posts do |t|
15
+ t.string :title, :default => ''
16
+ end
17
+
18
+ create_table :tags do |t|
19
+ t.string :name, :default => ''
20
+ t.string :kind, :default => ''
21
+ end
22
+
23
+ create_table :taggings do |t|
24
+ t.integer :tag_id
25
+
26
+ t.string :taggable_type, :default => ''
27
+ t.integer :taggable_id
28
+ end
29
+ end
30
+
31
+ class Post < ActiveRecord::Base
32
+ is_taggable :tags, :languages, :unique
33
+ end
34
+
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gnugeek-is_taggable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - giraffesoft,Karmi,Brian Knox
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-02-04 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Extends activerecord to make models taggable
17
+ email: gnutse@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ - CHANGELOG
25
+ files:
26
+ - init.rb
27
+ - README.rdoc
28
+ - CHANGELOG
29
+ - lib/is_taggable.rb
30
+ - lib/tagging.rb
31
+ - lib/tag.rb
32
+ - generators/is_taggable/is_taggable_generator.rb
33
+ - generators/is_taggable/USAGE
34
+ - generators/is_taggable/templates/autocomplete.js.erb
35
+ - generators/is_taggable/templates/migration.rb
36
+ - generators/is_taggable/templates/taggable_controller.rb
37
+ - generators/is_taggable/templates/taggable_helper.rb
38
+ has_rdoc: true
39
+ homepage: http://github.com/gnugeek/is_taggable/
40
+ post_install_message:
41
+ rdoc_options:
42
+ - --main
43
+ - README.rdoc
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ requirements: []
59
+
60
+ rubyforge_project:
61
+ rubygems_version: 1.2.0
62
+ signing_key:
63
+ specification_version: 2
64
+ summary: tagging that isn't ugly
65
+ test_files:
66
+ - test/is_taggable_test.rb
67
+ - test/tagging_test.rb
68
+ - test/tag_test.rb
69
+ - test/test_helper.rb