tagtical 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/CHANGELOG +25 -0
  2. data/Gemfile +20 -0
  3. data/Gemfile.lock +25 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.rdoc +306 -0
  6. data/Rakefile +59 -0
  7. data/VERSION +1 -0
  8. data/generators/tagtical_migration/tagtical_migration_generator.rb +7 -0
  9. data/generators/tagtical_migration/templates/migration.rb +34 -0
  10. data/lib/generators/tagtical/migration/migration_generator.rb +32 -0
  11. data/lib/generators/tagtical/migration/templates/active_record/migration.rb +35 -0
  12. data/lib/tagtical/acts_as_tagger.rb +69 -0
  13. data/lib/tagtical/compatibility/Gemfile +8 -0
  14. data/lib/tagtical/compatibility/active_record_backports.rb +21 -0
  15. data/lib/tagtical/tag.rb +314 -0
  16. data/lib/tagtical/tag_list.rb +133 -0
  17. data/lib/tagtical/taggable/cache.rb +53 -0
  18. data/lib/tagtical/taggable/collection.rb +141 -0
  19. data/lib/tagtical/taggable/core.rb +317 -0
  20. data/lib/tagtical/taggable/ownership.rb +110 -0
  21. data/lib/tagtical/taggable/related.rb +60 -0
  22. data/lib/tagtical/taggable.rb +51 -0
  23. data/lib/tagtical/tagging.rb +42 -0
  24. data/lib/tagtical/tags_helper.rb +17 -0
  25. data/lib/tagtical.rb +47 -0
  26. data/rails/init.rb +1 -0
  27. data/spec/bm.rb +53 -0
  28. data/spec/database.yml +17 -0
  29. data/spec/database.yml.sample +17 -0
  30. data/spec/models.rb +60 -0
  31. data/spec/schema.rb +46 -0
  32. data/spec/spec_helper.rb +159 -0
  33. data/spec/tagtical/acts_as_tagger_spec.rb +94 -0
  34. data/spec/tagtical/tag_list_spec.rb +102 -0
  35. data/spec/tagtical/tag_spec.rb +301 -0
  36. data/spec/tagtical/taggable_spec.rb +460 -0
  37. data/spec/tagtical/tagger_spec.rb +76 -0
  38. data/spec/tagtical/tagging_spec.rb +52 -0
  39. data/spec/tagtical/tags_helper_spec.rb +28 -0
  40. data/spec/tagtical/tagtical_spec.rb +340 -0
  41. metadata +132 -0
