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 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 Tag class
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
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :major: 0
3
3
  :minor: 1
4
- :patch: 3
4
+ :patch: 4
data/init.rb CHANGED
@@ -1,5 +1 @@
1
- require 'has_machine_tags'
2
- require 'has_machine_tags/tag'
3
- require 'has_machine_tags/tagging'
4
-
5
- ActiveRecord::Base.send :include, HasMachineTags
1
+ require File.join(File.dirname(__FILE__), 'rails', 'init.rb')
@@ -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
@@ -1,143 +1,3 @@
1
- # == Machine Tags
2
- # Machine tags, also known as triple tags, are in the format:
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 #:nodoc:
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
- def initialize(string_or_array)
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
@@ -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
- # :reverse_has_many - Defines a has_many :through from tags to the model using the plural of the model name.
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
- find_options_for_find_tagged_with(*args)
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 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)
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
- # Set tag list with an array of tags or comma delimited string of tags
129
- def tag_list=(list)
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.3
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-20 00:00:00 -08:00
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/namespace_group.rb
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