dm-taggings 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ require 'pathname'
2
+
3
+ # Add all external dependencies for the plugin here
4
+ require 'dm-core'
5
+ require 'dm-constraints'
6
+ require 'dm-is-remixable'
7
+
8
+ # Require plugin-files
9
+
10
+ dir = Pathname(__FILE__).dirname.expand_path / 'dm-taggings' / 'is'
11
+
12
+ require dir / 'taggable.rb'
13
+ require dir / 'tag.rb'
14
+ require dir / 'tagging.rb'
15
+ require dir / 'tagger.rb'
16
+
17
+ # Include the plugin in Resource
18
+ DataMapper::Model.append_extensions DataMapper::Is::Taggable
19
+ DataMapper::Model.append_extensions DataMapper::Is::Tagger
@@ -0,0 +1,42 @@
1
+ class Tag
2
+ include DataMapper::Resource
3
+
4
+ property :id, Serial
5
+ property :name, String, :required => true, :unique => true
6
+
7
+ # Shortcut to build method
8
+ #
9
+ # @see Tag.build
10
+ #
11
+ # @api public
12
+ def self.[](name)
13
+ build(name)
14
+ end
15
+
16
+ # Find or create a tag with the give name
17
+ #
18
+ # @param [String] name
19
+ # A name of a tag
20
+ #
21
+ # @return [DataMapper::Resource]
22
+ # A tag resource instance
23
+ #
24
+ # @api public
25
+ def self.build(name)
26
+ Tag.first_or_create(:name => name.strip) if name
27
+ end
28
+
29
+ # An overridden name attribute setter that strips the value
30
+ #
31
+ # @param [String] value
32
+ # A value to be set as the tag name
33
+ #
34
+ # @return [String]
35
+ # The name
36
+ #
37
+ # @api public
38
+ def name=(value)
39
+ super(value.strip) if value
40
+ name
41
+ end
42
+ end
@@ -0,0 +1,245 @@
1
+ module DataMapper
2
+ module Is
3
+ module Taggable
4
+
5
+ # Make a resource taggable
6
+ #
7
+ # @example
8
+ #
9
+ # class Post
10
+ # include DataMapper::Resource
11
+ #
12
+ # property :id, Serial
13
+ # property :title, String
14
+ # property :content, Text
15
+ #
16
+ # is :taggable
17
+ # end
18
+ #
19
+ # @param [Hash] options(optional)
20
+ # A hash with options
21
+ # @option options [Array] :by
22
+ # A list of DataMapper models that should become taggers
23
+ #
24
+ # @api public
25
+ def is_taggable(options={})
26
+
27
+ # Add class-methods
28
+ extend DataMapper::Is::Taggable::ClassMethods
29
+ # Add instance-methods
30
+ include DataMapper::Is::Taggable::InstanceMethods
31
+
32
+ class << self
33
+ attr_reader :tagging_parent_name, :tagging_relationship_name, :tagging_relationship,
34
+ :tagging_class, :taggable_relationship_name
35
+ end
36
+
37
+ # Make the magic happen
38
+ options[:by] ||= []
39
+
40
+ remix n, :taggings
41
+
42
+ @tagging_parent_name = DataMapper::Inflector.underscore(name).to_sym
43
+ @tagging_relationship_name = "#{@tagging_parent_name}_tags".to_sym
44
+ @tagging_relationship = relationships[@tagging_relationship_name]
45
+ @tagging_class = @tagging_relationship.child_model
46
+
47
+ @taggable_relationship_name = DataMapper::Inflector.underscore(name).pluralize.to_sym
48
+
49
+ @tagging_relationship.add_constraint_option(
50
+ @taggable_relationship_name, @tagging_class, self, :constraint => :destroy!)
51
+
52
+ tagging_parent_name = @tagging_parent_name
53
+
54
+ enhance :taggings do
55
+ belongs_to :tag
56
+ belongs_to tagging_parent_name
57
+
58
+ options[:by].each do |tagger_class|
59
+ belongs_to DataMapper::Inflector.underscore(tagger_class.name), :required => false
60
+ end
61
+ end
62
+
63
+ has n, :tags, :through => @tagging_relationship_name, :constraint => :destroy!
64
+
65
+ Tag.has n, @tagging_relationship_name, :constraint => :destroy!
66
+ Tag.has n, @taggable_relationship_name, :through => @tagging_relationship_name
67
+
68
+ options[:by].each do |tagger_class|
69
+ tagger_class.is :tagger, :for => [self]
70
+ end
71
+ end
72
+
73
+ module ClassMethods
74
+ # @attr_reader [String] tagging_parent_name
75
+ # @attr_reader [String] tagging_relationship_name
76
+ # @attr_reader [DataMapper::Associations::OneToMany::Relationship] tagging_relationship
77
+ # @attr_reader [DataMapper::Resource] tagging_class
78
+ # @attr_reader [String] taggable_relationship_name
79
+
80
+ # @api public
81
+ def taggable?
82
+ true
83
+ end
84
+
85
+ # Return all the taggable resources that are tagged with the given list of tags.
86
+ #
87
+ # Can be chained, for instance:
88
+ #
89
+ # Post.tagged_with(["foo", "bar"]).all(:created_at.lt => 1.day.ago)
90
+ #
91
+ # @param [Array] tags_or_names
92
+ # A list of either tag resources or tag names
93
+ #
94
+ # @return [DataMapper::Collection]
95
+ # A collection of taggables
96
+ #
97
+ # @api public
98
+ def tagged_with(tags_or_names)
99
+ tags_or_names = [tags_or_names] unless tags_or_names.kind_of?(Array)
100
+
101
+ tag_ids = if tags_or_names.all? { |tag| tag.kind_of?(Tag) }
102
+ tags_or_names
103
+ else
104
+ Tag.all(:name => tags_or_names)
105
+ end.map { |tag| tag.id }
106
+
107
+ all("#{tagging_relationship_name}.tag_id" => tag_ids)
108
+ end
109
+ end # ClassMethods
110
+
111
+ module InstanceMethods
112
+ # Add tags to a resource but do not persist them.
113
+ #
114
+ # @param [Array] tags_or_names
115
+ # A list of either tag resources or tag names
116
+ #
117
+ # @return [DataMapper::Associations::OneToMany::Collection]
118
+ # A DataMapper collection of resource's tags
119
+ #
120
+ # @api public
121
+ def tag(tags_or_names)
122
+ tags = extract_tags_from_names(tags_or_names)
123
+
124
+ tags.each do |tag|
125
+ next if self.tags.include?(tag)
126
+ taggings.new(:tag => tag)
127
+ end
128
+
129
+ taggings
130
+ end
131
+
132
+ # Add tags to a resource and persists them.
133
+ #
134
+ # @param [Array] tags_or_names
135
+ # A list of either tag resources or tag names
136
+ #
137
+ # @return [DataMapper::Associations::OneToMany::Collection]
138
+ # A DataMapper collection of resource's tags
139
+ #
140
+ # @api public
141
+ def tag!(tags_or_names)
142
+ taggings = tag(tags_or_names)
143
+ taggings.save! unless new?
144
+ taggings
145
+ end
146
+
147
+ # Delete given tags from a resource collection without actually deleting
148
+ # them from the datastore. Everything will be deleted if no tags are given.
149
+ #
150
+ # @param [Array] tags_or_names (optional)
151
+ # A list of either tag resources or tag names
152
+ #
153
+ # @return [DataMapper::Associations::OneToMany::Collection]
154
+ # A DataMapper collection of resource's tags
155
+ #
156
+ # @api public
157
+ def untag(tags_or_names=nil)
158
+ tags = extract_tags_from_names(tags_or_names) if tags_or_names
159
+
160
+ taggings_to_destroy = if tags.blank?
161
+ taggings.all
162
+ else
163
+ taggings.all(:tag => tags)
164
+ end
165
+
166
+ self.taggings = taggings - taggings_to_destroy
167
+
168
+ taggings_to_destroy
169
+ end
170
+
171
+ # Same as untag but actually delete the tags from the datastore.
172
+ #
173
+ # @param [Array] tags_or_names (optional)
174
+ # A list of either tag resources or tag names
175
+ #
176
+ # @return [DataMapper::Associations::OneToMany::Collection]
177
+ # A DataMapper collection of resource's tags
178
+ #
179
+ # @api public
180
+ def untag!(tags_or_names=nil)
181
+ taggings_to_destroy = untag(tags_or_names)
182
+ taggings_to_destroy.destroy! unless new?
183
+ taggings_to_destroy
184
+ end
185
+
186
+ # Return a string representation of tags collection
187
+ #
188
+ # @return [String]
189
+ # A tag list separated by commas
190
+ #
191
+ # @api public
192
+ def tag_list
193
+ @tag_list ||= tags.collect { |tag| tag.name }.join(", ")
194
+ end
195
+
196
+ # Tag a resource using tag names from the give list separated by commas.
197
+ #
198
+ # @param [String]
199
+ # A tag list separated by commas
200
+ #
201
+ # @return [DataMapper::Associations::OneToMany::Collection]
202
+ # A DataMapper collection of resource's tags
203
+ def tag_list=(list)
204
+ @tag_list = list
205
+
206
+ tag_names = list.split(",").map { |name| name.blank? ? nil : name.strip }.compact
207
+
208
+ old_tag_names = taggings.map { |tagging| tagging.tag.name } - tag_names
209
+
210
+ untag!(old_tag_names)
211
+ tag(tag_names)
212
+ end
213
+
214
+ # @api public
215
+ def reload
216
+ @tag_list = nil
217
+ super
218
+ end
219
+
220
+ # @api public
221
+ def taggings
222
+ send(self.class.tagging_relationship_name)
223
+ end
224
+
225
+ # @api public
226
+ def taggings=(taggings)
227
+ send("#{self.class.tagging_relationship_name}=", taggings)
228
+ end
229
+
230
+ protected
231
+
232
+ # @api private
233
+ def extract_tags_from_names(tags_or_names)
234
+ tags_or_names = [tags_or_names] unless tags_or_names.kind_of?(Array)
235
+
236
+ tags_or_names.map do |tag_or_name|
237
+ tag_or_name.kind_of?(Tag) ? tag_or_name : Tag[tag_or_name]
238
+ end
239
+ end
240
+ end # InstanceMethods
241
+
242
+ end # Taggable
243
+ end # Is
244
+ end # DataMapper
245
+
@@ -0,0 +1,115 @@
1
+ module DataMapper
2
+ module Is
3
+ module Tagger
4
+ # Set up a resource as tagger
5
+ #
6
+ # @example
7
+ #
8
+ # class Song
9
+ # include DataMapper::Resource
10
+ #
11
+ # property :id, Serial
12
+ # property :title, String
13
+ #
14
+ # is :taggable, :by => [ User ]
15
+ # end
16
+ #
17
+ # class User
18
+ # include DataMapper::Resource
19
+ #
20
+ # property :id, Serial
21
+ # property :name, String
22
+ #
23
+ # is :tagger, :for => [ Song ]
24
+ # end
25
+ #
26
+ # @param [Hash] options
27
+ # A hash of options
28
+ # @option options [Array] :for
29
+ # A list of DataMapper taggable models
30
+ #
31
+ # @return [Array]
32
+ # A list of DataMapper taggable models
33
+ #
34
+ # @api public
35
+ def is_tagger(options={})
36
+ unless self.respond_to?(:tagger?)
37
+ # Add class-methods
38
+ extend DataMapper::Is::Tagger::ClassMethods
39
+
40
+ # Add instance-methods
41
+ include DataMapper::Is::Tagger::InstanceMethods
42
+
43
+ cattr_accessor(:taggable_object_classes)
44
+ self.taggable_object_classes = []
45
+ end
46
+
47
+ raise "options[:for] is missing" unless options[:for]
48
+
49
+ add_taggable_object_classes(options[:for])
50
+ end
51
+
52
+ module ClassMethods
53
+ # Return if a model is tagger
54
+ #
55
+ # @return [TrueClass]
56
+ # true
57
+ #
58
+ # @api public
59
+ def tagger?
60
+ true
61
+ end
62
+
63
+ # Register new taggables and set up relationships
64
+ #
65
+ # @param [Array] taggable_object_classes
66
+ # An array of taggable DataMapper models
67
+ #
68
+ # @api public
69
+ def add_taggable_object_classes(taggable_object_classes)
70
+ taggable_object_classes.each do |taggable_object_class|
71
+ self.taggable_object_classes << taggable_object_class
72
+
73
+ has n, taggable_object_class.tagging_relationship_name,
74
+ :constraint => :destroy
75
+
76
+ has n, taggable_object_class.taggable_relationship_name,
77
+ :through => taggable_object_class.tagging_relationship_name,
78
+ :constraint => :destroy
79
+ end
80
+ end
81
+ end # ClassMethods
82
+
83
+ module InstanceMethods
84
+ # Tag a resource
85
+ #
86
+ # @param [DataMapper::Resource]
87
+ # An instance of a taggable resource
88
+ #
89
+ # @param [Hash] options (optional)
90
+ # A hash with options
91
+ #
92
+ # @return [DataMapper::Collection]
93
+ # A collection of tags that were associated with the resource
94
+ #
95
+ # @api public
96
+ def tag!(taggable, options={})
97
+ unless self.taggable_object_classes.include?(taggable.class)
98
+ raise "Object of type #{taggable.class} isn't taggable!"
99
+ end
100
+
101
+ tags = options[:with]
102
+ tags = [tags] unless tags.kind_of?(Array)
103
+
104
+ tags.each do |tag|
105
+ taggable.taggings.create(:tag => tag, :tagger => self)
106
+ end
107
+
108
+ tags
109
+ end
110
+ end # InstanceMethods
111
+
112
+ end # Tagger
113
+ end # Is
114
+ end # DataMapper
115
+
@@ -0,0 +1,13 @@
1
+ module Tagging
2
+ include DataMapper::Resource
3
+
4
+ property :id, Serial
5
+ property :tag_id, Integer, :min => 1, :required => true
6
+
7
+ is :remixable, :suffix => "tag"
8
+
9
+ def tagger=(tagger)
10
+ send("#{DataMapper::Inflector.underscore(tagger.class.name).to_sym}=", tagger)
11
+ end
12
+ end
13
+
@@ -0,0 +1,7 @@
1
+ module DataMapper
2
+ module Is
3
+ module Taggable
4
+ VERSION = File.read(File.expand_path("../../../../VERSION", __FILE__)).strip
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,259 @@
1
+ share_examples_for 'A taggable resource' do
2
+ def create_taggable(attrs={})
3
+ @taggable.create(@taggable_attributes.merge(attrs))
4
+ end
5
+
6
+ before :all do
7
+ %w[ @taggable ].each do |ivar|
8
+ raise "+#{ivar}+ should be defined in before block" unless instance_variable_defined?(ivar)
9
+ end
10
+
11
+ @taggable_attributes ||= {}
12
+
13
+ @foo_tag = Tag["foo"]
14
+ @bar_tag = Tag["bar"]
15
+
16
+ @tags = [@foo_tag, @bar_tag]
17
+ end
18
+
19
+ describe "public class methods" do
20
+ subject { @taggable }
21
+
22
+ it { should respond_to(:is_taggable) }
23
+ it { should respond_to(:taggable?) }
24
+ it { should respond_to(:tagged_with) }
25
+ it { should respond_to(:tagging_relationship_name) }
26
+ it { should respond_to(:tagging_relationship) }
27
+ it { should respond_to(:tagging_class) }
28
+ it { should respond_to(:tagging_parent_name) }
29
+ it { should respond_to(:taggable_relationship_name) }
30
+
31
+ describe ".taggable?" do
32
+ it "should return true" do
33
+ @taggable.taggable?.should be(true)
34
+ end
35
+ end
36
+
37
+ describe "relationships" do
38
+ subject { @taggable.relationships }
39
+
40
+ it { should have_key(@taggable.tagging_relationship_name) }
41
+
42
+ describe "tagging constraint" do
43
+ subject { @taggable.tagging_relationship.constraint }
44
+ it { subject.should eql(:destroy!) }
45
+ end
46
+ end
47
+
48
+ describe ".tagged_with" do
49
+ before :all do
50
+ @resource_one = create_taggable(:tag_list => "red, green, blue")
51
+ @resource_two = create_taggable(:tag_list => "orange, yellow")
52
+ end
53
+
54
+ it "should return correct resources" do
55
+ result = @taggable.tagged_with(["red", "yellow", "purple"])
56
+ result.size.should eql(2)
57
+ result.should include(@resource_one, @resource_two)
58
+ end
59
+ end
60
+ end
61
+
62
+ describe "public instance methods" do
63
+ subject { @taggable.new }
64
+
65
+ it { should respond_to(:tag) }
66
+ it { should respond_to(:tag!) }
67
+ it { should respond_to(:untag) }
68
+ it { should respond_to(:untag!) }
69
+ it { should respond_to(:tag_list) }
70
+ it { should respond_to(:taggings) }
71
+
72
+ describe ".tag" do
73
+ before :all do
74
+ @resource = create_taggable
75
+ @taggings = @resource.tag([@foo_tag, @bar_tag])
76
+ end
77
+
78
+ it "should set new taggings" do
79
+ @taggings.should eql(@resource.taggings)
80
+ end
81
+
82
+ it "should not create new taggings" do
83
+ @resource.tags.should be_empty
84
+ end
85
+ end
86
+
87
+ describe ".tag!" do
88
+ before :all do
89
+ @resource = create_taggable
90
+ @taggings = @resource.tag!([@foo_tag, @bar_tag])
91
+ end
92
+
93
+ it "should create new taggings" do
94
+ @resource.reload.tags.should include(@foo_tag, @bar_tag)
95
+ end
96
+ end
97
+
98
+ describe ".untag" do
99
+ describe "all" do
100
+ before :all do
101
+ @resource = create_taggable
102
+ @taggings = @resource.tag!([@foo_tag, @bar_tag])
103
+
104
+ @resource.untag
105
+ end
106
+
107
+ it "should remove the taggings from the collection" do
108
+ @resource.taggings.should be_empty
109
+ end
110
+
111
+ it "should not destroy the taggings" do
112
+ @resource.reload.tags.should_not be_empty
113
+ end
114
+ end
115
+
116
+ describe "specific names" do
117
+ before :all do
118
+ @resource = create_taggable
119
+ @taggings = @resource.tag!([@foo_tag, @bar_tag])
120
+
121
+ @resource.untag([@foo_tag])
122
+ end
123
+
124
+ it "should remove the related tagging from the collection" do
125
+ @resource.taggings.size.should eql(1)
126
+ end
127
+
128
+ it "should remove the related tag" do
129
+ @resource.tags.should_not include(@foo_tag)
130
+ end
131
+ end
132
+
133
+ describe "when save is called" do
134
+ before :all do
135
+ @resource = create_taggable
136
+ @taggings = @resource.tag!([@foo_tag, @bar_tag])
137
+
138
+ @resource.untag
139
+ end
140
+
141
+ it "should return true" do
142
+ pending "Currently DataMapper doesn't support saving an empty collection" do
143
+ @resource.save.should be(true)
144
+ end
145
+ end
146
+
147
+ it "should destroy taggings" do
148
+ pending "Currently DataMapper doesn't support saving an empty collection" do
149
+ @resource.reload.taggings.should be_empty
150
+ end
151
+ end
152
+
153
+ it "should destroy tags" do
154
+ pending "Currently DataMapper doesn't support saving an empty collection" do
155
+ @resource.reload.tags.should be_empty
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ describe ".untag!" do
162
+ describe "all" do
163
+ before :all do
164
+ @resource = create_taggable
165
+ @taggings = @resource.tag!([@foo_tag, @bar_tag])
166
+
167
+ @resource.untag!
168
+ end
169
+
170
+ it "should destroy the taggings" do
171
+ @resource.reload.taggings.should be_empty
172
+ end
173
+ end
174
+
175
+ describe "specific names" do
176
+ before :all do
177
+ @resource = create_taggable
178
+ @taggings = @resource.tag!([@foo_tag, @bar_tag])
179
+
180
+ @resource.untag!([@foo_tag])
181
+ @resource.reload
182
+ end
183
+
184
+ subject { @resource.tags }
185
+
186
+ it { should_not include(@foo_tag) }
187
+ it { should include(@bar_tag) }
188
+ end
189
+ end
190
+
191
+ describe ".tag_list=" do
192
+ describe "with a list of tag names" do
193
+ describe "with blank values" do
194
+ before :all do
195
+ @resource = create_taggable(:tag_list => "foo, , ,bar, , ")
196
+ end
197
+
198
+ it "should add new tags and reject blank names" do
199
+ @resource.reload.tags.should include(Tag["foo"], Tag["bar"])
200
+ end
201
+ end
202
+
203
+ describe "when tags are removed and added" do
204
+ before :all do
205
+ @resource = create_taggable(:tag_list => "foo, bar")
206
+ @resource.update(:tag_list => "foo, bar, pub")
207
+ end
208
+
209
+ it "should add new tags" do
210
+ @resource.reload.tags.should include(Tag["bar"], Tag["bar"], Tag["pub"])
211
+ end
212
+ end
213
+
214
+ describe "when tags are added" do
215
+ before :all do
216
+ @resource = create_taggable(:tag_list => "foo, bar")
217
+ @resource.update(:tag_list => "bar, pub")
218
+ end
219
+
220
+ it "should add new tags" do
221
+ @resource.reload.tags.should include(Tag["bar"], Tag["pub"])
222
+ end
223
+
224
+ it "should remove tags" do
225
+ @resource.reload.tags.should_not include(Tag["foo"])
226
+ end
227
+ end
228
+ end
229
+
230
+ describe "when no list of tag names is given" do
231
+ before :all do
232
+ @resource = create_taggable(:tag_list => "foo, bar")
233
+ @resource.update(:tag_list => "")
234
+ end
235
+
236
+ it "should destroy taggings" do
237
+ @resource.reload.taggings.should be_blank
238
+ end
239
+
240
+ it "should remove the tags" do
241
+ @resource.reload.tags.should be_blank
242
+ end
243
+ end
244
+ end
245
+
246
+ describe ".tag_list" do
247
+ before :all do
248
+ @tag_names = %w(red green blue)
249
+ @expected = @tag_names.join(', ')
250
+ @resource = create_taggable(:tag_list => @expected)
251
+ end
252
+
253
+ it "should return the list of tag names" do
254
+ @resource.tag_list.should eql(@expected)
255
+ end
256
+ end
257
+ end
258
+ end
259
+