data/spec/bm.rb ADDED
@@ -0,0 +1,53 @@
1
+ require 'active_record'
2
+ require 'action_view'
3
+ require File.expand_path('../../lib/tagtical', __FILE__)
4
+
5
+ if defined?(ActiveRecord::Acts::TaggableOn)
6
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::TaggableOn
7
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::Tagger
8
+ ActionView::Base.send :include, TagsHelper if defined?(ActionView::Base)
9
+ end
10
+
11
+ TEST_DATABASE_FILE = File.join(File.dirname(__FILE__), '..', 'test.sqlite3')
12
+ File.unlink(TEST_DATABASE_FILE) if File.exist?(TEST_DATABASE_FILE)
13
+ ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => TEST_DATABASE_FILE
14
+
15
+ ActiveRecord::Base.silence do
16
+ ActiveRecord::Migration.verbose = false
17
+ ActiveRecord::Schema.define :version => 0 do
18
+ create_table "taggings", :force => true do |t|
19
+ t.float "relevance"
20
+ t.integer "tag_id", :limit => 11
21
+ t.integer "taggable_id", :limit => 11
22
+ t.string "taggable_type"
23
+ t.string "context"
24
+ t.datetime "created_at"
25
+ t.integer "tagger_id", :limit => 11
26
+ t.string "tagger_type" if Tagtical.config.polymorphic_tagger?
27
+ end
28
+
29
+ add_index "taggings", ["tag_id"], :name => "index_taggings_on_tag_id"
30
+ add_index "taggings", ["taggable_id", "taggable_type"], :name => "index_taggings_on_taggable_id_and_taggable_type"
31
+
32
+ create_table :tags, :force => true do |t|
33
+ t.string :value
34
+ t.string :type
35
+ end
36
+ add_index :tags, [:type, :value], :unique => true
37
+ add_index :tags, :value
38
+
39
+ create_table :taggable_models, :force => true do |t|
40
+ t.column :name, :string
41
+ t.column :type, :string
42
+ t.column :cached_tag_list, :string
43
+ end
44
+ end
45
+
46
+ class TaggableModel < ActiveRecord::Base
47
+ acts_as_taggable(:languages, :skills, :needs, :offerings)
48
+ end
49
+ end
50
+
51
+ puts Benchmark.measure {
52
+ 1000.times { TaggableModel.create :tag_list => "awesome, epic, neat" }
53
+ }
data/spec/database.yml ADDED
@@ -0,0 +1,17 @@
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: tagtical.sqlite3
4
+
5
+ mysql:
6
+ adapter: mysql
7
+ hostname: localhost
8
+ username: root
9
+ password:
10
+ database: tagtical
11
+
12
+ postgresql:
13
+ adapter: postgresql
14
+ hostname: localhost
15
+ username: postgres
16
+ password:
17
+ database: tagtical
@@ -0,0 +1,17 @@
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: tagtical.sqlite3
4
+
5
+ mysql:
6
+ adapter: mysql
7
+ hostname: localhost
8
+ username: root
9
+ password:
10
+ database: tagtical
11
+
12
+ postgresql:
13
+ adapter: postgresql
14
+ hostname: localhost
15
+ username: postgres
16
+ password:
17
+ database: tagtical
data/spec/models.rb ADDED
@@ -0,0 +1,60 @@
1
+ ### START Tag Subclasses ###
2
+
3
+ module Tagtical
4
+ class Tag
5
+ class Language < Tagtical::Tag
6
+ end
7
+ class PartTag < Tagtical::Tag
8
+
9
+ def dump_value(value)
10
+ value && value.downcase
11
+ end
12
+
13
+ end
14
+ end
15
+ end
16
+ module Tag
17
+ class Skill < Tagtical::Tag
18
+
19
+ def load_value(value)
20
+ value.gsub("ball", "baller") if value
21
+ end
22
+
23
+ end
24
+ class Craft < Skill # Multiple levels of inheritance
25
+ end
26
+ end
27
+ class NeedTag < Tagtical::Tag # Tag subclass ending in "Tag"
28
+ end
29
+ class Offering < Tagtical::Tag # Top level
30
+ end
31
+
32
+ ### END Tag Subclasses ###
33
+
34
+ class TaggableModel < ActiveRecord::Base
35
+ acts_as_taggable(:languages, :skills, :crafts, :needs, :offerings)
36
+ has_many :untaggable_models
37
+ end
38
+
39
+ class CachedModel < ActiveRecord::Base
40
+ acts_as_taggable
41
+ end
42
+
43
+ class OtherTaggableModel < ActiveRecord::Base
44
+ acts_as_taggable(:languages, :needs, :offerings)
45
+ end
46
+
47
+ class InheritingTaggableModel < TaggableModel
48
+ end
49
+
50
+ class AlteredInheritingTaggableModel < TaggableModel
51
+ acts_as_taggable(:parts)
52
+ end
53
+
54
+ class TaggableUser < ActiveRecord::Base
55
+ acts_as_tagger
56
+ end
57
+
58
+ class UntaggableModel < ActiveRecord::Base
59
+ belongs_to :taggable_model
60
+ end
data/spec/schema.rb ADDED
@@ -0,0 +1,46 @@
1
+ ActiveRecord::Schema.define :version => 0 do
2
+ create_table "taggings", :force => true do |t|
3
+ t.integer "tag_id", :limit => 11
4
+ t.integer "taggable_id", :limit => 11
5
+ t.string "taggable_type"
6
+ t.datetime "created_at"
7
+ t.column :tagger_id, :integer
8
+ t.column :tagger_type, :string
9
+ t.column :relevance, :float
10
+ end
11
+
12
+ add_index :taggings, :tag_id
13
+ add_index :taggings, [:taggable_id, :taggable_type]
14
+
15
+ create_table :tags, :force => true do |t|
16
+ t.column :value, :string
17
+ t.column :type, :string
18
+ end
19
+ add_index :tags, [:type, :value], :unique => true
20
+ add_index :tags, :value
21
+
22
+ create_table :taggable_models, :force => true do |t|
23
+ t.column :name, :string
24
+ t.column :type, :string
25
+ end
26
+
27
+ create_table :untaggable_models, :force => true do |t|
28
+ t.column :taggable_model_id, :integer
29
+ t.column :name, :string
30
+ end
31
+
32
+ create_table :cached_models, :force => true do |t|
33
+ t.column :name, :string
34
+ t.column :type, :string
35
+ t.column :cached_tag_list, :string
36
+ end
37
+
38
+ create_table :taggable_users, :force => true do |t|
39
+ t.column :name, :string
40
+ end
41
+
42
+ create_table :other_taggable_models, :force => true do |t|
43
+ t.column :name, :string
44
+ t.column :type, :string
45
+ end
46
+ end
@@ -0,0 +1,159 @@
1
+ $LOAD_PATH << "." unless $LOAD_PATH.include?(".")
2
+
3
+ begin
4
+ require "rubygems"
5
+ require "bundler"
6
+
7
+ if Gem::Version.new(Bundler::VERSION) <= Gem::Version.new("0.9.5")
8
+ raise RuntimeError, "Your bundler version is too old." +
9
+ "Run `gem install bundler` to upgrade."
10
+ end
11
+
12
+ # Set up load paths for all bundled gems
13
+ Bundler.setup
14
+ rescue Bundler::GemNotFound
15
+ raise RuntimeError, "Bundler couldn't find some gems." +
16
+ "Did you run \`bundler install\`?"
17
+ end
18
+
19
+ Bundler.require(:test, :default)
20
+ require File.expand_path('../../lib/tagtical', __FILE__)
21
+
22
+ RSpec.configure do |config|
23
+ config.mock_with :mocha
24
+ end
25
+
26
+ RSpec::Matchers.define :have_tag_values do |expected|
27
+ match do |actual|
28
+ actual.map(&:value).sort == expected.sort
29
+ end
30
+ end
31
+
32
+ unless [].respond_to?(:freq)
33
+ class Array
34
+ def freq
35
+ k=Hash.new(0)
36
+ each {|e| k[e]+=1}
37
+ k
38
+ end
39
+ end
40
+ end
41
+
42
+ ENV['DB'] ||= 'sqlite3'
43
+
44
+ database_yml = File.expand_path('../database.yml', __FILE__)
45
+ if File.exists?(database_yml)
46
+ active_record_configuration = YAML.load_file(database_yml)[ENV['DB']]
47
+
48
+ ActiveRecord::Base.establish_connection(active_record_configuration)
49
+ ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), "debug.log"))
50
+
51
+ ActiveRecord::Base.silence do
52
+ ActiveRecord::Migration.verbose = false
53
+
54
+ load(File.dirname(__FILE__) + '/schema.rb')
55
+ load(File.dirname(__FILE__) + '/models.rb')
56
+ end
57
+
58
+ else
59
+ raise "Please create #{database_yml} first to configure your database. Take a look at: #{database_yml}.sample"
60
+ end
61
+
62
+ def clean_database!
63
+ models = [Tagtical::Tag, Tagtical::Tagging, TaggableModel, OtherTaggableModel, InheritingTaggableModel,
64
+ AlteredInheritingTaggableModel, TaggableUser, UntaggableModel]
65
+ models.each do |model|
66
+ ActiveRecord::Base.connection.execute "DELETE FROM #{model.table_name}"
67
+ end
68
+ end
69
+
70
+ clean_database!
71
+
72
+ module QueryAnalyzer
73
+ extend self
74
+ attr_accessor :enabled, :options
75
+ self.enabled = false
76
+ self.options = {}
77
+
78
+ IGNORE_SQL_REGEXP = /^explain/i # ignore SQL explain
79
+
80
+ def start(options=nil)
81
+ options = {:match => options} if options.is_a?(Regexp)
82
+ if block_given?
83
+ begin
84
+ (old_options, @options = @options, options) if options
85
+ old_enabled, @enabled = @enabled, true
86
+ yield
87
+ ensure
88
+ @enabled = old_enabled
89
+ @options = old_options if options
90
+ end
91
+ else
92
+ @options = options if options
93
+ @enabled = true
94
+ end
95
+ end
96
+
97
+ # Always disable it in production environment
98
+ def enabled?(sql=nil)
99
+ !Rails.env.production? && (@enabled && (!sql || match_sql?(sql)))
100
+ end
101
+
102
+ def match_sql?(sql)
103
+ sql !~ IGNORE_SQL_REGEXP && (!options[:match] || options[:match]===sql)
104
+ end
105
+
106
+ def stop
107
+ options.clear
108
+ @enabled = false
109
+ end
110
+
111
+ # class Results < Array
112
+ #
113
+ # def qa_columnized
114
+ # sized = {}
115
+ # self.each do |row|
116
+ # row.values.each_with_index do |value, i|
117
+ # sized[i] = [sized[i].to_i, row.keys[i].length, value.to_s.length].max
118
+ # end
119
+ # end
120
+ #
121
+ # table = []
122
+ # table << qa_columnized_row(self.first.keys, sized)
123
+ # table << '-' * table.first.length
124
+ # self.each { |row| table << qa_columnized_row(row.values, sized) }
125
+ # table.join("\n ") # Spaces added to work with format_log_entry
126
+ # end
127
+ #
128
+ # private
129
+ #
130
+ # def qa_columnized_row(fields, sized)
131
+ # row = []
132
+ # fields.each_with_index do |f, i|
133
+ # row << sprintf("%0-#{sized[i]}s", f.to_s)
134
+ # end
135
+ # row.join(' | ')
136
+ # end
137
+ #
138
+ # end # Results
139
+ end
140
+
141
+ module ActiveRecord
142
+ module ConnectionAdapters
143
+ class SQLiteAdapter < AbstractAdapter
144
+ private
145
+
146
+ def execute_with_analyzer(sql, name = nil)
147
+ if QueryAnalyzer.enabled?(sql)
148
+ display = "\nQUERY ANALYZER: \n#{sql}"
149
+ puts [display, nil, caller.map { |str| str.insert(0, " -> ")}]
150
+ @logger.debug(display) if @logger && @logger.debug?
151
+ end
152
+ execute_without_analyzer(sql, name)
153
+ end
154
+ # Always disable this in production
155
+ alias_method_chain :execute, :analyzer unless Rails.env.production?
156
+
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,94 @@
1
+ require File.expand_path('../../spec_helper', __FILE__)
2
+
3
+ describe "acts_as_tagger" do
4
+ before(:each) do
5
+ clean_database!
6
+ end
7
+
8
+ describe "Tagger Method Generation" do
9
+ before(:each) do
10
+ @tagger = TaggableUser.new()
11
+ end
12
+
13
+ it "should add #is_tagger? query method to the class-side" do
14
+ TaggableUser.should respond_to(:is_tagger?)
15
+ end
16
+
17
+ it "should return true from the class-side #is_tagger?" do
18
+ TaggableUser.is_tagger?.should be_true
19
+ end
20
+
21
+ it "should return false from the base #is_tagger?" do
22
+ ActiveRecord::Base.is_tagger?.should be_false
23
+ end
24
+
25
+ it "should add #is_tagger? query method to the singleton" do
26
+ @tagger.should respond_to(:is_tagger?)
27
+ end
28
+
29
+ it "should add #tag method on the instance-side" do
30
+ @tagger.should respond_to(:tag)
31
+ end
32
+
33
+ it "should generate an association for #owned_taggings and #owned_tags" do
34
+ @tagger.should respond_to(:owned_taggings, :owned_tags)
35
+ end
36
+ end
37
+
38
+ describe "#tag" do
39
+ context 'when called with a non-existent tag context' do
40
+ before(:each) do
41
+ @tagger = TaggableUser.new()
42
+ @taggable = TaggableModel.new(:name=>"Richard Prior")
43
+ end
44
+
45
+ it "should by default not throw an exception " do
46
+ @taggable.tag_list_on(:foo).should be_empty
47
+ lambda {
48
+ @tagger.tag(@taggable, :with=>'this, and, that', :on=>:foo)
49
+ }.should_not raise_error
50
+ end
51
+
52
+ it "should show all the tag list when both public and owned tags exist" do
53
+ @taggable.tag_list = 'ruby, python'
54
+ @tagger.tag(@taggable, :with => 'java, lisp', :on => :tags)
55
+ @taggable.all_tags_on(:tags).map(&:value).sort.should == %w(ruby python java lisp).sort
56
+ end
57
+
58
+ it "should not add owned tags to the common list" do
59
+ @taggable.tag_list = 'ruby, python'
60
+ @tagger.tag(@taggable, :with => 'java, lisp', :on => :tags)
61
+ @taggable.tag_list.should == %w(ruby python)
62
+ @tagger.tag(@taggable, :with => '', :on => :tags)
63
+ @taggable.tag_list.should == %w(ruby python)
64
+ end
65
+
66
+ end
67
+
68
+ describe "when called by multiple tagger's" do
69
+ before(:each) do
70
+ @user_x = TaggableUser.create(:name => "User X")
71
+ @user_y = TaggableUser.create(:name => "User Y")
72
+ @taggable = TaggableModel.create(:name => 'tagtical', :tag_list => 'plugin')
73
+
74
+ @user_x.tag(@taggable, :with => 'ruby, rails', :on => :tags)
75
+ @user_y.tag(@taggable, :with => 'ruby, plugin', :on => :tags)
76
+
77
+ @user_y.tag(@taggable, :with => '', :on => :tags)
78
+ @user_y.tag(@taggable, :with => '', :on => :tags)
79
+ end
80
+
81
+ it "should delete owned tags" do
82
+ @user_y.owned_tags.should == []
83
+ end
84
+
85
+ it "should not delete other taggers tags" do
86
+ @user_x.owned_tags.should have(2).items
87
+ end
88
+
89
+ it "should not delete original tags" do
90
+ @taggable.all_tags_list_on(:tags).should include('plugin')
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,102 @@
1
+ require File.expand_path('../../spec_helper', __FILE__)
2
+
3
+ describe Tagtical::TagList do
4
+ before { @tag_list = Tagtical::TagList.new("awesome", "radical") }
5
+ subject { @tag_list }
6
+
7
+ it { should be_an Array }
8
+
9
+ specify do
10
+ @tag_list.each do |value|
11
+ value.should be_an_instance_of Tagtical::TagList::TagValue
12
+ value.relevance.should be_nil
13
+ end
14
+ end
15
+
16
+ describe ".from" do
17
+
18
+ it "should accept a hash with relevance values" do
19
+ @tag_list = Tagtical::TagList.from("tag 1" => 4.5, "tag 2" => 4.454)
20
+ @tag_list.map(&:relevance).should == [4.5, 4.454]
21
+ end
22
+
23
+ end
24
+
25
+ it "should convert all values to Tagtical::TagList::TagValue" do
26
+ @tag_list << "foo"
27
+ @tag_list.concat(["bar"])
28
+ @tag_list.each do |value|
29
+ value.should be_an_instance_of Tagtical::TagList::TagValue
30
+ end
31
+ end
32
+
33
+ it "should be able to be add a new tag word" do
34
+ @tag_list.add("cool")
35
+ @tag_list.include?("cool").should be_true
36
+ end
37
+
38
+ it "should be able to add delimited lists of words" do
39
+ @tag_list.add("cool, wicked", :parse => true)
40
+ @tag_list.should include("cool", "wicked")
41
+ end
42
+
43
+ it "should be able to add delimited list of words with quoted delimiters" do
44
+ @tag_list.add("'cool, wicked', \"really cool, really wicked\"", :parse => true)
45
+ @tag_list.should include("cool, wicked", "really cool, really wicked")
46
+ end
47
+
48
+ it "should be able to handle other uses of quotation marks correctly" do
49
+ @tag_list.add("john's cool car, mary's wicked toy", :parse => true)
50
+ @tag_list.should include("john's cool car", "mary's wicked toy")
51
+ end
52
+
53
+ it "should be able to add an array of words" do
54
+ @tag_list.add(["cool", "wicked"], :parse => true)
55
+ @tag_list.should include("cool", "wicked")
56
+ end
57
+
58
+ it "should be able to add a hash" do
59
+ @tag_list.add("crazy" => 0.45, "narly" => 5.4)
60
+ @tag_list.should include("crazy", "narly")
61
+ @tag_list.detect { |t| t=="crazy" }.relevance.should == 0.45
62
+ end
63
+
64
+ it "should be able to remove words" do
65
+ @tag_list.remove("awesome")
66
+ @tag_list.include?("awesome").should be_false
67
+ end
68
+
69
+ it "should be able to remove delimited lists of words" do
70
+ @tag_list.remove("awesome, radical", :parse => true)
71
+ @tag_list.should be_empty
72
+ end
73
+
74
+ it "should be able to remove an array of words" do
75
+ @tag_list.remove(["awesome", "radical"], :parse => true)
76
+ @tag_list.should be_empty
77
+ end
78
+
79
+ its(:to_s) { should == "awesome, radical" }
80
+
81
+ it "should quote escape tags with commas in them" do
82
+ @tag_list.add("cool","rad,bodacious")
83
+ @tag_list.to_s.should == "awesome, radical, cool, \"rad,bodacious\""
84
+ end
85
+
86
+ it "should be able to call to_s on a frozen tag list" do
87
+ @tag_list.freeze
88
+ lambda { @tag_list.add("cool","rad,bodacious") }.should raise_error
89
+ lambda { @tag_list.to_s }.should_not raise_error
90
+ end
91
+
92
+ describe "TagValue" do
93
+ before do
94
+ @tag_value = Tagtical::TagList::TagValue.new("sweetness", @relevance = 3.2)
95
+ end
96
+ subject { @tag_value }
97
+
98
+ its(:relevance) { should == @relevance }
99
+
100
+ end
101
+
102
+ end