cldwalker-has_machine_tags 0.1.3 → 0.1.4
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 +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
|