scoped-tags 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Josh Kalderimis
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 0
3
+ :major: 0
4
+ :minor: 3
@@ -0,0 +1,7 @@
1
+ class ScopedTagsGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => "scoped_tags_migration"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,21 @@
1
+ class ScopedTagsMigration < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :tags do |t|
4
+ t.string :name
5
+ t.string :context
6
+ end
7
+
8
+ create_table :taggings do |t|
9
+ t.references :tag
10
+ t.references :taggable, :polymorphic => true
11
+ end
12
+
13
+ add_index "tags", ['context', 'name']
14
+ add_index "taggings", ['taggable_id', 'taggable_type']
15
+ end
16
+
17
+ def self.down
18
+ drop_table :taggings
19
+ drop_table :tags
20
+ end
21
+ end
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,13 @@
1
+ require 'scoped_tags/active_record_additions'
2
+ require 'scoped_tags/tag'
3
+ require 'scoped_tags/tagging'
4
+ require 'scoped_tags/tag_list_proxy'
5
+ require 'scoped_tags/tag_list_collection'
6
+
7
+ module ActiveRecord
8
+ Base.class_eval do
9
+ include ScopedTags::ActiveRecordAdditions
10
+ end
11
+ end
12
+
13
+ TagListCollection.delimiter = ','
@@ -0,0 +1,64 @@
1
+ module ScopedTags
2
+
3
+ module ActiveRecordAdditions
4
+
5
+ def self.included(base)
6
+ base.class_eval do
7
+ def self.scoped_tags(contexts, options = nil)
8
+ self.class.instance_eval{ attr_accessor :tag_contexts }
9
+
10
+ raise ScopedTagsError, 'context is required for scoped-tags setup' if contexts.blank?
11
+
12
+ self.tag_contexts = [contexts].flatten
13
+
14
+ has_many :taggings, :as => :taggable, :class_name => 'Tagging', :dependent => :delete_all
15
+ has_many :tags, :through => :taggings, :class_name => 'Tag', :readonly => true
16
+
17
+ self.tag_contexts.each do |context|
18
+ has_many context, :through => :taggings, :class_name => 'Tag',
19
+ :source => :tag,
20
+ :conditions => ['context = ?', context.to_s.downcase]
21
+
22
+ c = context.to_s.singularize
23
+ define_method("#{c}_list") { get_tag_list(context) }
24
+ define_method("#{c}_list=") { |new_list| set_tag_list(context, new_list) }
25
+ self.class.instance_eval do
26
+ define_method("tagged_with_#{context}") { |*args| find_tagged_with(args.first, context.to_s, args.extract_options!) }
27
+ end
28
+ end
29
+
30
+ self.send :extend, ClassMethods
31
+ self.send :include, InstanceMethods
32
+ end
33
+ end
34
+ end
35
+
36
+
37
+ module ClassMethods
38
+ def find_tagged_with(tag_names, context, options = {})
39
+ tag_names = tag_names.is_a?(Array) ? tag_names : tag_names.split(TagListCollection.delimiter)
40
+ tag_names = tag_names.collect(&:strip).reject(&:blank?)
41
+
42
+ required_options = { :include => [:taggings, :tags],
43
+ :conditions => ['tags.name IN (?) AND tags.context = ?', tag_names, context] }
44
+
45
+ self.all(options.merge(required_options))
46
+ end
47
+ end
48
+
49
+
50
+ module InstanceMethods
51
+ protected
52
+ def get_tag_list(context)
53
+ @tag_list_collections = { } if not @tag_list_collections
54
+ @tag_list_collections[context] ||= TagListCollection.new(self, context.to_s.downcase)
55
+ end
56
+
57
+ def set_tag_list(context, new_tags)
58
+ get_tag_list(context).replace(new_tags)
59
+ end
60
+ end
61
+
62
+ end
63
+
64
+ end
@@ -0,0 +1,32 @@
1
+ class Tag < ActiveRecord::Base
2
+ has_many :taggings, :dependent => :delete_all
3
+
4
+ validates_presence_of :context
5
+ validates_uniqueness_of :name, :case_sensitive => false, :scope => :context
6
+
7
+ attr_accessible :name, :context
8
+
9
+ before_validation :trim_spaces, :lowercase_name
10
+
11
+
12
+ def self.find_or_new_by_name_and_context(name, context)
13
+ tag = self.find(:first, :conditions => ["name = ? and context = ?", name, context])
14
+ tag || Tag.new(:name => name, :context => context)
15
+ end
16
+
17
+ def ==(object)
18
+ super || (object.is_a?(Tag) && self.name == object.name && self.context == object.context)
19
+ end
20
+
21
+
22
+ private
23
+ def trim_spaces
24
+ self.name.try(:strip!).try(:squeeze!, ' ')
25
+ self.context.try(:strip!).try(:squeeze!, ' ')
26
+ end
27
+
28
+ def lowercase_name
29
+ self.name.try(:downcase!)
30
+ self.context.try(:downcase!)
31
+ end
32
+ end
@@ -0,0 +1,76 @@
1
+ class TagListCollection < TagListProxy
2
+
3
+ self.class.instance_eval do
4
+ attr_accessor :delimiter
5
+ end
6
+
7
+
8
+ def <<(tag_names)
9
+ tag_names = clean_tag_list(tag_names)
10
+ current_list = self.to_a
11
+ context_tags = self.proxy_owner.send(self.proxy_context)
12
+ tag_names.each do |new_tag|
13
+ unless current_list.include?(new_tag)
14
+ context_tags << Tag.find_or_new_by_name_and_context(new_tag, self.proxy_context.to_s)
15
+ end
16
+ end
17
+ self
18
+ end
19
+
20
+ alias_method :push, :<<
21
+ alias_method :concat, :<<
22
+
23
+
24
+ def delete(tag_names)
25
+ context_tags = self.proxy_owner.send(self.proxy_context)
26
+ to_delete = []
27
+ tag_names = clean_tag_list(tag_names)
28
+ context_tags.each do |tag|
29
+ to_delete << tag if tag_names.include?(tag.name)
30
+ end
31
+ context_tags.delete(to_delete)
32
+ to_delete.collect(&:name)
33
+ end
34
+
35
+ def delete_at(index)
36
+ context_tags = self.proxy_owner.send(self.proxy_context)
37
+ return nil if 0 > index or index > context_tags.size
38
+ tag_at_index = context_tags[index]
39
+ context_tags.delete(tag_at_index)
40
+ tag_at_index
41
+ end
42
+
43
+ def pop
44
+ self.delete_at(self.size - 1)
45
+ end
46
+
47
+ def replace(tag_names)
48
+ new_uniq_list = clean_tag_list(tag_names).uniq
49
+ new_tag_list = new_uniq_list.inject([]) do |list, tag_name|
50
+ list << Tag.find_or_new_by_name_and_context(tag_name, self.proxy_context)
51
+ list
52
+ end
53
+ context_tags = self.proxy_owner.send(self.proxy_context)
54
+ context_tags.replace(new_tag_list)
55
+ self
56
+ end
57
+
58
+
59
+ def to_s
60
+ self.join("#{TagListCollection.delimiter} ")
61
+ end
62
+
63
+
64
+ private
65
+
66
+ def find_target
67
+ association = self.proxy_owner.send proxy_context
68
+ association.collect(&:name)
69
+ end
70
+
71
+ def clean_tag_list(tags)
72
+ tags = tags.is_a?(Array) ? tags : tags.split(TagListCollection.delimiter)
73
+ tags.collect(&:strip).reject(&:blank?)
74
+ end
75
+
76
+ end
@@ -0,0 +1,98 @@
1
+ class TagListProxy
2
+
3
+ alias_method :proxy_respond_to?, :respond_to?
4
+ alias_method :proxy_extend, :extend
5
+ delegate :to_param, :to => :proxy_target
6
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
7
+
8
+ def initialize(owner, context)
9
+ @owner, @context = owner, context
10
+ @target = nil
11
+ end
12
+
13
+ # Returns the owner of the proxy.
14
+ def proxy_owner
15
+ @owner
16
+ end
17
+
18
+ # Returns the \context of the proxy, same as +context+.
19
+ def proxy_context
20
+ @context
21
+ end
22
+
23
+ # Returns the \target of the proxy, same as +target+.
24
+ def proxy_target
25
+ @target
26
+ end
27
+
28
+ # Does the proxy or its \target respond to +symbol+?
29
+ def respond_to?(*args)
30
+ proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
31
+ end
32
+
33
+ # Forwards <tt>===</tt> explicitly to the \target because the instance method
34
+ # removal above doesn't catch it. Loads the \target if needed.
35
+ def ===(other)
36
+ reload
37
+ other === @target
38
+ end
39
+
40
+ # Reloads the \target and returns +self+ on success.
41
+ def reload
42
+ @target = nil
43
+ load_target
44
+ self unless @target.nil?
45
+ end
46
+
47
+ # Returns the target of this proxy, same as +proxy_target+.
48
+ def target
49
+ @target
50
+ end
51
+
52
+ # Sets the target of this proxy to <tt>\target</tt>, and the \loaded flag to +true+.
53
+ def target=(target)
54
+ @target = target
55
+ end
56
+
57
+ # Forwards the call to the target. Loads the \target if needed.
58
+ def inspect
59
+ reload
60
+ @target.inspect
61
+ end
62
+
63
+ def send(method, *args)
64
+ if proxy_respond_to?(method)
65
+ super
66
+ else
67
+ reload
68
+ @target.send(method, *args)
69
+ end
70
+ end
71
+
72
+
73
+ private
74
+ # Forwards any missing method call to the \target.
75
+ def method_missing(method, *args)
76
+ if reload
77
+ unless @target.respond_to?(method)
78
+ message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}"
79
+ raise NoMethodError, message
80
+ end
81
+
82
+ if block_given?
83
+ @target.send(method, *args) { |*block_args| yield(*block_args) }
84
+ else
85
+ @target.send(method, *args)
86
+ end
87
+ end
88
+ end
89
+
90
+ def load_target
91
+ @target = find_target
92
+ @target
93
+ rescue ActiveRecord::RecordNotFound
94
+ reset
95
+ end
96
+
97
+
98
+ end
@@ -0,0 +1,8 @@
1
+ class Tagging < ActiveRecord::Base
2
+ belongs_to :tag
3
+ belongs_to :taggable, :polymorphic => true
4
+
5
+ validates_uniqueness_of :taggable_id, :scope => :tag_id
6
+
7
+ attr_accessible :taggable_type, :taggable_id, :tag_id
8
+ end
@@ -0,0 +1,93 @@
1
+ Scoped-Tags
2
+ ===========
3
+
4
+ When it comes to tagging dsls there are three main players, acts-as-taggable-on, acts-as-taggable-on-steriods and is-taggable.
5
+ Each seem to have one thing is common and thats using an after save callback to sync the tags list (usually an array) with
6
+ the tags association.
7
+
8
+ The key difference between scoped-tags and the other players on the field is that scoped-tags syncs
9
+ the array version with the association version upon each request.
10
+
11
+ The initial array tag list implementation was half Array half beast. The current version has been updated to use a proxy
12
+ based implementation for the array list, similar to the ActiveRecord ProxyAssociation which is used for the different
13
+ relationship associations. This is the heart of the code which syncs the array style listing with the association listing.
14
+
15
+ Scoped-tags will, in its next release, allow for tags to be strictly or silently scoped, with the default allowing
16
+ tag creation if the tag does not exist.
17
+
18
+
19
+ Key Features
20
+ ------------
21
+
22
+ - multiple tag contexts per model
23
+ - works with sphinx and thinking-sphinx
24
+ - add and remove tags using array like features
25
+ - association and array list stay in sync
26
+ - tags are available for use in the validations as they are always kept in sync
27
+
28
+
29
+
30
+ How can I use it?
31
+ -----------------
32
+
33
+ class Person
34
+ scoped_tags :genres
35
+ end
36
+
37
+ me = Person.new(:name => 'Josh')
38
+
39
+ me.interests << Tag.new(:name => 'scuba', :context => 'interests')
40
+ # it would be nice to leave the context out, but sadly not just yet
41
+ # and me.interests.build(:name => 'scuba') does not work at the moment either
42
+ # (has_many through issue)
43
+
44
+ me.interest_list => ['scuba']
45
+
46
+ me.interest_list << 'cycling'
47
+ # comma seperated strings are excepted, as well as arrays of strings
48
+
49
+ me.interests => [#<Tag id: nil, name: "scuba", context: 'interests'>,#<Tag id: nil, name: "cycling", context: 'interests'>]
50
+
51
+
52
+
53
+ Things to watch out for
54
+ -----------------------
55
+
56
+ ### _updating a scoped tagged model via a controller update_
57
+
58
+ be aware that if no tags are selected then the browser
59
+ does not pass through a blank array. This means if the model previously had tags saved to it and you remove the tags, the browser
60
+ will not pass though the changes because the select box is empty meaning the tags will not be removed.
61
+
62
+ An example of
63
+ this is the FCBKcomplete javascript plugin, it uses a select box behind the scenes to store the tags and pass
64
+ them through to the controller, but if none are selected then the browser passes nothing to the controller,
65
+ not even a blank array. To fix this, add the following method to the controller and add it as a before\_filter to the update
66
+ action.
67
+
68
+ Add to the controller:
69
+
70
+ before_filter :check_blank_tag_ids, :only => :update
71
+
72
+ def check_blank_tag_ids
73
+ params[:person][:interest_ids] = [] if params[:person] && params[:person][:interest_ids].blank?
74
+ end
75
+
76
+
77
+ ### _use a transaction when updating the scoped model_
78
+
79
+ otherwise the tags will save even if the model validations fail.
80
+ This is due to the standard behavior of has\_many :through relationships.
81
+
82
+ Example
83
+
84
+ Person.transaction { @person.update_attributes!(params[:person]) }
85
+
86
+
87
+
88
+
89
+ Future enhancements
90
+ -------------------
91
+
92
+ - add strict and silent scoping on setup (scoped_tags :interests, :strict => true), thus any tag added which is not already in the tags table for that context will be rejected
93
+ - get the instance.context.build method to work correctly
@@ -0,0 +1 @@
1
+ # Logfile created on Sat Oct 17 19:04:03 +0200 2009 by logger.rb/22285
@@ -0,0 +1,20 @@
1
+ ActiveRecord::Schema.define :version => 0 do
2
+
3
+ create_table "scoped_tagged_models", :force => true do |t|
4
+ t.string :name
5
+ end
6
+
7
+ create_table "tags", :force => true do |t|
8
+ t.string :name
9
+ t.string :context
10
+ end
11
+
12
+ create_table "taggings", :force => true do |t|
13
+ t.references :tag
14
+ t.references :taggable, :polymorphic => true
15
+ end
16
+
17
+ add_index "tags", ['context', 'name']
18
+ add_index "taggings", ['taggable_id', 'taggable_type']
19
+
20
+ end
@@ -0,0 +1,145 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+
4
+ describe "ScopedTaggedModel" do
5
+ context "attaching scoped-tags to a model" do
6
+ before(:each) do
7
+ @scoped_model = ScopedTaggedModel.new
8
+ end
9
+
10
+ it "should include the taggings method" do
11
+ @scoped_model.should respond_to(:taggings)
12
+ end
13
+
14
+ it "should include a tags method" do
15
+ @scoped_model.should respond_to(:tags)
16
+ end
17
+
18
+ it "should include the genres method" do
19
+ @scoped_model.should respond_to(:genres)
20
+ end
21
+
22
+ it "should at tag_contexts to the class" do
23
+ ScopedTaggedModel.should respond_to(:tag_contexts)
24
+ end
25
+
26
+ it "should create a getter and setter context tag list" do
27
+ @scoped_model.should respond_to(:genre_list, :genre_list=)
28
+ end
29
+ end
30
+
31
+ context "using scoped-tags in an every day situation" do
32
+ before(:each) do
33
+ @scoped_model = ScopedTaggedModel.new
34
+ @scoped_model.genres << Tag.new(:name => 'rock', :context => 'genres')
35
+ end
36
+
37
+ it "should return the genres association name list when genre_list is called" do
38
+ @scoped_model.genre_list.should include('rock')
39
+ end
40
+
41
+ it "should add a tag to the association when genre_list << is used" do
42
+ @scoped_model.genre_list << 'pop'
43
+ @scoped_model.genres.size.should == 2
44
+ @scoped_model.genre_list.should include('pop')
45
+ end
46
+
47
+ it "should change the entire list when using genre_list =" do
48
+ @scoped_model.genre_list = 'blues'
49
+ @scoped_model.genres.size.should == 1
50
+ @scoped_model.genre_list.should include('blues')
51
+ end
52
+
53
+ it "should change the entire list when using genre_list= with a comma seperated list of tags" do
54
+ @scoped_model.genre_list = 'blues, rock, country'
55
+ @scoped_model.genres.size.should == 3
56
+ @scoped_model.genre_list.should == ["rock", "blues", "country"]
57
+ end
58
+
59
+ it "should only add uniq tags to the association" do
60
+ @scoped_model.genre_list << 'blues'
61
+ @scoped_model.genres.size.should == 2
62
+ @scoped_model.genre_list.should include('blues', 'rock')
63
+ @scoped_model.genre_list << 'blues'
64
+ @scoped_model.genre_list << 'blues'
65
+ @scoped_model.genre_list << 'rock'
66
+ @scoped_model.genres.size.should == 2
67
+ @scoped_model.genre_list.should include('blues', 'rock')
68
+ end
69
+
70
+ it "should allow tags to be removed from the association and have the tag list act consistently" do
71
+ @scoped_model.genre_list << 'blues'
72
+ @scoped_model.genres.size.should == 2
73
+ @scoped_model.genres.delete(@scoped_model.genres.last)
74
+ @scoped_model.genres.size.should == 1
75
+ @scoped_model.genre_list.should include('rock')
76
+ end
77
+
78
+ it "#tagged_with_genres" do
79
+ tag = 'pop'
80
+
81
+ @scoped_model.genre_list << tag
82
+ @scoped_model.save!
83
+
84
+ ar_check = ScopedTaggedModel.all(:conditions => ['tags.name IN (?) AND tags.context = ?', tag, 'genres'], :include => [:taggings, :tags])
85
+ ar_check.size.should == 1
86
+
87
+ ScopedTaggedModel.methods.should include('tagged_with_genres')
88
+
89
+ st_check = ScopedTaggedModel.tagged_with_genres(tag)
90
+ st_check.size.should == 1
91
+ st_check.first.id.should == @scoped_model.id
92
+ end
93
+
94
+ it "#tagged_with_genres with options" do
95
+ tag = 'pop'
96
+
97
+ ScopedTaggedModel.delete_all
98
+ options_check1 = ScopedTaggedModel.new
99
+ options_check1.genre_list << tag
100
+ options_check1.save!
101
+
102
+ options_check2 = ScopedTaggedModel.new
103
+ options_check2.genre_list << tag
104
+ options_check2.save!
105
+
106
+ st_check = ScopedTaggedModel.tagged_with_genres(tag, :limit => 1)
107
+ st_check.size.should == 1
108
+ st_check.first.id.should == options_check1.id
109
+ end
110
+
111
+ it "#tagged_with_genres with options and multiple tags in an array" do
112
+ tag = ['pop', 'techno', 'dance']
113
+
114
+ ScopedTaggedModel.delete_all
115
+ options_check1 = ScopedTaggedModel.new
116
+ options_check1.genre_list << tag
117
+ options_check1.save!
118
+
119
+ options_check2 = ScopedTaggedModel.new
120
+ options_check2.genre_list << tag
121
+ options_check2.save!
122
+
123
+ st_check = ScopedTaggedModel.tagged_with_genres(tag, :limit => 1)
124
+ st_check.size.should == 1
125
+ st_check.first.id.should == options_check1.id
126
+ end
127
+
128
+ it "#tagged_with_genres with options and multiple tags in a string" do
129
+ tag = 'pop, techno, dance'
130
+
131
+ ScopedTaggedModel.delete_all
132
+ options_check1 = ScopedTaggedModel.new
133
+ options_check1.genre_list << tag
134
+ options_check1.save!
135
+
136
+ options_check2 = ScopedTaggedModel.new
137
+ options_check2.genre_list << tag
138
+ options_check2.save!
139
+
140
+ st_check = ScopedTaggedModel.tagged_with_genres(tag, :limit => 1)
141
+ st_check.size.should == 1
142
+ st_check.first.id.should == options_check1.id
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,63 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+
4
+ describe Tagging do
5
+ before(:all) { Tagging.create!(:tag_id => 1, :taggable_id => 1) }
6
+
7
+ it { should belong_to(:tag) }
8
+ it { should belong_to(:taggable) }
9
+ it { should validate_uniqueness_of(:taggable_id).scoped_to(:tag_id) }
10
+
11
+ it { should allow_mass_assignment_of(:taggable_id)
12
+ should allow_mass_assignment_of(:taggable_type)
13
+ should allow_mass_assignment_of(:tag_id) }
14
+
15
+ it { should_not allow_mass_assignment_of(:created_at)
16
+ should_not allow_mass_assignment_of(:updated_at) }
17
+
18
+ it { should have_db_index([:taggable_id, :taggable_type]) }
19
+ end
20
+
21
+
22
+ describe Tag do
23
+ before(:all) { Tag.create!(:name => 'rock', :context => 'genres') }
24
+
25
+ it { should have_many(:taggings).dependent(:delete_all) }
26
+
27
+ it { should validate_uniqueness_of(:name).scoped_to(:context).case_insensitive }
28
+ it { should validate_presence_of(:context) }
29
+
30
+ it { should allow_mass_assignment_of(:name)
31
+ should allow_mass_assignment_of(:context) }
32
+
33
+ it { should_not allow_mass_assignment_of(:created_at)
34
+ should_not allow_mass_assignment_of(:updated_at) }
35
+
36
+ it { should have_db_index([:context, :name]) }
37
+
38
+ it "should trim and squeeze spaces from name and context before validation" do
39
+ t = Tag.new(:name => ' rock and roll', :context => ' genres ')
40
+ t.valid?
41
+ t.name.should == 'rock and roll'
42
+ t.context.should == 'genres'
43
+ end
44
+
45
+ it "should lowercase the name and context before validation" do
46
+ t = Tag.new(:name => 'POP', :context => 'GENRES')
47
+ t.valid?
48
+ t.name.should == 'pop'
49
+ t.context.should == 'genres'
50
+ end
51
+
52
+ it "should recognize that it is equal to another tag with the same name and context" do
53
+ t = Tag.new(:name => 'pop', :context => 'genres')
54
+ s = Tag.new(:name => 'pop', :context => 'genres')
55
+ t.should == s
56
+ end
57
+
58
+ it "#find_or_new_by_name_ane_context" do
59
+ Tag.create!(:name => 'pop', :context => 'genres')
60
+ s = Tag.find_or_new_by_name_and_context('pop', 'genres')
61
+ s.new_record?.should be_false
62
+ end
63
+ end
@@ -0,0 +1,123 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+
4
+ describe TagListCollection do
5
+
6
+ before(:each) do
7
+ @sm = ScopedTaggedModel.new
8
+ @tlc = TagListCollection.new(@sm, :genres)
9
+ end
10
+
11
+ it "#delimiter" do
12
+ TagListCollection.delimiter.should == ','
13
+ TagListCollection.delimiter = '-'
14
+ TagListCollection.delimiter.should == '-'
15
+ TagListCollection.delimiter = ','
16
+ TagListCollection.delimiter.should == ','
17
+ end
18
+
19
+ it ".proxy_owner" do
20
+ @sm.genre_list.proxy_owner.should == @sm
21
+ end
22
+
23
+ it ".===" do
24
+ @sm.genre_list << "disco"
25
+ @sm.genre_list.should === ['disco']
26
+ end
27
+
28
+ it "should return an array with the class method is called (think its an array)" do
29
+ @tlc.class.should == Array
30
+ end
31
+
32
+ it "should return the tag names in the genre list" do
33
+ @sm.genres << Tag.new(:name => 'pop', :context => 'genres')
34
+ @sm.genres << Tag.new(:name => 'rock', :context => 'genres')
35
+ @sm.genre_list.should == ['pop', 'rock']
36
+ end
37
+
38
+ it "should add a tag to the tag list when the association << method is used" do
39
+ @sm.genre_list.should == []
40
+ @sm.genres << Tag.new(:name => 'rock', :context => 'genres')
41
+ @sm.genre_list.should == ['rock']
42
+ end
43
+
44
+ it "should be able to be add a new tag word using <<" do
45
+ @sm.genre_list << "disco"
46
+ @sm.genre_list.should include('disco')
47
+ end
48
+
49
+ it "should be able to be add a new tag word using push" do
50
+ @tlc.push('techno')
51
+ @tlc.should include('techno')
52
+ end
53
+
54
+ it "should be able to be add a new tag word using concat" do
55
+ @tlc.concat('dance')
56
+ @tlc.should include('dance')
57
+ end
58
+
59
+ it "should be able to add delimited lists of tags" do
60
+ @tlc << "techno, house"
61
+ @tlc.should include('techno')
62
+ @tlc.should include('house')
63
+ @tlc.size.should == 2
64
+ end
65
+
66
+ it "should be able to delete tags using delete" do
67
+ @tlc << 'rock'
68
+ @sm.genres.size.should == 1
69
+ @tlc.delete('rock')
70
+ @tlc.should_not include('rock')
71
+ @sm.genres.size.should == 0
72
+ end
73
+
74
+ it "should be able to delete tags using delete_at" do
75
+ @tlc << 'rock' << 'pop' << 'disco'
76
+ @sm.genres.size.should == 3
77
+ @sm.save!
78
+ @tlc.delete_at(0)
79
+ @tlc.should_not include('rock')
80
+ @sm.genres.size.should == 2
81
+ @sm.save!
82
+ @sm.reload
83
+ @sm.genres.size.should == 2
84
+ end
85
+
86
+ it "should be able to delete tags using pop" do
87
+ @tlc << 'rock' << 'pop' << 'disco'
88
+ @sm.genres.size.should == 3
89
+ @sm.genre_list.last.should == 'disco'
90
+ @tlc.pop
91
+ @tlc.should_not include('disco')
92
+ @sm.save!
93
+ @sm.reload
94
+ @sm.genres.size.should == 2
95
+ end
96
+
97
+ it "should give a delimited list of words when converted to string" do
98
+ @tlc << 'rock' << 'pop'
99
+ @tlc.to_s.should == "rock, pop"
100
+ end
101
+
102
+ it "should not add duplicate tags" do
103
+ @tlc << 'rock' << 'pop' << 'disco'
104
+ @sm.genres.size.should == 3
105
+ @tlc.should == ['rock', 'pop', 'disco']
106
+ @tlc << "rock"
107
+ @tlc.should == ['rock', 'pop', 'disco']
108
+ @sm.genres.size.should == 3
109
+ end
110
+
111
+ it ".replace" do
112
+ @tlc << 'rock' << 'pop' << 'disco'
113
+ @sm.genre_list.should == ['rock', 'pop', 'disco']
114
+ @sm.genre_list = ['funk', 'house']
115
+ @sm.genre_list.should == ['funk', 'house']
116
+ end
117
+
118
+ it ".to_param" do
119
+ @sm.genre_list = ['funk', 'house']
120
+ @sm.genre_list.to_param.should == 'funk/house'
121
+ end
122
+
123
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec'
2
+ require 'shoulda'
3
+
4
+ require 'active_support'
5
+ require 'active_record'
6
+
7
+ require 'scoped_tags'
8
+
9
+ Spec::Runner.configure do |config|
10
+ config.include(Shoulda::ActiveRecord::Matchers, :type => :model)
11
+ end
12
+
13
+ TEST_DATABASE_FILE = 'spec/test.sqlite3'
14
+
15
+ File.unlink(TEST_DATABASE_FILE) if File.exist?(TEST_DATABASE_FILE)
16
+ ActiveRecord::Base.establish_connection(
17
+ "adapter" => "sqlite3", "database" => TEST_DATABASE_FILE
18
+ )
19
+
20
+ load('schema.rb')
21
+
22
+ RAILS_DEFAULT_LOGGER = Logger.new("spec/debug.log")
23
+
24
+ class ScopedTaggedModel < ActiveRecord::Base
25
+ scoped_tags :genres
26
+ end
27
+
28
+ class UnScopedTaggedModel < ActiveRecord::Base
29
+ end
Binary file
@@ -0,0 +1 @@
1
+ # Uninstall hook code here
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: scoped-tags
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Josh Kalderimis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-18 00:00:00 +02:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activerecord
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.3.3
24
+ version:
25
+ description:
26
+ email: josh.kalderimis@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - MIT-LICENSE
35
+ - VERSION.yml
36
+ - generators/scoped_tags_migration/scoped_tags_generator.rb
37
+ - generators/scoped_tags_migration/templates/migration.rb
38
+ - install.rb
39
+ - lib/scoped_tags.rb
40
+ - lib/scoped_tags/active_record_additions.rb
41
+ - lib/scoped_tags/tag.rb
42
+ - lib/scoped_tags/tag_list_collection.rb
43
+ - lib/scoped_tags/tag_list_proxy.rb
44
+ - lib/scoped_tags/tagging.rb
45
+ - readme.md
46
+ - uninstall.rb
47
+ has_rdoc: true
48
+ homepage: http://github.com/joshk/scoped-tags
49
+ licenses: []
50
+
51
+ post_install_message:
52
+ rdoc_options:
53
+ - --charset=UTF-8
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: "0"
67
+ version:
68
+ requirements: []
69
+
70
+ rubyforge_project:
71
+ rubygems_version: 1.3.5
72
+ signing_key:
73
+ specification_version: 3
74
+ summary: Scoped tagging plugin for your rails models which keeps your associations in sync with your tag arrays
75
+ test_files:
76
+ - spec/debug.log
77
+ - spec/schema.rb
78
+ - spec/scoped_tags/scoped_tags_spec.rb
79
+ - spec/scoped_tags/tag_and_tagging_spec.rb
80
+ - spec/scoped_tags/tag_list_spec.rb
81
+ - spec/spec_helper.rb
82
+ - spec/test.sqlite3