dm-taggings 0.11.0

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