cldwalker-has_machine_tags 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +3 -1
- data/Rakefile +1 -1
- data/VERSION.yml +1 -1
- data/init.rb +1 -5
- data/lib/has_machine_tags/console.rb +44 -0
- data/lib/has_machine_tags/singleton_methods.rb +91 -0
- data/lib/has_machine_tags/tag.rb +2 -142
- data/lib/has_machine_tags/tag_list.rb +34 -3
- data/lib/has_machine_tags/tag_methods.rb +165 -0
- data/lib/has_machine_tags.rb +17 -94
- data/rails/init.rb +13 -0
- data/test/has_machine_tags_test.rb +16 -0
- metadata +6 -3
- data/lib/has_machine_tags/namespace_group.rb +0 -146
data/README.rdoc
CHANGED
@@ -9,9 +9,11 @@ Basically, a machine tag has a namespace, a predicate and a value in the format
|
|
9
9
|
|
10
10
|
This allows for more precise tagging as tags can have unlimited contexts provided
|
11
11
|
by combinations of namespaces and predicates. These unlimited contexts also make
|
12
|
-
machine tags ripe for modeling relationships between objects. Read the
|
12
|
+
machine tags ripe for modeling relationships between objects. Read the HasMachineTags::TagMethods class
|
13
13
|
documentation for a more thorough explanation.
|
14
14
|
|
15
|
+
A demo app using this plugin is {here}[http://github.com/cldwalker/tag-tree].
|
16
|
+
|
15
17
|
== Install
|
16
18
|
|
17
19
|
Install as a gem
|
data/Rakefile
CHANGED
@@ -25,7 +25,7 @@ begin
|
|
25
25
|
s.authors = ["Gabriel Horner"]
|
26
26
|
s.has_rdoc = true
|
27
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}/**/*"]
|
28
|
+
s.files = FileList["README.rdoc", "LICENSE.txt", "init.rb", "Rakefile", "VERSION.yml", "{rails,generators,bin,lib,test}/**/*"]
|
29
29
|
end
|
30
30
|
|
31
31
|
rescue LoadError
|
data/VERSION.yml
CHANGED
data/init.rb
CHANGED
@@ -0,0 +1,44 @@
|
|
1
|
+
module HasMachineTags
|
2
|
+
# Methods for console/irb use.
|
3
|
+
module Console
|
4
|
+
module InstanceMethods
|
5
|
+
# Removes first list, adds second list and saves changes.
|
6
|
+
def tag_add_and_remove(remove_list, add_list)
|
7
|
+
self.class.transaction do
|
8
|
+
tag_add_and_save(add_list)
|
9
|
+
tag_remove_and_save(remove_list)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Adds given list and saves.
|
14
|
+
def tag_add_and_save(add_list)
|
15
|
+
self.tag_list = self.tag_list + current_tag_list(add_list)
|
16
|
+
self.save
|
17
|
+
self.tag_list
|
18
|
+
end
|
19
|
+
|
20
|
+
# Removes given list and saves.
|
21
|
+
def tag_remove_and_save(remove_list)
|
22
|
+
self.tag_list = self.tag_list - current_tag_list(remove_list)
|
23
|
+
self.save
|
24
|
+
self.tag_list
|
25
|
+
end
|
26
|
+
|
27
|
+
# Resets tag_list to given tag_list and saves.
|
28
|
+
def tag_and_save(tag_list)
|
29
|
+
self.tag_list = tag_list
|
30
|
+
self.save
|
31
|
+
self.tag_list
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module ClassMethods
|
36
|
+
# Updates items tagged with an old tag to use the given new tag in place of the old tag.
|
37
|
+
def find_and_change_tag(old_tag, new_tag)
|
38
|
+
results = tagged_with(old_tag)
|
39
|
+
results.each {|e| e.tag_add_and_remove(old_tag, new_tag)}
|
40
|
+
puts "Changed tag for #{results.length} records"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module HasMachineTags
|
2
|
+
module SingletonMethods
|
3
|
+
# Takes a string of delimited tags or an array of tags.
|
4
|
+
# Note that each tag is interpreted as a possible wildcard machine tag.
|
5
|
+
#
|
6
|
+
# Options:
|
7
|
+
# :conditions - A piece of SQL conditions to add to the query.
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
# Url.tagged_with 'something' # => fetches urls tagged with 'something'
|
11
|
+
# Url.tagged_with 'gem:' # => fetches urls tagged with tags that have namespace gem
|
12
|
+
# Url.tagged_with 'gem, something' # => fetches urls that are tagged with 'something'
|
13
|
+
# or 'gem'
|
14
|
+
#
|
15
|
+
# Note: This method really only needs to be used with Rails < 2.1 .
|
16
|
+
# Rails 2.1 and greater should use tagged_with(), which acts the same but with
|
17
|
+
# the benefits of named_scope.
|
18
|
+
#
|
19
|
+
def find_tagged_with(*args)
|
20
|
+
options = find_options_for_tagged_with(*args)
|
21
|
+
options.blank? ? [] : find(:all,options)
|
22
|
+
end
|
23
|
+
|
24
|
+
# :stopdoc:
|
25
|
+
def find_options_for_tagged_with(tags, options = {})
|
26
|
+
tags = TagList.new(tags)
|
27
|
+
return {} if tags.empty?
|
28
|
+
conditions = []
|
29
|
+
conditions << sanitize_sql(options.delete(:conditions)) if options[:conditions]
|
30
|
+
conditions << condition_from_tags(tags)
|
31
|
+
default_find_options_for_tagged_with.update(:conditions=>conditions.join(" AND ")).update(options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def condition_from_tags(tags)
|
35
|
+
tag_sql = tags.map {|t|
|
36
|
+
if match = Tag.match_wildcard_machine_tag(t)
|
37
|
+
string = match.map {|k,v|
|
38
|
+
sanitize_sql(["#{tags_alias}.#{k} = ?", v])
|
39
|
+
}.join(" AND ")
|
40
|
+
"(#{string})"
|
41
|
+
else
|
42
|
+
sanitize_sql(["#{tags_alias}.name = ?", t])
|
43
|
+
end
|
44
|
+
}.join(" OR ")
|
45
|
+
end
|
46
|
+
|
47
|
+
def taggings_alias
|
48
|
+
"#{table_name}_taggings"
|
49
|
+
end
|
50
|
+
|
51
|
+
def tags_alias
|
52
|
+
"#{table_name}_tags"
|
53
|
+
end
|
54
|
+
|
55
|
+
def default_find_options_for_tagged_with
|
56
|
+
{ :select => "DISTINCT #{table_name}.*",
|
57
|
+
:joins => "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias} ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key} AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)} " +
|
58
|
+
"LEFT OUTER JOIN #{Tag.table_name} #{tags_alias} ON #{tags_alias}.id = #{taggings_alias}.tag_id",
|
59
|
+
# :group => group
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
# TODO: add back in options as needed.
|
64
|
+
# Options:
|
65
|
+
# :exclude - Find models that are not tagged with the given tags.
|
66
|
+
# :match_all - Find models that match all of the given tags, not just one (doesn't work with machine tags yet).
|
67
|
+
def old_find_options_for_find_tagged_with(tags, options = {}) #:nodoc:
|
68
|
+
# options.reverse_merge!(:match_all=>true)
|
69
|
+
machine_tag_used = false
|
70
|
+
if options.delete(:exclude)
|
71
|
+
tags_conditions = tags.map { |t| sanitize_sql(["#{Tag.table_name}.name = ?", t]) }.join(" OR ")
|
72
|
+
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])
|
73
|
+
else
|
74
|
+
conditions << condition_from_tags(tags)
|
75
|
+
|
76
|
+
if options.delete(:match_all)
|
77
|
+
group = "#{taggings_alias}.taggable_id HAVING COUNT(#{taggings_alias}.taggable_id) = "
|
78
|
+
if machine_tag_used
|
79
|
+
#Since a machine tag matches multiple tags per given tag, we need to dynamically calculate the count
|
80
|
+
#TODO: this select needs to return differently for each taggable_id
|
81
|
+
group += "(SELECT count(id) FROM #{Tag.table_name} #{tags_alias} WHERE #{tag_sql})"
|
82
|
+
else
|
83
|
+
group += tags.size.to_s
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
default_find_options_for_tagged_with.update(:conditions=>conditions.join(" AND ")).update(options)
|
88
|
+
end
|
89
|
+
# :startdoc:
|
90
|
+
end
|
91
|
+
end
|
data/lib/has_machine_tags/tag.rb
CHANGED
@@ -1,143 +1,3 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
# [namespace]:[predicate]=[value]
|
4
|
-
#
|
5
|
-
# As explained here[http://www.flickr.com/groups/api/discuss/72157594497877875],
|
6
|
-
# a namespace and predicate must start with a letter a-z while its remaining characters can be any lowercase alphanumeric character
|
7
|
-
# and underscore. A value can contain any characters that normal tags use.
|
8
|
-
#
|
9
|
-
# == Wildcard Machine Tags
|
10
|
-
# Wildcard machine tag syntax is used with Tag.machine_tags() and {tagged_with() or find_tagged_with()}[link:classes/HasMachineTags/SingletonMethods.html] of tagged objects.
|
11
|
-
# This syntax allows one to fetch items that fall under a group of tags, as specified by namespace, predicate, value or
|
12
|
-
# a combination of these ways. While this plugin supports {Flickr's wildcard format}[http://code.flickr.com/blog/2008/07/18/wildcard-machine-tag-urls/],
|
13
|
-
# it also supports its own slightly shorter format.
|
14
|
-
#
|
15
|
-
# === Examples
|
16
|
-
#
|
17
|
-
# For a tag 'user:name=john', the following wildcards would match it:
|
18
|
-
#
|
19
|
-
# * Wild namespace (any tag with namespace user)
|
20
|
-
# Tag.machine_tags 'user:' # Our way
|
21
|
-
# Tag.machine_tags 'user:*=' # Flickr way
|
22
|
-
#
|
23
|
-
# * Wild predicate (any tag with predicate name)
|
24
|
-
# Tag.machine_tags 'name=' # Our way
|
25
|
-
# Tag.machine_tags '*:name=' # Flickr way
|
26
|
-
#
|
27
|
-
# * Wild predicate (any tag with value john)
|
28
|
-
# Tag.machine_tags '=john' # Our way
|
29
|
-
# Tag.machine_tags '*:*=john' # Flickr way
|
30
|
-
#
|
31
|
-
# * Wild namespace and predicate (any tag with namespace user and predicate name)
|
32
|
-
# Tag.machine_tags 'user:name' # Our way
|
33
|
-
# Tag.machine_tags 'user:name=' # Flickr way
|
34
|
-
#
|
35
|
-
# * Wild predicate and value (any tag with predicate name and value john)
|
36
|
-
# Tag.machine_tags 'name=john' # Our way
|
37
|
-
# Tag.machine_tags '*:name=john' # Flickr way
|
38
|
-
#
|
39
|
-
# * Wild namespace and value (any tag with namespace user and value john)
|
40
|
-
# Tag.machine_tags 'user.john' # Our way
|
41
|
-
# Tag.machine_tags 'user:*=john' # Flickr way
|
42
|
-
#
|
43
|
-
# == Food For Thought
|
44
|
-
# So what's so great about being able to give a tag a namespace and a predicate?
|
45
|
-
# * It allows for more fine-grained tag queries by giving multiple contexts:
|
46
|
-
#
|
47
|
-
# Say instead of having tagged with 'user:name=john' we had tagged with the traditional separate
|
48
|
-
# tags: user, name and john. How would we know that we had meant an item to be tagged
|
49
|
-
# as a user ie with namespace user? We wouldn't know. Any query for 'user' would return
|
50
|
-
# all user-tagged items <b>without context</b>. With machine tags, we can have 'user' refer
|
51
|
-
# to a particular combination of namespace, predicate and value.
|
52
|
-
#
|
53
|
-
# * It keeps tag-spaces cleaner because there are more contexts:
|
54
|
-
#
|
55
|
-
# With traditional separate tags, tags just have a global context. So
|
56
|
-
# if different users decide to give different meaning to the same tag, the tag starts to become
|
57
|
-
# polluted and loses its usefulness. With the limitless contexts provided by machine tags,
|
58
|
-
# a machine tag is less likely to pollute other tags.
|
59
|
-
#
|
60
|
-
# * It allows tagging to serve as a medium for defining relationships between objects:
|
61
|
-
#
|
62
|
-
# Since a machine tag tracks three attributes (namespace, predicate and value), it's possible to develop relationships
|
63
|
-
# between the attributes. This means namespaces can have many predicates and predicates can have many values.
|
64
|
-
# Since this closely resembles object modeling, we can start to use tagging to form relationships between tagged items and other objects.
|
65
|
-
|
66
|
-
class Tag < ActiveRecord::Base
|
67
|
-
has_many :taggings
|
68
|
-
|
69
|
-
validates_presence_of :name
|
70
|
-
validates_uniqueness_of :name
|
71
|
-
before_save :update_name_related_columns
|
72
|
-
|
73
|
-
NAMESPACE_REGEX = "[a-z](?:[a-z0-9_]+)"
|
74
|
-
PREDICATE_REGEX = "[a-z](?:[a-z0-9_-]+)"
|
75
|
-
VALUE_REGEX = '.+'
|
76
|
-
|
77
|
-
#disallow machine tags special characters and tag list delimiter OR allow machine tag format
|
78
|
-
validates_format_of :name, :with=>/\A(([^\*\=\:\.,]+)|(#{NAMESPACE_REGEX}\:#{PREDICATE_REGEX}\=#{VALUE_REGEX}))\Z/
|
79
|
-
|
80
|
-
named_scope :namespace_counts, :select=>'*, namespace as counter, count(namespace) as count', :group=>"namespace HAVING count(namespace)>=1"
|
81
|
-
named_scope :predicate_counts, :select=>'*, predicate as counter, count(predicate) as count', :group=>"predicate HAVING count(predicate)>=1"
|
82
|
-
named_scope :value_counts, :select=>'*, value as counter, count(value) as count', :group=>"value HAVING count(value)>=1"
|
83
|
-
named_scope :distinct_namespaces, :select=>"distinct namespace"
|
84
|
-
named_scope :distinct_predicates, :select=>"distinct predicate"
|
85
|
-
named_scope :distinct_values, :select=>"distinct value"
|
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
|
-
|
91
|
-
# To be used with the *counts methods.
|
92
|
-
# For example:
|
93
|
-
# stat(:namespace_counts)
|
94
|
-
# This prints out pairs of a namespaces and their counts in the tags table.
|
95
|
-
def self.stat(type)
|
96
|
-
shortcuts = {:n=>:namespace_counts, :p=>:predicate_counts, :v=>:value_counts }
|
97
|
-
send(shortcuts[type] || type).map {|e| [e.counter, e.count] }
|
98
|
-
end
|
99
|
-
|
100
|
-
# Takes a wildcard machine tag and returns matching tags.
|
101
|
-
def self.machine_tags(name)
|
102
|
-
conditions = if (match = match_wildcard_machine_tag(name))
|
103
|
-
match.map {|k,v|
|
104
|
-
sanitize_sql(["#{k} = ?", v])
|
105
|
-
}.join(" AND ")
|
106
|
-
else
|
107
|
-
sanitize_sql(["name = ?", name])
|
108
|
-
end
|
109
|
-
find(:all, :conditions=>conditions)
|
110
|
-
end
|
111
|
-
|
112
|
-
# Valid wildcards with their equivalent shortcuts
|
113
|
-
# namespace:*=* -> namespace:
|
114
|
-
# *:predicate=* -> predicate=
|
115
|
-
# *:*=value -> :value
|
116
|
-
def self.match_wildcard_machine_tag(name) #:nodoc:
|
117
|
-
if name =~ /^(#{NAMESPACE_REGEX}|\*)\:(#{PREDICATE_REGEX}|\*)\=(#{VALUE_REGEX}|\*)?$/
|
118
|
-
result = [[:namespace, $1], [:predicate, $2], [:value, $3]].select {|k,v| ![nil,'*'].include?(v) }
|
119
|
-
result.size == 3 ? nil : result
|
120
|
-
#duo shortcuts
|
121
|
-
elsif name =~ /^(#{NAMESPACE_REGEX}\:#{PREDICATE_REGEX})|(#{PREDICATE_REGEX}\=#{VALUE_REGEX})|(#{NAMESPACE_REGEX}\.#{VALUE_REGEX})$/
|
122
|
-
$1 ? [:namespace, :predicate].zip($1.split(":")) : ($2 ? [:predicate, :value].zip($2.split("=")) :
|
123
|
-
[:namespace, :value].zip($3.split('.')) )
|
124
|
-
#single shortcuts
|
125
|
-
elsif name =~ /^((#{NAMESPACE_REGEX})(?:\:)|(#{PREDICATE_REGEX})(?:\=)|(?:\=)(#{VALUE_REGEX}))$/
|
126
|
-
$2 ? [[:namespace, $2]] : ($3 ? [[:predicate, $3]] : [[:value, $4]])
|
127
|
-
else
|
128
|
-
nil
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
def extract_from_name(tag_name) #:nodoc:
|
133
|
-
(tag_name =~ /^(#{NAMESPACE_REGEX})\:(#{PREDICATE_REGEX})\=(#{VALUE_REGEX})$/) ? [$1, $2, $3] : nil
|
134
|
-
end
|
135
|
-
|
136
|
-
private
|
137
|
-
|
138
|
-
def update_name_related_columns
|
139
|
-
if self.changed.include?('name') && (arr = extract_from_name(self.name))
|
140
|
-
self[:namespace], self[:predicate], self[:value] = arr
|
141
|
-
end
|
142
|
-
end
|
1
|
+
class ::Tag < ActiveRecord::Base
|
2
|
+
include HasMachineTags::TagMethods
|
143
3
|
end
|
@@ -1,14 +1,45 @@
|
|
1
1
|
module HasMachineTags
|
2
|
-
class TagList < Array
|
2
|
+
class TagList < Array
|
3
3
|
cattr_accessor :delimiter
|
4
4
|
self.delimiter = ','
|
5
|
+
cattr_accessor :default_predicate
|
6
|
+
self.default_predicate = 'tags'
|
7
|
+
QUICK_MODE_DELIMITER = ';'
|
5
8
|
|
6
|
-
|
9
|
+
# ==== Options:
|
10
|
+
# [:quick_mode]
|
11
|
+
# When true enables a quick mode for inputing multiple machine tags under the same namespace.
|
12
|
+
# These machine tags are delimited by QUICK_MODE_DELIMITER. If a predicate is not specified, default_predicate() is used.
|
13
|
+
# Examples:
|
14
|
+
# # Namespace is added to tag 'type=test'.
|
15
|
+
# HasMachineTags::TagList.new("gem:name=flog;type=test, user:name=seattlerb", :quick_mode=>true)
|
16
|
+
# => ["gem:name=flog", "gem:type=test", "user:name=seattlerb"]
|
17
|
+
#
|
18
|
+
# # Namespace and default predicate (tags) are added to tag 'git'.
|
19
|
+
# HasMachineTags::TagList.new("gem:name=grit;git, user:name=mojombo")
|
20
|
+
# => ["gem:name=grit", "gem:tags=git", "user:name=mojombo"]
|
21
|
+
def initialize(string_or_array, options={})
|
22
|
+
@options = options
|
7
23
|
array = string_or_array.is_a?(Array) ? string_or_array : string_or_array.split(/\s*#{delimiter}\s*/)
|
24
|
+
array = parse_quick_mode(array) if @options[:quick_mode]
|
8
25
|
concat array
|
9
26
|
end
|
27
|
+
|
28
|
+
def parse_quick_mode(mtag_list) #:nodoc:
|
29
|
+
mtag_list = mtag_list.map {|e|
|
30
|
+
if e.include?(Tag::PREDICATE_DELIMITER)
|
31
|
+
namespace, remainder = e.split(Tag::PREDICATE_DELIMITER)
|
32
|
+
remainder.split(QUICK_MODE_DELIMITER).map {|e|
|
33
|
+
e.include?(Tag::VALUE_DELIMITER) ? "#{namespace}#{Tag::PREDICATE_DELIMITER}#{e}" :
|
34
|
+
Tag.build_machine_tag(namespace, default_predicate, e)
|
35
|
+
}
|
36
|
+
else
|
37
|
+
e
|
38
|
+
end
|
39
|
+
}.flatten
|
40
|
+
end
|
10
41
|
|
11
|
-
def to_s
|
42
|
+
def to_s #:nodoc:
|
12
43
|
join("#{delimiter} ")
|
13
44
|
end
|
14
45
|
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
module HasMachineTags
|
2
|
+
# == Machine Tags
|
3
|
+
# Machine tags, also known as triple tags, are in the format:
|
4
|
+
# [namespace]:[predicate]=[value]
|
5
|
+
#
|
6
|
+
# As explained here[http://www.flickr.com/groups/api/discuss/72157594497877875],
|
7
|
+
# a namespace and predicate must start with a letter a-z while its remaining characters can be any lowercase alphanumeric character
|
8
|
+
# and underscore. A value can contain any characters that normal tags use.
|
9
|
+
#
|
10
|
+
# == Wildcard Machine Tags
|
11
|
+
# Wildcard machine tag syntax is used with Tag.machine_tags() and {tagged_with() or find_tagged_with()}[link:classes/HasMachineTags/SingletonMethods.html] of tagged objects.
|
12
|
+
# This syntax allows one to fetch items that fall under a group of tags, as specified by namespace, predicate, value or
|
13
|
+
# a combination of these ways. While this plugin supports {Flickr's wildcard format}[http://code.flickr.com/blog/2008/07/18/wildcard-machine-tag-urls/],
|
14
|
+
# it also supports its own slightly shorter format.
|
15
|
+
#
|
16
|
+
# === Examples
|
17
|
+
#
|
18
|
+
# For a tag 'user:name=john', the following wildcards would match it:
|
19
|
+
#
|
20
|
+
# * Wild namespace (any tag with namespace user)
|
21
|
+
# Tag.machine_tags 'user:' # Our way
|
22
|
+
# Tag.machine_tags 'user:*=' # Flickr way
|
23
|
+
#
|
24
|
+
# * Wild predicate (any tag with predicate name)
|
25
|
+
# Tag.machine_tags 'name=' # Our way
|
26
|
+
# Tag.machine_tags '*:name=' # Flickr way
|
27
|
+
#
|
28
|
+
# * Wild predicate (any tag with value john)
|
29
|
+
# Tag.machine_tags '=john' # Our way
|
30
|
+
# Tag.machine_tags '*:*=john' # Flickr way
|
31
|
+
#
|
32
|
+
# * Wild namespace and predicate (any tag with namespace user and predicate name)
|
33
|
+
# Tag.machine_tags 'user:name' # Our way
|
34
|
+
# Tag.machine_tags 'user:name=' # Flickr way
|
35
|
+
#
|
36
|
+
# * Wild predicate and value (any tag with predicate name and value john)
|
37
|
+
# Tag.machine_tags 'name=john' # Our way
|
38
|
+
# Tag.machine_tags '*:name=john' # Flickr way
|
39
|
+
#
|
40
|
+
# * Wild namespace and value (any tag with namespace user and value john)
|
41
|
+
# Tag.machine_tags 'user.john' # Our way
|
42
|
+
# Tag.machine_tags 'user:*=john' # Flickr way
|
43
|
+
#
|
44
|
+
# == Food For Thought
|
45
|
+
# So what's so great about being able to give a tag a namespace and a predicate?
|
46
|
+
# * It allows for more fine-grained tag queries by giving multiple contexts:
|
47
|
+
#
|
48
|
+
# Say instead of having tagged with 'user:name=john' we had tagged with the traditional separate
|
49
|
+
# tags: user, name and john. How would we know that we had meant an item to be tagged
|
50
|
+
# as a user ie with namespace user? We wouldn't know. Any query for 'user' would return
|
51
|
+
# all user-tagged items <b>without context</b>. With machine tags, we can have 'user' refer
|
52
|
+
# to a particular combination of namespace, predicate and value.
|
53
|
+
#
|
54
|
+
# * It keeps tag-spaces cleaner because there are more contexts:
|
55
|
+
#
|
56
|
+
# With traditional separate tags, tags just have a global context. So
|
57
|
+
# if different users decide to give different meaning to the same tag, the tag starts to become
|
58
|
+
# polluted and loses its usefulness. With the limitless contexts provided by machine tags,
|
59
|
+
# a machine tag is less likely to pollute other tags.
|
60
|
+
#
|
61
|
+
# * It allows tagging to serve as a medium for defining relationships between objects:
|
62
|
+
#
|
63
|
+
# Since a machine tag tracks three attributes (namespace, predicate and value), it's possible to develop relationships
|
64
|
+
# between the attributes. This means namespaces can have many predicates and predicates can have many values.
|
65
|
+
# Since this closely resembles object modeling, we can start to use tagging to form relationships between tagged items and other objects.
|
66
|
+
module TagMethods
|
67
|
+
NAMESPACE_REGEX = "[a-z](?:[a-z0-9_]+)"
|
68
|
+
PREDICATE_REGEX = "[a-z](?:[a-z0-9_-]+)"
|
69
|
+
VALUE_REGEX = '.+'
|
70
|
+
#TODO: use delimiters in this file
|
71
|
+
PREDICATE_DELIMITER = ':'
|
72
|
+
VALUE_DELIMITER = '='
|
73
|
+
|
74
|
+
def self.included(base) #:nodoc:
|
75
|
+
name_format = /\A(([^\*\=\:\.,]+)|(#{NAMESPACE_REGEX}\:#{PREDICATE_REGEX}\=#{VALUE_REGEX}))\Z/
|
76
|
+
base.class_eval %[
|
77
|
+
has_many :taggings
|
78
|
+
|
79
|
+
validates_presence_of :name
|
80
|
+
validates_uniqueness_of :name
|
81
|
+
before_save :update_name_related_columns
|
82
|
+
|
83
|
+
#disallow machine tags special characters and tag list delimiter OR allow machine tag format
|
84
|
+
validates_format_of :name, :with=>name_format
|
85
|
+
|
86
|
+
named_scope :namespace_counts, :select=>'*, namespace as counter, count(namespace) as count', :group=>"namespace HAVING count(namespace)>=1"
|
87
|
+
named_scope :predicate_counts, :select=>'*, predicate as counter, count(predicate) as count', :group=>"predicate HAVING count(predicate)>=1"
|
88
|
+
named_scope :value_counts, :select=>'*, value as counter, count(value) as count', :group=>"value HAVING count(value)>=1"
|
89
|
+
named_scope :distinct_namespaces, :select=>"distinct namespace"
|
90
|
+
named_scope :distinct_predicates, :select=>"distinct predicate"
|
91
|
+
named_scope :distinct_values, :select=>"distinct value"
|
92
|
+
]
|
93
|
+
base.extend(ClassMethods)
|
94
|
+
base.send :include, InstanceMethods
|
95
|
+
end
|
96
|
+
|
97
|
+
module ClassMethods
|
98
|
+
#:stopdoc:
|
99
|
+
def namespaces; distinct_namespaces.map(&:namespace).compact; end
|
100
|
+
def predicates; distinct_predicates.map(&:predicate).compact; end
|
101
|
+
def values; distinct_values.map(&:value).compact; end
|
102
|
+
#:startdoc:
|
103
|
+
|
104
|
+
# To be used with the *counts methods.
|
105
|
+
# For example:
|
106
|
+
# stat(:namespace_counts)
|
107
|
+
# This prints out pairs of a namespaces and their counts in the tags table.
|
108
|
+
def stat(type)
|
109
|
+
shortcuts = {:n=>:namespace_counts, :p=>:predicate_counts, :v=>:value_counts }
|
110
|
+
send(shortcuts[type] || type).map {|e| [e.counter, e.count] }
|
111
|
+
end
|
112
|
+
|
113
|
+
# Takes a wildcard machine tag and returns matching tags.
|
114
|
+
def machine_tags(name)
|
115
|
+
conditions = if (match = match_wildcard_machine_tag(name))
|
116
|
+
match.map {|k,v|
|
117
|
+
sanitize_sql(["#{k} = ?", v])
|
118
|
+
}.join(" AND ")
|
119
|
+
else
|
120
|
+
sanitize_sql(["name = ?", name])
|
121
|
+
end
|
122
|
+
find(:all, :conditions=>conditions)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Builds a machine tag string given namespace, predicate and value.
|
126
|
+
def build_machine_tag(namespace, predicate, value)
|
127
|
+
"#{namespace}:#{predicate}=#{value}"
|
128
|
+
end
|
129
|
+
|
130
|
+
# Valid wildcards with their equivalent shortcuts
|
131
|
+
# namespace:*=* -> namespace:
|
132
|
+
# *:predicate=* -> predicate=
|
133
|
+
# *:*=value -> :value
|
134
|
+
def match_wildcard_machine_tag(name) #:nodoc:
|
135
|
+
if name =~ /^(#{NAMESPACE_REGEX}|\*)\:(#{PREDICATE_REGEX}|\*)\=(#{VALUE_REGEX}|\*)?$/
|
136
|
+
result = [[:namespace, $1], [:predicate, $2], [:value, $3]].select {|k,v| ![nil,'*'].include?(v) }
|
137
|
+
result.size == 3 ? nil : result
|
138
|
+
#duo shortcuts
|
139
|
+
elsif name =~ /^(#{NAMESPACE_REGEX}\:#{PREDICATE_REGEX})|(#{PREDICATE_REGEX}\=#{VALUE_REGEX})|(#{NAMESPACE_REGEX}\.#{VALUE_REGEX})$/
|
140
|
+
$1 ? [:namespace, :predicate].zip($1.split(":")) : ($2 ? [:predicate, :value].zip($2.split("=")) :
|
141
|
+
[:namespace, :value].zip($3.split('.')) )
|
142
|
+
#single shortcuts
|
143
|
+
elsif name =~ /^((#{NAMESPACE_REGEX})(?:\:)|(#{PREDICATE_REGEX})(?:\=)|(?:\=)(#{VALUE_REGEX}))$/
|
144
|
+
$2 ? [[:namespace, $2]] : ($3 ? [[:predicate, $3]] : [[:value, $4]])
|
145
|
+
else
|
146
|
+
nil
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
module InstanceMethods
|
152
|
+
def extract_from_name(tag_name) #:nodoc:
|
153
|
+
(tag_name =~ /^(#{NAMESPACE_REGEX})\:(#{PREDICATE_REGEX})\=(#{VALUE_REGEX})$/) ? [$1, $2, $3] : nil
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
def update_name_related_columns
|
159
|
+
if self.changed.include?('name') && (arr = extract_from_name(self.name))
|
160
|
+
self[:namespace], self[:predicate], self[:value] = arr
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
data/lib/has_machine_tags.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
current_dir = File.dirname(__FILE__)
|
2
2
|
$:.unshift(current_dir) unless $:.include?(current_dir) || $:.include?(File.expand_path(current_dir))
|
3
|
+
require 'has_machine_tags/singleton_methods'
|
3
4
|
require 'has_machine_tags/tag_list'
|
5
|
+
require 'has_machine_tags/console'
|
4
6
|
|
5
7
|
module HasMachineTags
|
6
8
|
def self.included(base) #:nodoc:
|
@@ -8,8 +10,10 @@ module HasMachineTags
|
|
8
10
|
end
|
9
11
|
|
10
12
|
module ClassMethods
|
11
|
-
# Options
|
12
|
-
#
|
13
|
+
# ==== Options:
|
14
|
+
# [:console] When true, adds additional helper methods from HasMachineTags::Console to use mainly in irb.
|
15
|
+
# [:reverse_has_many] Defines a has_many :through from tags to the model using the plural of the model name.
|
16
|
+
# [:quick_mode] When true, enables a quick mode to input machine tags with HasMachineTags::InstanceMethods.tag_list=(). See examples at HasMachineTags::TagList.new().
|
13
17
|
def has_machine_tags(options={})
|
14
18
|
cattr_accessor :quick_mode
|
15
19
|
self.quick_mode = options[:quick_mode] || false
|
@@ -20,9 +24,13 @@ module HasMachineTags
|
|
20
24
|
|
21
25
|
include HasMachineTags::InstanceMethods
|
22
26
|
extend HasMachineTags::SingletonMethods
|
27
|
+
if options[:console]
|
28
|
+
include HasMachineTags::Console::InstanceMethods
|
29
|
+
extend HasMachineTags::Console::ClassMethods
|
30
|
+
end
|
23
31
|
if respond_to?(:named_scope)
|
24
32
|
named_scope :tagged_with, lambda{ |*args|
|
25
|
-
|
33
|
+
find_options_for_tagged_with(*args)
|
26
34
|
}
|
27
35
|
end
|
28
36
|
end
|
@@ -34,102 +42,17 @@ module HasMachineTags
|
|
34
42
|
end
|
35
43
|
end
|
36
44
|
end
|
37
|
-
|
38
|
-
module SingletonMethods
|
39
|
-
# Takes a string of delimited tags or an array of tags.
|
40
|
-
# Note that each tag is interpreted as a possible wildcard machine tag.
|
41
|
-
#
|
42
|
-
# Options:
|
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.
|
46
|
-
#
|
47
|
-
# Example:
|
48
|
-
# Url.tagged_with 'something' # => fetches urls tagged with 'something'
|
49
|
-
# Url.tagged_with 'gem:' # => fetches urls tagged with tags that have namespace gem
|
50
|
-
# Url.tagged_with 'gem, something' # => fetches urls that are tagged with 'something'
|
51
|
-
# and 'gem'
|
52
|
-
#
|
53
|
-
# Note: This method really only needs to be used with Rails < 2.1 .
|
54
|
-
# Rails 2.1 and greater should use tagged_with(), which acts the same but with
|
55
|
-
# the benefits of named_scope.
|
56
|
-
#
|
57
|
-
def find_tagged_with(*args)
|
58
|
-
options = find_options_for_find_tagged_with(*args)
|
59
|
-
options.blank? ? [] : find(:all,options)
|
60
|
-
end
|
61
45
|
|
62
|
-
def find_options_for_find_tagged_with(tags, options = {}) #:nodoc:
|
63
|
-
# options.reverse_merge!(:match_all=>true)
|
64
|
-
tags = TagList.new(tags)
|
65
|
-
return {} if tags.empty?
|
66
|
-
|
67
|
-
conditions = []
|
68
|
-
conditions << sanitize_sql(options.delete(:conditions)) if options[:conditions]
|
69
|
-
|
70
|
-
taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"
|
71
|
-
|
72
|
-
machine_tag_used = false
|
73
|
-
if options.delete(:exclude)
|
74
|
-
tags_conditions = tags.map { |t| sanitize_sql(["#{Tag.table_name}.name = ?", t]) }.join(" OR ")
|
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])
|
76
|
-
else
|
77
|
-
tag_sql = tags.map {|t|
|
78
|
-
if match = Tag.match_wildcard_machine_tag(t)
|
79
|
-
machine_tag_used = true
|
80
|
-
string = match.map {|k,v|
|
81
|
-
sanitize_sql(["#{tags_alias}.#{k} = ?", v])
|
82
|
-
}.join(" AND ")
|
83
|
-
"(#{string})"
|
84
|
-
else
|
85
|
-
sanitize_sql(["#{tags_alias}.name = ?", t])
|
86
|
-
end
|
87
|
-
}.join(" OR ")
|
88
|
-
conditions << tag_sql
|
89
|
-
|
90
|
-
if options.delete(:match_all)
|
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
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
{ :select => "DISTINCT #{table_name}.*",
|
103
|
-
:joins => "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias} ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key} AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)} " +
|
104
|
-
"LEFT OUTER JOIN #{Tag.table_name} #{tags_alias} ON #{tags_alias}.id = #{taggings_alias}.tag_id",
|
105
|
-
:conditions => conditions.join(" AND "),
|
106
|
-
:group => group
|
107
|
-
}.update(options)
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
46
|
module InstanceMethods
|
112
|
-
|
113
|
-
def
|
114
|
-
|
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)
|
47
|
+
# Set tag list with an array of tags or comma delimited string of tags.
|
48
|
+
def tag_list=(list)
|
49
|
+
@tag_list = current_tag_list(list)
|
126
50
|
end
|
127
51
|
|
128
|
-
|
129
|
-
|
130
|
-
@tag_list = quick_mode ? quick_mode_tag_list(list) : TagList.new(list)
|
52
|
+
def current_tag_list(list) #:nodoc:
|
53
|
+
TagList.new(list, :quick_mode=>self.quick_mode)
|
131
54
|
end
|
132
|
-
|
55
|
+
|
133
56
|
# Fetches latest tag list for an object
|
134
57
|
def tag_list
|
135
58
|
@tag_list ||= self.tags.map(&:name)
|
data/rails/init.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'has_machine_tags'
|
2
|
+
require 'has_machine_tags/tag_methods'
|
3
|
+
|
4
|
+
#attempt to load constant
|
5
|
+
::Tag rescue nil
|
6
|
+
if Object.const_defined? :Tag
|
7
|
+
::Tag.class_eval %[include HasMachineTags::TagMethods]
|
8
|
+
else
|
9
|
+
require 'has_machine_tags/tag'
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'has_machine_tags/tagging'
|
13
|
+
ActiveRecord::Base.send :include, HasMachineTags
|
@@ -22,6 +22,22 @@ class HasMachineTagsTest < Test::Unit::TestCase
|
|
22
22
|
@taggable.tag_list.should == arr
|
23
23
|
@taggable.tag_list.to_s.should == arr.join(", ")
|
24
24
|
end
|
25
|
+
|
26
|
+
context "with quick_mode" do
|
27
|
+
before(:all) { TaggableModel.quick_mode = true }
|
28
|
+
after(:all) { TaggableModel.quick_mode = false }
|
29
|
+
|
30
|
+
test "sets tag list normally with non quick_mode characters" do
|
31
|
+
arr = ['more', 'tag:type=dumb', 'really']
|
32
|
+
@taggable.tag_list = "more,tag:type=dumb, really"
|
33
|
+
@taggable.tag_list.should == arr
|
34
|
+
end
|
35
|
+
|
36
|
+
test "sets default predicate and infers namespace" do
|
37
|
+
@taggable.tag_list = "gem:irb;name=utility_belt, article"
|
38
|
+
@taggable.tag_list.should == ["gem:tags=irb", "gem:name=utility_belt", "article"]
|
39
|
+
end
|
40
|
+
end
|
25
41
|
end
|
26
42
|
|
27
43
|
context "HasMachineTags" do
|
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.4
|
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-23 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -28,14 +28,17 @@ files:
|
|
28
28
|
- init.rb
|
29
29
|
- Rakefile
|
30
30
|
- VERSION.yml
|
31
|
+
- rails/init.rb
|
31
32
|
- generators/has_machine_tags_migration
|
32
33
|
- generators/has_machine_tags_migration/has_machine_tags_migration_generator.rb
|
33
34
|
- generators/has_machine_tags_migration/templates
|
34
35
|
- generators/has_machine_tags_migration/templates/migration.rb
|
35
36
|
- lib/has_machine_tags
|
36
|
-
- lib/has_machine_tags/
|
37
|
+
- lib/has_machine_tags/console.rb
|
38
|
+
- lib/has_machine_tags/singleton_methods.rb
|
37
39
|
- lib/has_machine_tags/tag.rb
|
38
40
|
- lib/has_machine_tags/tag_list.rb
|
41
|
+
- lib/has_machine_tags/tag_methods.rb
|
39
42
|
- lib/has_machine_tags/tagging.rb
|
40
43
|
- lib/has_machine_tags.rb
|
41
44
|
- test/has_machine_tags_test.rb
|
@@ -1,146 +0,0 @@
|
|
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
|