tagtical 1.0.6

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