gnugeek-is_taggable 0.0.1

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/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