cldwalker-has_machine_tags 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +5 -1
- data/Rakefile +49 -0
- data/VERSION.yml +4 -0
- data/init.rb +5 -0
- data/lib/has_machine_tags/namespace_group.rb +146 -0
- data/lib/has_machine_tags/tag.rb +12 -6
- data/lib/has_machine_tags/tagging.rb +1 -0
- data/lib/has_machine_tags.rb +51 -15
- metadata +6 -2
data/README.rdoc
CHANGED
@@ -18,10 +18,14 @@ Install as a gem
|
|
18
18
|
|
19
19
|
bash> gem install cldwalker-has_machine_tags -s http://gems.github.com
|
20
20
|
|
21
|
+
# add in your environment.rb
|
22
|
+
config.gem "cldwalker-has_machine_tags", :source => "http://gems.github.com", :lib => "has_machine_tags"
|
23
|
+
|
21
24
|
Or as a plugin
|
22
25
|
|
23
26
|
bash> script/plugin install git://github.com/cldwalker/has_machine_tags.git
|
24
27
|
|
28
|
+
|
25
29
|
Migrate your database from Rails root:
|
26
30
|
|
27
31
|
bash> script/generate has_machine_tags_migration
|
@@ -128,7 +132,7 @@ these characters are off limit unless used in the machine tag context:
|
|
128
132
|
|
129
133
|
== Credits
|
130
134
|
|
131
|
-
Thanks goes to Flickr for
|
135
|
+
Thanks goes to Flickr for popularizing this tagging model.
|
132
136
|
Thanks also goes to the {acts-as-taggable-on plugin}[http://github.com/mbleigh/acts-as-taggable-on/tree/master]
|
133
137
|
for their finder code and the {is_taggable plugin}[http://github.com/giraffesoft/is_taggable/tree/master]
|
134
138
|
for demonstrating sane testing for a Rails plugin.
|
data/Rakefile
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
begin
|
5
|
+
require 'rcov/rcovtask'
|
6
|
+
|
7
|
+
Rcov::RcovTask.new do |t|
|
8
|
+
t.libs << 'test'
|
9
|
+
t.test_files = FileList['test/**/*_test.rb']
|
10
|
+
t.rcov_opts = ["-T -x '/Library/Ruby/*'"]
|
11
|
+
t.verbose = true
|
12
|
+
end
|
13
|
+
rescue LoadError
|
14
|
+
puts "Rcov not available. Install it for rcov-related tasks with: sudo gem install rcov"
|
15
|
+
end
|
16
|
+
|
17
|
+
begin
|
18
|
+
require 'jeweler'
|
19
|
+
Jeweler::Tasks.new do |s|
|
20
|
+
s.name = "has_machine_tags"
|
21
|
+
s.description = "A rails tagging plugin implementing flickr's machine tags + maybe more (semantic tags)"
|
22
|
+
s.summary = s.description
|
23
|
+
s.email = "gabriel.horner@gmail.com"
|
24
|
+
s.homepage = "http://github.com/cldwalker/has_machine_tags"
|
25
|
+
s.authors = ["Gabriel Horner"]
|
26
|
+
s.has_rdoc = true
|
27
|
+
s.extra_rdoc_files = ["README.rdoc", "LICENSE.txt"]
|
28
|
+
s.files = FileList["README.rdoc", "LICENSE.txt", "init.rb", "Rakefile", "VERSION.yml", "{generators,bin,lib,test}/**/*"]
|
29
|
+
end
|
30
|
+
|
31
|
+
rescue LoadError
|
32
|
+
puts "Jeweler not available. Install it for jeweler-related tasks with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
33
|
+
end
|
34
|
+
|
35
|
+
Rake::TestTask.new do |t|
|
36
|
+
t.libs << 'lib'
|
37
|
+
t.pattern = 'test/**/*_test.rb'
|
38
|
+
t.verbose = false
|
39
|
+
end
|
40
|
+
|
41
|
+
Rake::RDocTask.new do |rdoc|
|
42
|
+
rdoc.rdoc_dir = 'rdoc'
|
43
|
+
rdoc.title = 'test'
|
44
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
45
|
+
rdoc.rdoc_files.include('README*')
|
46
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
47
|
+
end
|
48
|
+
|
49
|
+
task :default => :test
|
data/VERSION.yml
ADDED
data/init.rb
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
module HasMachineTags
|
2
|
+
# These classes are used for querying machine tags and grouping results as outlines.
|
3
|
+
# These methods are for console use and may change quickly.
|
4
|
+
class NamespaceGroup #:nodoc:
|
5
|
+
def initialize(name, options={})
|
6
|
+
@name = name.to_s
|
7
|
+
@options = options
|
8
|
+
@tags = options[:tags] if options[:tags]
|
9
|
+
end
|
10
|
+
|
11
|
+
def tags
|
12
|
+
@tags ||= Tag.find_all_by_namespace(@name)
|
13
|
+
end
|
14
|
+
|
15
|
+
def predicates
|
16
|
+
tags.map(&:predicate)
|
17
|
+
end
|
18
|
+
|
19
|
+
def values
|
20
|
+
tags.map(&:value)
|
21
|
+
end
|
22
|
+
|
23
|
+
def predicate_map
|
24
|
+
tags.map {|e| [e.predicate, e.value] }.inject({}) {
|
25
|
+
|t, (k,v)| (t[k] ||=[]) << v; t
|
26
|
+
}
|
27
|
+
end
|
28
|
+
alias :pm :predicate_map
|
29
|
+
|
30
|
+
def value_count(pred,value)
|
31
|
+
Url.tagged_with("#{@name}:#{pred}=#{value}").count
|
32
|
+
end
|
33
|
+
|
34
|
+
def pred_count(pred)
|
35
|
+
(predicate_map[pred] ||[]).map {|e| [e, value_count(pred, e)]}
|
36
|
+
end
|
37
|
+
|
38
|
+
def group_pred_count(pred)
|
39
|
+
pred_count(pred).inject({}) {|hash,(k,v)|
|
40
|
+
(hash[v] ||= []) << k
|
41
|
+
hash
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def sort_group_pred_count(pred)
|
46
|
+
hash = group_pred_count(pred)
|
47
|
+
hash.keys.sort.reverse.map {|e|
|
48
|
+
[e, hash[e]]
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
def predicate_view(pred, view=nil)
|
53
|
+
if view == :group
|
54
|
+
sort_group_pred_count(pred).map {|k,v|
|
55
|
+
"#{k}: #{v.join(', ')}"
|
56
|
+
}
|
57
|
+
elsif view == :count
|
58
|
+
pred_count(pred).map {|k,v|
|
59
|
+
"#{k}: #{v}"
|
60
|
+
}
|
61
|
+
else
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def outline_view(type=nil)
|
67
|
+
body = [@name]
|
68
|
+
level_delim = "\t"
|
69
|
+
predicate_map.each do |pred, vals|
|
70
|
+
body << [level_delim + pred]
|
71
|
+
values = predicate_view(pred, type) || vals
|
72
|
+
values.each {|e|
|
73
|
+
body << level_delim * 2 + e
|
74
|
+
if type == :result
|
75
|
+
urls = Url.tagged_with("#{@name}:#{pred}=#{e}")
|
76
|
+
urls.each {|u|
|
77
|
+
body << level_delim * 3 + format_result(u)
|
78
|
+
}
|
79
|
+
end
|
80
|
+
}
|
81
|
+
end
|
82
|
+
body.join("\n")
|
83
|
+
end
|
84
|
+
|
85
|
+
def format_result(result)
|
86
|
+
"#{result.id}: #{result.name}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def inspect
|
90
|
+
outline_view(@options[:view])
|
91
|
+
end
|
92
|
+
|
93
|
+
def duplicate_values
|
94
|
+
array_duplicates(values)
|
95
|
+
end
|
96
|
+
|
97
|
+
def array_duplicates(array)
|
98
|
+
hash = array.inject({}) {|h,e|
|
99
|
+
h[e] ||= 0
|
100
|
+
h[e] += 1
|
101
|
+
h
|
102
|
+
}
|
103
|
+
hash.delete_if {|k,v| v<=1}
|
104
|
+
hash.keys
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
class TagGroup < NamespaceGroup #:nodoc:
|
110
|
+
def namespaces
|
111
|
+
tags.map(&:namespace).uniq
|
112
|
+
end
|
113
|
+
|
114
|
+
def outline_view(type=nil)
|
115
|
+
"\n" + namespace_groups.map {|e| e.outline_view(type) }.join("\n")
|
116
|
+
end
|
117
|
+
|
118
|
+
def inspect; super; end
|
119
|
+
|
120
|
+
def namespace_tags
|
121
|
+
tags.inject({}) {|h,t|
|
122
|
+
(h[t.namespace] ||= []) << t
|
123
|
+
h
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
def namespace_groups
|
128
|
+
unless @namespace_groups
|
129
|
+
@namespace_groups = namespace_tags.map {|name, tags|
|
130
|
+
NamespaceGroup.new(name, :tags=>tags)
|
131
|
+
}
|
132
|
+
end
|
133
|
+
@namespace_groups
|
134
|
+
end
|
135
|
+
|
136
|
+
def tags
|
137
|
+
@tags ||= Tag.machine_tags(@name)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class QueryGroup < TagGroup #:nodoc:
|
142
|
+
def tags
|
143
|
+
@tags ||= Url.tagged_with(@name).map(&:tags).flatten.uniq
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
data/lib/has_machine_tags/tag.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# == Machine Tags
|
2
|
-
# Machine tags are in the format:
|
2
|
+
# Machine tags, also known as triple tags, are in the format:
|
3
3
|
# [namespace]:[predicate]=[value]
|
4
4
|
#
|
5
5
|
# As explained here[http://www.flickr.com/groups/api/discuss/72157594497877875],
|
@@ -65,12 +65,13 @@
|
|
65
65
|
|
66
66
|
class Tag < ActiveRecord::Base
|
67
67
|
has_many :taggings
|
68
|
+
|
68
69
|
validates_presence_of :name
|
69
70
|
validates_uniqueness_of :name
|
70
71
|
before_save :update_name_related_columns
|
71
72
|
|
72
73
|
NAMESPACE_REGEX = "[a-z](?:[a-z0-9_]+)"
|
73
|
-
PREDICATE_REGEX = "[a-z](?:[a-z0-9_]+)"
|
74
|
+
PREDICATE_REGEX = "[a-z](?:[a-z0-9_-]+)"
|
74
75
|
VALUE_REGEX = '.+'
|
75
76
|
|
76
77
|
#disallow machine tags special characters and tag list delimiter OR allow machine tag format
|
@@ -79,16 +80,21 @@ class Tag < ActiveRecord::Base
|
|
79
80
|
named_scope :namespace_counts, :select=>'*, namespace as counter, count(namespace) as count', :group=>"namespace HAVING count(namespace)>=1"
|
80
81
|
named_scope :predicate_counts, :select=>'*, predicate as counter, count(predicate) as count', :group=>"predicate HAVING count(predicate)>=1"
|
81
82
|
named_scope :value_counts, :select=>'*, value as counter, count(value) as count', :group=>"value HAVING count(value)>=1"
|
82
|
-
named_scope :
|
83
|
-
named_scope :
|
84
|
-
named_scope :
|
83
|
+
named_scope :distinct_namespaces, :select=>"distinct namespace"
|
84
|
+
named_scope :distinct_predicates, :select=>"distinct predicate"
|
85
|
+
named_scope :distinct_values, :select=>"distinct value"
|
85
86
|
|
87
|
+
def self.namespaces; distinct_namespaces.map(&:namespace).compact; end
|
88
|
+
def self.predicates; distinct_predicates.map(&:predicate).compact; end
|
89
|
+
def self.values; distinct_values.map(&:value).compact; end
|
90
|
+
|
86
91
|
# To be used with the *counts methods.
|
87
92
|
# For example:
|
88
93
|
# stat(:namespace_counts)
|
89
94
|
# This prints out pairs of a namespaces and their counts in the tags table.
|
90
95
|
def self.stat(type)
|
91
|
-
|
96
|
+
shortcuts = {:n=>:namespace_counts, :p=>:predicate_counts, :v=>:value_counts }
|
97
|
+
send(shortcuts[type] || type).map {|e| [e.counter, e.count] }
|
92
98
|
end
|
93
99
|
|
94
100
|
# Takes a wildcard machine tag and returns matching tags.
|
data/lib/has_machine_tags.rb
CHANGED
@@ -7,8 +7,12 @@ module HasMachineTags
|
|
7
7
|
base.extend(ClassMethods)
|
8
8
|
end
|
9
9
|
|
10
|
-
module ClassMethods
|
11
|
-
|
10
|
+
module ClassMethods
|
11
|
+
# Options
|
12
|
+
# :reverse_has_many - Defines a has_many :through from tags to the model using the plural of the model name.
|
13
|
+
def has_machine_tags(options={})
|
14
|
+
cattr_accessor :quick_mode
|
15
|
+
self.quick_mode = options[:quick_mode] || false
|
12
16
|
self.class_eval do
|
13
17
|
has_many :taggings, :as=>:taggable, :dependent=>:destroy
|
14
18
|
has_many :tags, :through=>:taggings
|
@@ -17,11 +21,17 @@ module HasMachineTags
|
|
17
21
|
include HasMachineTags::InstanceMethods
|
18
22
|
extend HasMachineTags::SingletonMethods
|
19
23
|
if respond_to?(:named_scope)
|
20
|
-
named_scope :tagged_with, lambda{ |
|
21
|
-
find_options_for_find_tagged_with(
|
24
|
+
named_scope :tagged_with, lambda{ |*args|
|
25
|
+
find_options_for_find_tagged_with(*args)
|
22
26
|
}
|
23
27
|
end
|
24
28
|
end
|
29
|
+
if options[:reverse_has_many]
|
30
|
+
model = self.to_s
|
31
|
+
'Tag'.constantize.class_eval do
|
32
|
+
has_many(model.tableize, :through => :taggings, :source => :taggable, :source_type =>model)
|
33
|
+
end
|
34
|
+
end
|
25
35
|
end
|
26
36
|
end
|
27
37
|
|
@@ -30,17 +40,17 @@ module HasMachineTags
|
|
30
40
|
# Note that each tag is interpreted as a possible wildcard machine tag.
|
31
41
|
#
|
32
42
|
# Options:
|
33
|
-
# :exclude - Find models that are not tagged with the given tags
|
34
|
-
# :match_all - Find models that match all of the given tags, not just one
|
35
|
-
# :conditions - A piece of SQL conditions to add to the query
|
43
|
+
# :exclude - Find models that are not tagged with the given tags.
|
44
|
+
# :match_all - Find models that match all of the given tags, not just one (doesn't work with machine tags yet).
|
45
|
+
# :conditions - A piece of SQL conditions to add to the query.
|
36
46
|
#
|
37
47
|
# Example:
|
38
48
|
# Url.tagged_with 'something' # => fetches urls tagged with 'something'
|
39
49
|
# Url.tagged_with 'gem:' # => fetches urls tagged with tags that have namespace gem
|
40
|
-
# Url.tagged_with 'gem
|
41
|
-
# and
|
50
|
+
# Url.tagged_with 'gem, something' # => fetches urls that are tagged with 'something'
|
51
|
+
# and 'gem'
|
42
52
|
#
|
43
|
-
# Note: This method really only needs to be used
|
53
|
+
# Note: This method really only needs to be used with Rails < 2.1 .
|
44
54
|
# Rails 2.1 and greater should use tagged_with(), which acts the same but with
|
45
55
|
# the benefits of named_scope.
|
46
56
|
#
|
@@ -50,7 +60,7 @@ module HasMachineTags
|
|
50
60
|
end
|
51
61
|
|
52
62
|
def find_options_for_find_tagged_with(tags, options = {}) #:nodoc:
|
53
|
-
options.reverse_merge!(:match_all=>true)
|
63
|
+
# options.reverse_merge!(:match_all=>true)
|
54
64
|
tags = TagList.new(tags)
|
55
65
|
return {} if tags.empty?
|
56
66
|
|
@@ -59,12 +69,14 @@ module HasMachineTags
|
|
59
69
|
|
60
70
|
taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"
|
61
71
|
|
72
|
+
machine_tag_used = false
|
62
73
|
if options.delete(:exclude)
|
63
74
|
tags_conditions = tags.map { |t| sanitize_sql(["#{Tag.table_name}.name = ?", t]) }.join(" OR ")
|
64
75
|
conditions << sanitize_sql(["#{table_name}.id NOT IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} LEFT OUTER JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id WHERE (#{tags_conditions}) AND #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})", tags])
|
65
76
|
else
|
66
|
-
|
77
|
+
tag_sql = tags.map {|t|
|
67
78
|
if match = Tag.match_wildcard_machine_tag(t)
|
79
|
+
machine_tag_used = true
|
68
80
|
string = match.map {|k,v|
|
69
81
|
sanitize_sql(["#{tags_alias}.#{k} = ?", v])
|
70
82
|
}.join(" AND ")
|
@@ -73,9 +85,17 @@ module HasMachineTags
|
|
73
85
|
sanitize_sql(["#{tags_alias}.name = ?", t])
|
74
86
|
end
|
75
87
|
}.join(" OR ")
|
76
|
-
|
88
|
+
conditions << tag_sql
|
89
|
+
|
77
90
|
if options.delete(:match_all)
|
78
|
-
group = "#{taggings_alias}.taggable_id HAVING COUNT(#{taggings_alias}.taggable_id) =
|
91
|
+
group = "#{taggings_alias}.taggable_id HAVING COUNT(#{taggings_alias}.taggable_id) = "
|
92
|
+
if machine_tag_used
|
93
|
+
#Since a machine tag matches multiple tags per given tag, we need to dynamically calculate the count
|
94
|
+
#TODO: this select needs to return differently for each taggable_id
|
95
|
+
group += "(SELECT count(id) FROM #{Tag.table_name} #{tags_alias} WHERE #{tag_sql})"
|
96
|
+
else
|
97
|
+
group += tags.size.to_s
|
98
|
+
end
|
79
99
|
end
|
80
100
|
end
|
81
101
|
|
@@ -89,9 +109,25 @@ module HasMachineTags
|
|
89
109
|
end
|
90
110
|
|
91
111
|
module InstanceMethods
|
112
|
+
|
113
|
+
def quick_mode_tag_list(list) #:nodoc:
|
114
|
+
mtag_list = TagList.new(list)
|
115
|
+
mtag_list = mtag_list.map {|e|
|
116
|
+
if e.include?(":")
|
117
|
+
namespace,other = e.split(":")
|
118
|
+
other.split(";").map {|e|
|
119
|
+
e.include?("=") ? "#{namespace}:#{e}" : "#{namespace}:tags=#{e}"
|
120
|
+
}
|
121
|
+
else
|
122
|
+
e
|
123
|
+
end
|
124
|
+
}.flatten
|
125
|
+
TagList.new(mtag_list)
|
126
|
+
end
|
127
|
+
|
92
128
|
# Set tag list with an array of tags or comma delimited string of tags
|
93
129
|
def tag_list=(list)
|
94
|
-
@tag_list = TagList.new(list)
|
130
|
+
@tag_list = quick_mode ? quick_mode_tag_list(list) : TagList.new(list)
|
95
131
|
end
|
96
132
|
|
97
133
|
# Fetches latest tag list for an object
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cldwalker-has_machine_tags
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gabriel Horner
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-02-
|
12
|
+
date: 2009-02-20 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -25,11 +25,15 @@ extra_rdoc_files:
|
|
25
25
|
files:
|
26
26
|
- README.rdoc
|
27
27
|
- LICENSE.txt
|
28
|
+
- init.rb
|
29
|
+
- Rakefile
|
30
|
+
- VERSION.yml
|
28
31
|
- generators/has_machine_tags_migration
|
29
32
|
- generators/has_machine_tags_migration/has_machine_tags_migration_generator.rb
|
30
33
|
- generators/has_machine_tags_migration/templates
|
31
34
|
- generators/has_machine_tags_migration/templates/migration.rb
|
32
35
|
- lib/has_machine_tags
|
36
|
+
- lib/has_machine_tags/namespace_group.rb
|
33
37
|
- lib/has_machine_tags/tag.rb
|
34
38
|
- lib/has_machine_tags/tag_list.rb
|
35
39
|
- lib/has_machine_tags/tagging.rb
|