genki-dm-is-taggable 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1 @@
1
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Aaron Qian, Maxime Guilbot at Ekohe
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.
data/Manifest.txt ADDED
@@ -0,0 +1,25 @@
1
+ History.txt
2
+ LICENSE
3
+ Manifest.txt
4
+ README.textile
5
+ Rakefile
6
+ TODO
7
+ lib/dm-is-taggable.rb
8
+ lib/dm-is-taggable/aggregate_patch.rb
9
+ lib/dm-is-taggable/tag.rb
10
+ lib/dm-is-taggable/tagging.rb
11
+ lib/dm-is-taggable/tag_list.rb
12
+ lib/dm-is-taggable/is/taggable.rb
13
+ lib/dm-is-taggable/is/version.rb
14
+ lib/dm-is-taggable/is/shared.rb
15
+ lib/dm-is-taggable/is/tag.rb
16
+ lib/dm-is-taggable/is/tagging.rb
17
+ lib/dm-is-taggable/is/tagger.rb
18
+ spec/integration/taggable_spec.rb
19
+ spec/integration/tagger_similarity_spec.rb
20
+ spec/data/article.rb
21
+ spec/data/picture.rb
22
+ spec/data/bot.rb
23
+ spec/data/user.rb
24
+ spec/spec.opts
25
+ spec/spec_helper.rb
data/README.textile ADDED
@@ -0,0 +1,185 @@
1
+ h1. dm-is-taggable
2
+
3
+ dm-is-taggable is a tagging system built for datamapper. It has supports for multiple tagger types and taggable types.
4
+ Each tagger can tag different taggable objects.
5
+
6
+
7
+ h2. Installation
8
+
9
+ h3. Download the plugin
10
+
11
+ In your console:
12
+ <pre><code>
13
+ git clone git://github.com/aq1018/dm-is-taggable.git
14
+ </code></pre>
15
+
16
+ h3. Install the gem
17
+
18
+ In your console:
19
+ <pre><code>
20
+ cd dm-is-taggable
21
+ sudo rake install
22
+ </code></pre>
23
+
24
+ h3. Include it Merb
25
+
26
+ In merb init.rb:
27
+
28
+ <pre><code>
29
+ dependency "dm-is-taggable"
30
+ </code></pre>
31
+
32
+
33
+ h2. Using dm-is-taggable in your code
34
+
35
+ h3. Define taggers
36
+
37
+ <pre><code>
38
+ class User
39
+ include DataMapper::Resource
40
+ property :id, Serial
41
+ property :name, String
42
+ is :tagger, :on => ["Article", "Picture"]
43
+ end
44
+
45
+ class Bot
46
+ include DataMapper::Resource
47
+ property :id, Serial
48
+ property :name, String
49
+ is :tagger, :on => ["Article", "Picture"]
50
+ end
51
+ </code></pre>
52
+
53
+ h3. Define taggables
54
+
55
+ <pre><code>
56
+ class Article
57
+ include DataMapper::Resource
58
+ property :id, Serial
59
+ is :taggable, :by => ["User", "Bot"]
60
+ end
61
+
62
+ class Picture
63
+ include DataMapper::Resource
64
+ property :id, Serial
65
+ is :taggable, :by => ["User", "Bot"]
66
+ end
67
+ </code></pre>
68
+
69
+ h3. Create tags
70
+
71
+ <pre><code>
72
+
73
+ @picture = Picture.first
74
+ @scott = User.first
75
+
76
+ # You can tag like this
77
+ @picture.tag(:with => "shanghai, bar, beer", :by => @scott)
78
+
79
+ # or like this
80
+ # Note: this doesn't remove the previous tags
81
+ Tag.as(@scott) do
82
+ @picture.tag(:with => "cool, tag1, tag2")
83
+ end
84
+
85
+ # or like this
86
+ # Note, this removes all previous tags
87
+ Tag.as(@scott) do
88
+ @picture.taglist("cool, tag1, tag2")
89
+ end
90
+ </code></pre>
91
+
92
+ h3. Retrieve objects with tags
93
+
94
+ <pre><code>
95
+
96
+ # find pictures tagged with tag1 and tag2
97
+ Picture.find(:with => "tag1, tag2")
98
+
99
+ # find pictures tagged with tag1 or tag2
100
+ Picture.find(:with => "tag1, tag2", :match => :any)
101
+
102
+ # find pictures tagged with tag1 or tag2, tagged by @user1
103
+ Picture.find(:with => "tag1, tag2", :match => :any, :by => @user1)
104
+
105
+ # find pictures tagged with tag1 or tag2, tagged by all users
106
+ Picture.find(:with => "tag1, tag2", :match => :any, :by => User)
107
+
108
+ # or you can do scoped way
109
+
110
+ # find pictures tagged with tag1 or tag2, tagged by all users
111
+ Tag.as(User) do
112
+ Picture.find(:with => "tag1, tag2", :match => :any)
113
+ end
114
+
115
+ # find pictures tagged with tag1 or tag2, tagged by @user1
116
+ Tag.as(@user1) do
117
+ Picture.find(:with => "tag1, tag2", :match => :any)
118
+ end
119
+
120
+
121
+ # You can tag like this
122
+ @picture.tag(:with => "shanghai, bar, beer", :by => @scott)
123
+
124
+ # or like this
125
+ # Note: this doesn't remove the previous tags
126
+ Tag.as(@scott) do
127
+ @picture.tag(:with => "cool, tag1, tag2")
128
+ end
129
+
130
+ # or like this
131
+ # Note, this removes all previous tags
132
+ Tag.as(@scott) do
133
+ @picture.taglist("cool, tag1, tag2")
134
+ end
135
+ </code></pre>
136
+
137
+ h3. Retrieve tags with objects
138
+
139
+ <pre><code>
140
+
141
+ @picture1 = Picture.first
142
+
143
+ # get all tags associated with @picture2 as a string
144
+ @picture1.taglist
145
+
146
+ # or as tag objects
147
+ @picture1.tags
148
+
149
+ # tags tagged by users
150
+ @picture1.tags_by_users
151
+
152
+ # find tags by all users
153
+ Tag.by(User)
154
+
155
+ # find tags by a user
156
+ Tag.by(@user)
157
+
158
+ # find tags on all pictures
159
+ Tag.on(Picture)
160
+
161
+ # find tags on a picture
162
+ Tag.on(@picture1)
163
+
164
+ # find tags by a user, on all pictures
165
+ Tag.by(@user).on(Pictures)
166
+
167
+ # find tags by all users on a picture
168
+ Tag.by(User).on(@picture)
169
+
170
+ # find tags by a user on a picture
171
+ Tag.by(@user).on(@picture)
172
+
173
+ </code></pre>
174
+
175
+ h3. Counting tags
176
+
177
+ <pre><code>
178
+
179
+ # Count how many articles are tagged by @user1
180
+ Tag.tagged_count(:by => @user1, :on => Article)
181
+
182
+ # Count how many articles are tagged by @user1 with "tag1"
183
+ Tag.tagged_count(:by => @user1, :on => Article, :with => "tag1")
184
+
185
+ </code></pre>
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require 'spec/rake/spectask'
4
+ require 'pathname'
5
+
6
+ ROOT = Pathname(__FILE__).dirname.expand_path
7
+ require ROOT + 'lib/dm-is-taggable/is/version'
8
+
9
+ AUTHOR = "Aaron Qian, Maxime Guilbot"
10
+ EMAIL = "team [a] ekohe [d] com"
11
+ GEM_NAME = "dm-is-taggable"
12
+ GEM_VERSION = DataMapper::Is::Taggable::VERSION
13
+ GEM_DEPENDENCY_VERSION = DataMapper::Is::Taggable::DEPENDENCY_VERSION
14
+ GEM_DEPENDENCIES = [["dm-core", GEM_DEPENDENCY_VERSION], ["dm-aggregates", GEM_DEPENDENCY_VERSION]]
15
+ GEM_CLEAN = ["log", "pkg"]
16
+ GEM_EXTRAS = { :has_rdoc => false, :extra_rdoc_files => %w[ README.textile LICENSE TODO ] }
17
+
18
+ PROJECT_NAME = "dm-is-taggable"
19
+ PROJECT_URL = "http://github.com/aq1018/dm-is-taggable"
20
+ PROJECT_DESCRIPTION = PROJECT_SUMMARY = "Tagging implementation for DataMapper"
21
+
22
+ require 'tasks/hoe'
23
+
24
+ task :default => [ :spec ]
25
+
26
+ WIN32 = (RUBY_PLATFORM =~ /win32|mingw|cygwin/) rescue nil
27
+ SUDO = WIN32 ? '' : ('sudo' unless ENV['SUDOLESS'])
28
+
29
+ desc "Install #{GEM_NAME} #{GEM_VERSION}"
30
+ task :install => [ :package ] do
31
+ sh "#{SUDO} gem install --local pkg/#{GEM_NAME}-#{GEM_VERSION} --no-update-sources", :verbose => false
32
+ end
33
+
34
+ desc "Uninstall #{GEM_NAME} #{GEM_VERSION} (default ruby)"
35
+ task :uninstall => [ :clobber ] do
36
+ sh "#{SUDO} gem uninstall #{GEM_NAME} -v#{GEM_VERSION} -I -x", :verbose => false
37
+ end
38
+
39
+ desc 'Run specifications'
40
+ Spec::Rake::SpecTask.new(:spec) do |t|
41
+ t.spec_opts << '--options' << 'spec/spec.opts' if File.exists?('spec/spec.opts')
42
+ t.spec_files = Pathname.glob(Pathname.new(__FILE__).dirname + 'spec/**/*_spec.rb')
43
+
44
+ begin
45
+ t.rcov = ENV.has_key?('NO_RCOV') ? ENV['NO_RCOV'] != 'true' : true
46
+ t.rcov_opts << '--exclude' << 'spec'
47
+ t.rcov_opts << '--text-summary'
48
+ t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse'
49
+ rescue Exception
50
+ # rcov not installed
51
+ end
52
+ end
data/TODO ADDED
@@ -0,0 +1,2 @@
1
+ TODO
2
+ ====
@@ -0,0 +1,31 @@
1
+ # Needed to import datamapper and other gems
2
+ require 'rubygems'
3
+ require 'pathname'
4
+
5
+ # Add all external dependencies for the plugin here
6
+ gem 'dm-core', '=0.9.6'
7
+ gem 'dm-aggregates', '>=0.9.6'
8
+ require 'dm-core'
9
+ require 'dm-aggregates'
10
+
11
+ # Require plugin-files
12
+ require Pathname(__FILE__).dirname.expand_path / 'dm-is-taggable' / 'tag_list.rb'
13
+ require Pathname(__FILE__).dirname.expand_path / 'dm-is-taggable' / 'aggregate_patch.rb'
14
+ require Pathname(__FILE__).dirname.expand_path / 'dm-is-taggable' / 'is' / 'shared.rb'
15
+ require Pathname(__FILE__).dirname.expand_path / 'dm-is-taggable' / 'is' / 'taggable.rb'
16
+ require Pathname(__FILE__).dirname.expand_path / 'dm-is-taggable' / 'is' / 'tag.rb'
17
+ require Pathname(__FILE__).dirname.expand_path / 'dm-is-taggable' / 'is' / 'tagging.rb'
18
+ require Pathname(__FILE__).dirname.expand_path / 'dm-is-taggable' / 'is' / 'tagger.rb'
19
+
20
+ # Include the plugin in Resource
21
+ module DataMapper
22
+ module Resource
23
+ module ClassMethods
24
+ include DataMapper::Is::Taggable
25
+ end # module ClassMethods
26
+ end # module Resource
27
+ end # module DataMapper
28
+
29
+ # Require the Tag and Tagging Resources
30
+ require Pathname(__FILE__).dirname.expand_path / 'dm-is-taggable' / 'tag.rb'
31
+ require Pathname(__FILE__).dirname.expand_path / 'dm-is-taggable' / 'tagging.rb'
@@ -0,0 +1,47 @@
1
+ # we need this unless someone adds the "count(distinct id)" support in dm-aggegrates
2
+ module DataMapper
3
+ module Adapters
4
+ class DataObjectsAdapter
5
+ module SQL
6
+ private
7
+ def fields_statement(query)
8
+ qualify = query.links.any?
9
+ query.fields.map { |p| property_to_column_name(query.repository, p, qualify, query.unique?) } * ', '
10
+ end
11
+
12
+ def property_to_column_name(repository, property, qualify, unique=false)
13
+ case property
14
+ when Query::Operator
15
+ aggregate_field_statement(repository, property.operator, property.target, qualify, unique)
16
+ when Property
17
+ original_property_to_column_name(repository, property, qualify)
18
+ when Query::Path
19
+ original_property_to_column_name(repository, property, qualify)
20
+ else
21
+ raise ArgumentError, "+property+ must be a DataMapper::Query::Operator or a DataMapper::Property, but was a #{property.class} (#{property.inspect})"
22
+ end
23
+ end
24
+
25
+ def aggregate_field_statement(repository, aggregate_function, property, qualify, unique=false)
26
+ column_name = if aggregate_function == :count && property == :all
27
+ '*'
28
+ else
29
+ unique ? "distinct #{property_to_column_name(repository, property, qualify)}" :
30
+ property_to_column_name(repository, property, qualify)
31
+ end
32
+
33
+ function_name = case aggregate_function
34
+ when :count then 'COUNT'
35
+ when :min then 'MIN'
36
+ when :max then 'MAX'
37
+ when :avg then 'AVG'
38
+ when :sum then 'SUM'
39
+ else raise "Invalid aggregate function: #{aggregate_function.inspect}"
40
+ end
41
+
42
+ "#{function_name}(#{column_name})"
43
+ end
44
+ end # module SQL
45
+ end # class DataObjectsAdapter
46
+ end # module Adapters
47
+ end # module DataMapper
@@ -0,0 +1,111 @@
1
+ module DataMapper
2
+ module Is
3
+ module Taggable
4
+ module SharedClassMethods
5
+
6
+ def extract_class_object(obj)
7
+ return [] if obj.nil?
8
+ if obj.is_a?(Class)
9
+ [obj, nil]
10
+ else
11
+ [obj.class, obj]
12
+ end
13
+ end
14
+
15
+ def by(tagger_class_or_obj)
16
+ tagger_class, tagger_obj = extract_tagger_class_object(tagger_class_or_obj)
17
+ query = {:unique => true}
18
+ query.merge!(Tag.taggings.tagger_type => tagger_class.to_s) if tagger_class
19
+ query.merge!(Tag.taggings.tagger_id => tagger_obj.id) if tagger_obj
20
+ all(query)
21
+ end
22
+
23
+ def on(taggable_class_or_obj)
24
+ taggable_class, taggable_obj = extract_taggable_class_object(taggable_class_or_obj)
25
+ query = {:unique => true}
26
+ query.merge!(Tag.taggings.taggable_type => taggable_class.to_s) if taggable_class
27
+ query.merge!(Tag.taggings.taggable_id => taggable_obj.id) if taggable_obj
28
+ all(query)
29
+ end
30
+
31
+ def extract_options(options)
32
+ if options.is_a?(Array) || options.is_a?(String)
33
+ options = {:with => TagList.from(options)}
34
+ end
35
+ options = options.dup
36
+
37
+ tagger = options.delete(:by)
38
+ tagger ||= Tag.tagger
39
+ taggable = options.delete(:on)
40
+ tags = TagList.from(options.delete(:with))
41
+ return [tagger, taggable, tags, options]
42
+ end
43
+
44
+ def extract_tagger_class_object(obj)
45
+ obj = Extlib::Inflection.constantize(obj.to_s.camel_case.singular) if obj && (obj.is_a?(String) || obj.is_a?(Symbol))
46
+ return [] if obj.nil?
47
+
48
+ if obj.tagger?
49
+ extract_class_object(obj)
50
+ else
51
+ raise Exception.new("#{obj} is not a Tagger class or object!")
52
+ end
53
+ end
54
+
55
+ def extract_taggable_class_object(obj)
56
+ obj = Extlib::Inflection.constantize(obj.to_s.camel_case.singular) if obj && (obj.is_a?(String) || obj.is_a?(Symbol))
57
+ return [] if obj.nil?
58
+
59
+ if obj.taggable?
60
+ extract_class_object(obj)
61
+ else
62
+ raise Exception.new("#{obj} is not a Taggable class or object!")
63
+ end
64
+ end
65
+
66
+ def create_taggings(tagger, taggable, tags=nil)
67
+ return if tags.nil? || tags.empty?
68
+
69
+ unless tagger.nil? || (tagger.respond_to?(:tagger?) && tagger.tagger? && !tagger.is_a?(Class))
70
+ raise Exception.new("#{tagger} is not a tagger datamapper resource object!")
71
+ end
72
+
73
+ unless taggable.respond_to?(:taggable?) && taggable.taggable? && !taggable.is_a?(Class)
74
+ raise Exception.new("#{taggable} is not a taggable datamapper resource object!")
75
+ end
76
+
77
+ raise Exception.new("#{tagger.class} cannot Tag #{taggable.class}") unless tagger.nil? || tagger.can_tag_on?(taggable)
78
+
79
+ TagList.from(tags).each do |tag|
80
+ tag_obj = Tag.fetch(tag)
81
+
82
+ # build tagging (tagger could be anonymous)
83
+ tagging_hash = {:tag_id => tag_obj.id, :taggable_id => taggable.id, :taggable_type => taggable.class.to_s}
84
+ tagging_hash.merge!(:tagger_id => tagger.id, :tagger_type => tagger.class.to_s) if tagger
85
+
86
+ # see if we already have this tagging
87
+ tagging_obj = Tagging.first(tagging_hash)
88
+
89
+ # if we have the tagging already, just skip this one...
90
+ next if tagging_obj
91
+
92
+ # tagging is not in db, let's create one
93
+ Tagging.create(tagging_hash)
94
+ end # end of each
95
+ end
96
+ end
97
+
98
+ module SharedInstanceMethods
99
+ def extract_tagger_class_object(tagger_class_or_object)
100
+ self.class.extract_tagger_class_object(tagger_class_or_object)
101
+ end
102
+ def extract_taggable_class_object(taggable_class_or_object)
103
+ self.class.extract_taggable_class_object(taggable_class_or_object)
104
+ end
105
+ def extract_options(options)
106
+ self.class.extract_options(options)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end