cldwalker-has_machine_tags 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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