cldwalker-has_machine_tags 0.1.2 → 0.1.3
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.
- 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
|