cldwalker-has_machine_tags 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.rdoc CHANGED
@@ -18,10 +18,14 @@ Install as a gem
18
18
 
19
19
  bash> gem install cldwalker-has_machine_tags -s http://gems.github.com
20
20
 
21
+ # add in your environment.rb
22
+ config.gem "cldwalker-has_machine_tags", :source => "http://gems.github.com", :lib => "has_machine_tags"
23
+
21
24
  Or as a plugin
22
25
 
23
26
  bash> script/plugin install git://github.com/cldwalker/has_machine_tags.git
24
27
 
28
+
25
29
  Migrate your database from Rails root:
26
30
 
27
31
  bash> script/generate has_machine_tags_migration
@@ -128,7 +132,7 @@ these characters are off limit unless used in the machine tag context:
128
132
 
129
133
  == Credits
130
134
 
131
- Thanks goes to Flickr for implementing this clever tagging model.
135
+ Thanks goes to Flickr for popularizing this tagging model.
132
136
  Thanks also goes to the {acts-as-taggable-on plugin}[http://github.com/mbleigh/acts-as-taggable-on/tree/master]
133
137
  for their finder code and the {is_taggable plugin}[http://github.com/giraffesoft/is_taggable/tree/master]
134
138
  for demonstrating sane testing for a Rails plugin.
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ begin
5
+ require 'rcov/rcovtask'
6
+
7
+ Rcov::RcovTask.new do |t|
8
+ t.libs << 'test'
9
+ t.test_files = FileList['test/**/*_test.rb']
10
+ t.rcov_opts = ["-T -x '/Library/Ruby/*'"]
11
+ t.verbose = true
12
+ end
13
+ rescue LoadError
14
+ puts "Rcov not available. Install it for rcov-related tasks with: sudo gem install rcov"
15
+ end
16
+
17
+ begin
18
+ require 'jeweler'
19
+ Jeweler::Tasks.new do |s|
20
+ s.name = "has_machine_tags"
21
+ s.description = "A rails tagging plugin implementing flickr's machine tags + maybe more (semantic tags)"
22
+ s.summary = s.description
23
+ s.email = "gabriel.horner@gmail.com"
24
+ s.homepage = "http://github.com/cldwalker/has_machine_tags"
25
+ s.authors = ["Gabriel Horner"]
26
+ s.has_rdoc = true
27
+ s.extra_rdoc_files = ["README.rdoc", "LICENSE.txt"]
28
+ s.files = FileList["README.rdoc", "LICENSE.txt", "init.rb", "Rakefile", "VERSION.yml", "{generators,bin,lib,test}/**/*"]
29
+ end
30
+
31
+ rescue LoadError
32
+ puts "Jeweler not available. Install it for jeweler-related tasks with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
33
+ end
34
+
35
+ Rake::TestTask.new do |t|
36
+ t.libs << 'lib'
37
+ t.pattern = 'test/**/*_test.rb'
38
+ t.verbose = false
39
+ end
40
+
41
+ Rake::RDocTask.new do |rdoc|
42
+ rdoc.rdoc_dir = 'rdoc'
43
+ rdoc.title = 'test'
44
+ rdoc.options << '--line-numbers' << '--inline-source'
45
+ rdoc.rdoc_files.include('README*')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
48
+
49
+ task :default => :test
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 3
data/init.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'has_machine_tags'
2
+ require 'has_machine_tags/tag'
3
+ require 'has_machine_tags/tagging'
4
+
5
+ ActiveRecord::Base.send :include, HasMachineTags
@@ -0,0 +1,146 @@
1
+ module HasMachineTags
2
+ # These classes are used for querying machine tags and grouping results as outlines.
3
+ # These methods are for console use and may change quickly.
4
+ class NamespaceGroup #:nodoc:
5
+ def initialize(name, options={})
6
+ @name = name.to_s
7
+ @options = options
8
+ @tags = options[:tags] if options[:tags]
9
+ end
10
+
11
+ def tags
12
+ @tags ||= Tag.find_all_by_namespace(@name)
13
+ end
14
+
15
+ def predicates
16
+ tags.map(&:predicate)
17
+ end
18
+
19
+ def values
20
+ tags.map(&:value)
21
+ end
22
+
23
+ def predicate_map
24
+ tags.map {|e| [e.predicate, e.value] }.inject({}) {
25
+ |t, (k,v)| (t[k] ||=[]) << v; t
26
+ }
27
+ end
28
+ alias :pm :predicate_map
29
+
30
+ def value_count(pred,value)
31
+ Url.tagged_with("#{@name}:#{pred}=#{value}").count
32
+ end
33
+
34
+ def pred_count(pred)
35
+ (predicate_map[pred] ||[]).map {|e| [e, value_count(pred, e)]}
36
+ end
37
+
38
+ def group_pred_count(pred)
39
+ pred_count(pred).inject({}) {|hash,(k,v)|
40
+ (hash[v] ||= []) << k
41
+ hash
42
+ }
43
+ end
44
+
45
+ def sort_group_pred_count(pred)
46
+ hash = group_pred_count(pred)
47
+ hash.keys.sort.reverse.map {|e|
48
+ [e, hash[e]]
49
+ }
50
+ end
51
+
52
+ def predicate_view(pred, view=nil)
53
+ if view == :group
54
+ sort_group_pred_count(pred).map {|k,v|
55
+ "#{k}: #{v.join(', ')}"
56
+ }
57
+ elsif view == :count
58
+ pred_count(pred).map {|k,v|
59
+ "#{k}: #{v}"
60
+ }
61
+ else
62
+ nil
63
+ end
64
+ end
65
+
66
+ def outline_view(type=nil)
67
+ body = [@name]
68
+ level_delim = "\t"
69
+ predicate_map.each do |pred, vals|
70
+ body << [level_delim + pred]
71
+ values = predicate_view(pred, type) || vals
72
+ values.each {|e|
73
+ body << level_delim * 2 + e
74
+ if type == :result
75
+ urls = Url.tagged_with("#{@name}:#{pred}=#{e}")
76
+ urls.each {|u|
77
+ body << level_delim * 3 + format_result(u)
78
+ }
79
+ end
80
+ }
81
+ end
82
+ body.join("\n")
83
+ end
84
+
85
+ def format_result(result)
86
+ "#{result.id}: #{result.name}"
87
+ end
88
+
89
+ def inspect
90
+ outline_view(@options[:view])
91
+ end
92
+
93
+ def duplicate_values
94
+ array_duplicates(values)
95
+ end
96
+
97
+ def array_duplicates(array)
98
+ hash = array.inject({}) {|h,e|
99
+ h[e] ||= 0
100
+ h[e] += 1
101
+ h
102
+ }
103
+ hash.delete_if {|k,v| v<=1}
104
+ hash.keys
105
+ end
106
+
107
+ end
108
+
109
+ class TagGroup < NamespaceGroup #:nodoc:
110
+ def namespaces
111
+ tags.map(&:namespace).uniq
112
+ end
113
+
114
+ def outline_view(type=nil)
115
+ "\n" + namespace_groups.map {|e| e.outline_view(type) }.join("\n")
116
+ end
117
+
118
+ def inspect; super; end
119
+
120
+ def namespace_tags
121
+ tags.inject({}) {|h,t|
122
+ (h[t.namespace] ||= []) << t
123
+ h
124
+ }
125
+ end
126
+
127
+ def namespace_groups
128
+ unless @namespace_groups
129
+ @namespace_groups = namespace_tags.map {|name, tags|
130
+ NamespaceGroup.new(name, :tags=>tags)
131
+ }
132
+ end
133
+ @namespace_groups
134
+ end
135
+
136
+ def tags
137
+ @tags ||= Tag.machine_tags(@name)
138
+ end
139
+ end
140
+
141
+ class QueryGroup < TagGroup #:nodoc:
142
+ def tags
143
+ @tags ||= Url.tagged_with(@name).map(&:tags).flatten.uniq
144
+ end
145
+ end
146
+ end
@@ -1,5 +1,5 @@
1
1
  # == Machine Tags
2
- # Machine tags are in the format:
2
+ # Machine tags, also known as triple tags, are in the format:
3
3
  # [namespace]:[predicate]=[value]
4
4
  #
5
5
  # As explained here[http://www.flickr.com/groups/api/discuss/72157594497877875],
@@ -65,12 +65,13 @@
65
65
 
66
66
  class Tag < ActiveRecord::Base
67
67
  has_many :taggings
68
+
68
69
  validates_presence_of :name
69
70
  validates_uniqueness_of :name
70
71
  before_save :update_name_related_columns
71
72
 
72
73
  NAMESPACE_REGEX = "[a-z](?:[a-z0-9_]+)"
73
- PREDICATE_REGEX = "[a-z](?:[a-z0-9_]+)"
74
+ PREDICATE_REGEX = "[a-z](?:[a-z0-9_-]+)"
74
75
  VALUE_REGEX = '.+'
75
76
 
76
77
  #disallow machine tags special characters and tag list delimiter OR allow machine tag format
@@ -79,16 +80,21 @@ class Tag < ActiveRecord::Base
79
80
  named_scope :namespace_counts, :select=>'*, namespace as counter, count(namespace) as count', :group=>"namespace HAVING count(namespace)>=1"
80
81
  named_scope :predicate_counts, :select=>'*, predicate as counter, count(predicate) as count', :group=>"predicate HAVING count(predicate)>=1"
81
82
  named_scope :value_counts, :select=>'*, value as counter, count(value) as count', :group=>"value HAVING count(value)>=1"
82
- named_scope :namespace, lambda {|namespace| {:conditions=>{:namespace=>namespace}} }
83
- named_scope :predicate, lambda {|predicate| {:conditions=>{:predicate=>predicate}} }
84
- named_scope :value, lambda {|value| {:conditions=>{:value=>value}} }
83
+ named_scope :distinct_namespaces, :select=>"distinct namespace"
84
+ named_scope :distinct_predicates, :select=>"distinct predicate"
85
+ named_scope :distinct_values, :select=>"distinct value"
85
86
 
87
+ def self.namespaces; distinct_namespaces.map(&:namespace).compact; end
88
+ def self.predicates; distinct_predicates.map(&:predicate).compact; end
89
+ def self.values; distinct_values.map(&:value).compact; end
90
+
86
91
  # To be used with the *counts methods.
87
92
  # For example:
88
93
  # stat(:namespace_counts)
89
94
  # This prints out pairs of a namespaces and their counts in the tags table.
90
95
  def self.stat(type)
91
- send(type).map {|e| [e.counter, e.count] }
96
+ shortcuts = {:n=>:namespace_counts, :p=>:predicate_counts, :v=>:value_counts }
97
+ send(shortcuts[type] || type).map {|e| [e.counter, e.count] }
92
98
  end
93
99
 
94
100
  # Takes a wildcard machine tag and returns matching tags.
@@ -1,3 +1,4 @@
1
1
  class Tagging < ActiveRecord::Base
2
2
  belongs_to :tag
3
+ belongs_to :taggable, :polymorphic => true
3
4
  end
@@ -7,8 +7,12 @@ module HasMachineTags
7
7
  base.extend(ClassMethods)
8
8
  end
9
9
 
10
- module ClassMethods #:nodoc:
11
- def has_machine_tags
10
+ module ClassMethods
11
+ # Options
12
+ # :reverse_has_many - Defines a has_many :through from tags to the model using the plural of the model name.
13
+ def has_machine_tags(options={})
14
+ cattr_accessor :quick_mode
15
+ self.quick_mode = options[:quick_mode] || false
12
16
  self.class_eval do
13
17
  has_many :taggings, :as=>:taggable, :dependent=>:destroy
14
18
  has_many :tags, :through=>:taggings
@@ -17,11 +21,17 @@ module HasMachineTags
17
21
  include HasMachineTags::InstanceMethods
18
22
  extend HasMachineTags::SingletonMethods
19
23
  if respond_to?(:named_scope)
20
- named_scope :tagged_with, lambda{ |tags, options|
21
- find_options_for_find_tagged_with(tags, options)
24
+ named_scope :tagged_with, lambda{ |*args|
25
+ find_options_for_find_tagged_with(*args)
22
26
  }
23
27
  end
24
28
  end
29
+ if options[:reverse_has_many]
30
+ model = self.to_s
31
+ 'Tag'.constantize.class_eval do
32
+ has_many(model.tableize, :through => :taggings, :source => :taggable, :source_type =>model)
33
+ end
34
+ end
25
35
  end
26
36
  end
27
37
 
@@ -30,17 +40,17 @@ module HasMachineTags
30
40
  # Note that each tag is interpreted as a possible wildcard machine tag.
31
41
  #
32
42
  # Options:
33
- # :exclude - Find models that are not tagged with the given tags
34
- # :match_all - Find models that match all of the given tags, not just one, default: true
35
- # :conditions - A piece of SQL conditions to add to the query
43
+ # :exclude - Find models that are not tagged with the given tags.
44
+ # :match_all - Find models that match all of the given tags, not just one (doesn't work with machine tags yet).
45
+ # :conditions - A piece of SQL conditions to add to the query.
36
46
  #
37
47
  # Example:
38
48
  # Url.tagged_with 'something' # => fetches urls tagged with 'something'
39
49
  # Url.tagged_with 'gem:' # => fetches urls tagged with tags that have namespace gem
40
- # Url.tagged_with 'gem:, something' # => fetches urls that are tagged with 'something'
41
- # and tags that have namespace gem
50
+ # Url.tagged_with 'gem, something' # => fetches urls that are tagged with 'something'
51
+ # and 'gem'
42
52
  #
43
- # Note: This method really only needs to be used for Rails < 2.1 .
53
+ # Note: This method really only needs to be used with Rails < 2.1 .
44
54
  # Rails 2.1 and greater should use tagged_with(), which acts the same but with
45
55
  # the benefits of named_scope.
46
56
  #
@@ -50,7 +60,7 @@ module HasMachineTags
50
60
  end
51
61
 
52
62
  def find_options_for_find_tagged_with(tags, options = {}) #:nodoc:
53
- options.reverse_merge!(:match_all=>true)
63
+ # options.reverse_merge!(:match_all=>true)
54
64
  tags = TagList.new(tags)
55
65
  return {} if tags.empty?
56
66
 
@@ -59,12 +69,14 @@ module HasMachineTags
59
69
 
60
70
  taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"
61
71
 
72
+ machine_tag_used = false
62
73
  if options.delete(:exclude)
63
74
  tags_conditions = tags.map { |t| sanitize_sql(["#{Tag.table_name}.name = ?", t]) }.join(" OR ")
64
75
  conditions << sanitize_sql(["#{table_name}.id NOT IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} LEFT OUTER JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id WHERE (#{tags_conditions}) AND #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})", tags])
65
76
  else
66
- conditions << tags.map {|t|
77
+ tag_sql = tags.map {|t|
67
78
  if match = Tag.match_wildcard_machine_tag(t)
79
+ machine_tag_used = true
68
80
  string = match.map {|k,v|
69
81
  sanitize_sql(["#{tags_alias}.#{k} = ?", v])
70
82
  }.join(" AND ")
@@ -73,9 +85,17 @@ module HasMachineTags
73
85
  sanitize_sql(["#{tags_alias}.name = ?", t])
74
86
  end
75
87
  }.join(" OR ")
76
-
88
+ conditions << tag_sql
89
+
77
90
  if options.delete(:match_all)
78
- group = "#{taggings_alias}.taggable_id HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
91
+ group = "#{taggings_alias}.taggable_id HAVING COUNT(#{taggings_alias}.taggable_id) = "
92
+ if machine_tag_used
93
+ #Since a machine tag matches multiple tags per given tag, we need to dynamically calculate the count
94
+ #TODO: this select needs to return differently for each taggable_id
95
+ group += "(SELECT count(id) FROM #{Tag.table_name} #{tags_alias} WHERE #{tag_sql})"
96
+ else
97
+ group += tags.size.to_s
98
+ end
79
99
  end
80
100
  end
81
101
 
@@ -89,9 +109,25 @@ module HasMachineTags
89
109
  end
90
110
 
91
111
  module InstanceMethods
112
+
113
+ def quick_mode_tag_list(list) #:nodoc:
114
+ mtag_list = TagList.new(list)
115
+ mtag_list = mtag_list.map {|e|
116
+ if e.include?(":")
117
+ namespace,other = e.split(":")
118
+ other.split(";").map {|e|
119
+ e.include?("=") ? "#{namespace}:#{e}" : "#{namespace}:tags=#{e}"
120
+ }
121
+ else
122
+ e
123
+ end
124
+ }.flatten
125
+ TagList.new(mtag_list)
126
+ end
127
+
92
128
  # Set tag list with an array of tags or comma delimited string of tags
93
129
  def tag_list=(list)
94
- @tag_list = TagList.new(list)
130
+ @tag_list = quick_mode ? quick_mode_tag_list(list) : TagList.new(list)
95
131
  end
96
132
 
97
133
  # Fetches latest tag list for an object
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cldwalker-has_machine_tags
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriel Horner
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-02-12 00:00:00 -08:00
12
+ date: 2009-02-20 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -25,11 +25,15 @@ extra_rdoc_files:
25
25
  files:
26
26
  - README.rdoc
27
27
  - LICENSE.txt
28
+ - init.rb
29
+ - Rakefile
30
+ - VERSION.yml
28
31
  - generators/has_machine_tags_migration
29
32
  - generators/has_machine_tags_migration/has_machine_tags_migration_generator.rb
30
33
  - generators/has_machine_tags_migration/templates
31
34
  - generators/has_machine_tags_migration/templates/migration.rb
32
35
  - lib/has_machine_tags
36
+ - lib/has_machine_tags/namespace_group.rb
33
37
  - lib/has_machine_tags/tag.rb
34
38
  - lib/has_machine_tags/tag_list.rb
35
39
  - lib/has_machine_tags/tagging.rb