decisiv-is_msfte_taggable 0.0.4
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 +6 -0
- data/README.rdoc +4 -0
- data/generators/is_msfte_taggable/USAGE +14 -0
- data/generators/is_msfte_taggable/is_msfte_taggable_generator.rb +30 -0
- data/generators/is_msfte_taggable/templates/autocomplete.js.erb +5 -0
- data/generators/is_msfte_taggable/templates/migration.rb +31 -0
- data/generators/is_msfte_taggable/templates/taggable_controller.rb +15 -0
- data/generators/is_msfte_taggable/templates/taggable_helper.rb +32 -0
- data/init.rb +1 -0
- data/lib/is_msfte_taggable.rb +114 -0
- data/lib/is_msfte_taggable/tag.rb +29 -0
- data/lib/is_msfte_taggable/tagging.rb +9 -0
- data/test/is_msfte_taggable_test.rb +59 -0
- data/test/tag_test.rb +36 -0
- data/test/tagging_test.rb +8 -0
- data/test/test_helper.rb +37 -0
- metadata +71 -0
data/CHANGELOG
ADDED
data/README.rdoc
ADDED
@@ -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,30 @@
|
|
1
|
+
class IsMsfteTaggableGenerator < Rails::Generator::NamedBase
|
2
|
+
|
3
|
+
def manifest
|
4
|
+
record do |m|
|
5
|
+
|
6
|
+
m.migration_template 'migration.rb', "db/migrate", {
|
7
|
+
:assigns => { :migration_name => 'CreateTaggables' },
|
8
|
+
:migration_file_name => 'create_taggables'
|
9
|
+
}
|
10
|
+
|
11
|
+
if options[:with_tasks]
|
12
|
+
m.file 'blah', :collison => :skip
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def banner
|
20
|
+
"\nUsage: script/generate is_taggable . [options]\n\n"
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_options!(opt)
|
24
|
+
opt.separator ''
|
25
|
+
opt.separator 'Options:'
|
26
|
+
opt.on("--with-tasks",
|
27
|
+
"Generate tasks for MSFTE catalog setup") { |v| options[:with_tasks] = v }
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -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,114 @@
|
|
1
|
+
|
2
|
+
require 'is_msfte_taggable/tag'
|
3
|
+
require 'is_msfte_taggable/tagging'
|
4
|
+
|
5
|
+
module IsMsfteTaggable
|
6
|
+
|
7
|
+
class TagList < Array
|
8
|
+
def to_s
|
9
|
+
join(', ')
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ActiveRecordExtension
|
14
|
+
|
15
|
+
def is_msfte_taggable(*kinds)
|
16
|
+
class_inheritable_accessor :tag_kinds
|
17
|
+
self.tag_kinds = kinds.map(&:to_s).map(&:singularize)
|
18
|
+
self.tag_kinds << :tag if kinds.empty?
|
19
|
+
include IsMsfteTaggable::TaggableMethods
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
module TaggableMethods
|
25
|
+
|
26
|
+
def self.included(klass)
|
27
|
+
klass.class_eval do
|
28
|
+
include IsMsfteTaggable::TaggableMethods::InstanceMethods
|
29
|
+
|
30
|
+
has_many :taggings, :as => :taggable
|
31
|
+
has_many :tags, :through => :taggings
|
32
|
+
after_save :save_tags
|
33
|
+
|
34
|
+
def self.make_search_string(search_string, boolean="OR")
|
35
|
+
search_string.split(/ +/).map {|term| '"' + term + '*"'}.join(" #{boolean} ")
|
36
|
+
end
|
37
|
+
|
38
|
+
tag_kinds.each do |k|
|
39
|
+
define_method("#{k}_list") { get_tag_list(k) }
|
40
|
+
define_method("#{k}_list=") { |new_list| set_tag_list(k, new_list) }
|
41
|
+
|
42
|
+
named_scope "with_#{k}_tags".to_sym, lambda { |tag_or_tags|
|
43
|
+
if tag_or_tags.blank?
|
44
|
+
{}
|
45
|
+
else
|
46
|
+
start_condition = "id IN (SELECT taggable_id FROM #{Tagging.table_name}, #{Tag.table_name}
|
47
|
+
WHERE #{Tagging.table_name}.taggable_type = '#{klass.name}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND
|
48
|
+
CONTAINS (#{Tag.table_name}.name, '#{make_search_string(tag_or_tags, 'OR')}*'))"
|
49
|
+
{ :conditions => start_condition }
|
50
|
+
end
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
named_scope "with_tags".to_sym, lambda { |tag_or_tags|
|
55
|
+
if tag_or_tags.blank?
|
56
|
+
{}
|
57
|
+
else
|
58
|
+
start_condition = "id IN (SELECT taggable_id FROM #{Tagging.table_name}, #{Tag.table_name}
|
59
|
+
WHERE #{Tagging.table_name}.taggable_type = '#{klass.name}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND
|
60
|
+
CONTAINS (#{Tag.table_name}.name, '#{make_search_string(tag_or_tags,'OR')}*'))"
|
61
|
+
{ :conditions => start_condition }
|
62
|
+
end
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
module InstanceMethods
|
68
|
+
def set_tag_list(kind, list)
|
69
|
+
list.gsub!(/ *, */,',') unless list.is_a?(Array)
|
70
|
+
tag_list = TagList.new(list.is_a?(Array) ? list : list.split(','))
|
71
|
+
instance_variable_set(tag_list_name_for_kind(kind), tag_list)
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_tag_list(kind)
|
75
|
+
set_tag_list(kind, tags.of_kind(kind).map(&:name)) if tag_list_instance_variable(kind).nil?
|
76
|
+
tag_list_instance_variable(kind)
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
def tag_list_name_for_kind(kind)
|
83
|
+
"@#{kind}_list"
|
84
|
+
end
|
85
|
+
|
86
|
+
def tag_list_instance_variable(kind)
|
87
|
+
instance_variable_get(tag_list_name_for_kind(kind))
|
88
|
+
end
|
89
|
+
|
90
|
+
def save_tags
|
91
|
+
tag_kinds.each do |tag_kind|
|
92
|
+
delete_unused_tags(tag_kind)
|
93
|
+
add_new_tags(tag_kind)
|
94
|
+
end
|
95
|
+
|
96
|
+
taggings.each(&:save)
|
97
|
+
end
|
98
|
+
|
99
|
+
def delete_unused_tags(tag_kind)
|
100
|
+
tags.of_kind(tag_kind).each { |t| tags.delete(t) unless get_tag_list(tag_kind).include?(t.name) }
|
101
|
+
end
|
102
|
+
|
103
|
+
def add_new_tags(tag_kind)
|
104
|
+
tag_names = tags.of_kind(tag_kind).map(&:name)
|
105
|
+
get_tag_list(tag_kind).each do |tag_name|
|
106
|
+
tags << Tag.find_or_initialize_with_name_like_and_kind(tag_name, tag_kind) unless tag_names.include?(tag_name)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
ActiveRecord::Base.send :extend, IsMsfteTaggable::ActiveRecordExtension
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module IsMsfteTaggable
|
2
|
+
class Tag < ActiveRecord::Base
|
3
|
+
|
4
|
+
set_table_name :tags
|
5
|
+
|
6
|
+
has_many :taggings, :class_name => 'IsMsfteTaggable::Tagging', :foreign_key => 'tag_id'
|
7
|
+
|
8
|
+
validates_presence_of :name
|
9
|
+
validates_uniqueness_of :name, :scope => :kind
|
10
|
+
|
11
|
+
named_scope :with_name_like_and_kind, lambda { |name, kind| { :conditions => ["name like ? AND kind = ?", name, kind] } }
|
12
|
+
named_scope :of_kind, lambda { |kind| { :conditions => {:kind => kind} } }
|
13
|
+
named_scope :unique_by_name_for_kind, lambda { |kind| { :conditions => {:kind => kind}, :group => 'id,name,kind,created_at,updated_at' } }
|
14
|
+
|
15
|
+
class << self
|
16
|
+
|
17
|
+
def find_or_initialize_with_name_like_and_kind(name, kind)
|
18
|
+
with_name_like_and_kind(name, kind).first || new(:name => name, :kind => kind)
|
19
|
+
end
|
20
|
+
|
21
|
+
def unique_tag_list_by_kind(kind)
|
22
|
+
unique_by_name_for_kind('available_service').map(&:name)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
end
|
29
|
+
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
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'activerecord'
|
3
|
+
require File.dirname(__FILE__)+'/../lib/is_msfte_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 :users do |t|
|
15
|
+
t.string :name, :default => ''
|
16
|
+
end
|
17
|
+
|
18
|
+
create_table :posts do |t|
|
19
|
+
t.string :title, :default => ''
|
20
|
+
end
|
21
|
+
|
22
|
+
create_table :tags do |t|
|
23
|
+
t.string :name, :default => ''
|
24
|
+
t.string :kind, :default => ''
|
25
|
+
end
|
26
|
+
|
27
|
+
create_table :taggings do |t|
|
28
|
+
t.integer :tag_id
|
29
|
+
|
30
|
+
t.string :taggable_type, :default => ''
|
31
|
+
t.integer :taggable_id
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Post < ActiveRecord::Base
|
36
|
+
is_msfte_taggable :tags, :languages, :unique
|
37
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: decisiv-is_msfte_taggable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ken Collins
|
8
|
+
- Brian Knox
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2009-09-10 00:00:00 -07:00
|
14
|
+
default_executable:
|
15
|
+
dependencies: []
|
16
|
+
|
17
|
+
description: Extends ActiveRecord models to have tagging with full text search capability
|
18
|
+
email: gnutse@gmail.com
|
19
|
+
executables: []
|
20
|
+
|
21
|
+
extensions: []
|
22
|
+
|
23
|
+
extra_rdoc_files:
|
24
|
+
- README.rdoc
|
25
|
+
- CHANGELOG
|
26
|
+
files:
|
27
|
+
- init.rb
|
28
|
+
- README.rdoc
|
29
|
+
- CHANGELOG
|
30
|
+
- lib/is_msfte_taggable.rb
|
31
|
+
- lib/is_msfte_taggable/tagging.rb
|
32
|
+
- lib/is_msfte_taggable/tag.rb
|
33
|
+
- generators/is_msfte_taggable/is_msfte_taggable_generator.rb
|
34
|
+
- generators/is_msfte_taggable/USAGE
|
35
|
+
- generators/is_msfte_taggable/templates/autocomplete.js.erb
|
36
|
+
- generators/is_msfte_taggable/templates/migration.rb
|
37
|
+
- generators/is_msfte_taggable/templates/taggable_controller.rb
|
38
|
+
- generators/is_msfte_taggable/templates/taggable_helper.rb
|
39
|
+
has_rdoc: true
|
40
|
+
homepage: http://github.com/decisiv/is_msfte_taggable/
|
41
|
+
licenses:
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options:
|
44
|
+
- --main
|
45
|
+
- README.rdoc
|
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.3.5
|
64
|
+
signing_key:
|
65
|
+
specification_version: 2
|
66
|
+
summary: Tagging with full text search for MS SQL Server
|
67
|
+
test_files:
|
68
|
+
- test/is_msfte_taggable_test.rb
|
69
|
+
- test/tagging_test.rb
|
70
|
+
- test/tag_test.rb
|
71
|
+
- test/test_helper.rb
|