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 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