tagtical 1.5.9 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -39,6 +39,8 @@ parsed.
39
39
 
40
40
  7. Custom contexts are *not* supported since we use STI. That is, you must define a set of tag types
41
41
  ahead of time.
42
+ 8. Functionality for joining across tags (seeing which tags two different models have in common)
43
+ with both superset and subset matching.
42
44
 
43
45
  Additions include:
44
46
  1. Scopes are created on Tag so you can do photo.tags.color and grab all the tags of type Tag::Color, for example.
@@ -282,6 +284,40 @@ just define the subclass and the code will automatically instantiate it as that
282
284
 
283
285
  Now moving forward, these classes will be instantiated with this model. Wow cool!
284
286
 
287
+ === Tag Groups
288
+
289
+ This functionality allows to join models across tags (seeing which tags two different models have in common)
290
+ with both superset and subset matching.
291
+
292
+ You can do it simply by providing :has_many_through_tags call in taggable model, like:
293
+
294
+ class User < ActiveRecord::Base
295
+ acts_as_taggable :skills
296
+ has_many_through_tags :schools, :superset
297
+ end
298
+
299
+ class School < ActiveRecord::Base
300
+ acts_as_taggable :skills
301
+ has_many_through_tags :users # :subset is default
302
+ end
303
+
304
+ So, users can study in a school only if they have all the required skills. We want to get
305
+ list of all schools of a user and all users of a school.
306
+
307
+ school1.skill_list = "math, biology"
308
+ school2.skill_list = "programming, chemistry"
309
+ user.skill_list = "chemistry, math, biology"
310
+
311
+ user.schools # => [school1]
312
+ school1.users # => [user]
313
+ school2.users # => []
314
+
315
+ You also may specify different class name, if you want to name your association differently:
316
+
317
+ class User < ActiveRecord::Base
318
+ acts_as_taggable :skills
319
+ has_many_through_tags :possible_schools, :superset, class_name: "School"
320
+ end
285
321
 
286
322
  === Tag Ownership
287
323
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.5.8
1
+ 1.6.0
@@ -2,7 +2,7 @@ class TagticalMigration < ActiveRecord::Migration
2
2
  def self.up
3
3
  create_table :tags do |t|
4
4
  t.column :value, :string
5
- t.column :type, :string
5
+ t.column :type, :string, :limit => 100
6
6
  end
7
7
  add_index :tags, [:type, :value], :unique => true
8
8
  add_index :tags, :value
@@ -12,11 +12,11 @@ class TagticalMigration < ActiveRecord::Migration
12
12
  t.column :tag_id, :integer
13
13
  t.column :taggable_id, :integer
14
14
  t.column :tagger_id, :integer
15
- t.column :tagger_type, :string if Tagtical.config.polymorphic_tagger?
15
+ t.column :tagger_type, :string, :limit => 100 if Tagtical.config.polymorphic_tagger?
16
16
 
17
17
  # You should make sure that the column created is
18
18
  # long enough to store the required class names.
19
- t.column :taggable_type, :string
19
+ t.column :taggable_type, :string, :limit => 100
20
20
 
21
21
  t.column :created_at, :datetime
22
22
  end
@@ -2,7 +2,7 @@ class TagticalMigration < ActiveRecord::Migration
2
2
  def self.up
3
3
  create_table :tags do |t|
4
4
  t.string :value
5
- t.string :type
5
+ t.string :type, :limit => 100
6
6
  end
7
7
  add_index :tags, [:type, :value], :unique => true
8
8
  add_index :tags, :value
@@ -13,9 +13,9 @@ class TagticalMigration < ActiveRecord::Migration
13
13
 
14
14
  # You should make sure that the column created is
15
15
  # long enough to store the required class names.
16
- t.references :taggable, :polymorphic => true
16
+ t.references :taggable, :polymorphic => true, :limit => 100
17
17
  if Tagtical.config.polymorphic_tagger?
18
- t.references :tagger, :polymorphic => true
18
+ t.references :tagger, :polymorphic => true, :limit => 100
19
19
  else
20
20
  t.integer :tagger_id
21
21
  end
data/lib/tagtical/tag.rb CHANGED
@@ -469,6 +469,44 @@ module Tagtical
469
469
  Array(input).map { |o| taggable_class.find_tag_type!(o).klass }
470
470
  end
471
471
 
472
+ # Instead of subclassing from Array, we make a Proxy, so we actually can use method_missing
473
+ # for catching results and converting them into Collection class if they return Array
474
+ class Collection
475
+ def initialize(*args)
476
+ @array = Array.new(*args)
477
+ end
478
+
479
+ # Usage:
480
+ # tag_types.get(["boy", "girl"]) # => ["boy", "girl"]
481
+ # tag_types.get("boy") # => "boy"
482
+ # # "boy".class === Tagtical::Tag::Type everywhere
483
+ def get(tag_type_names)
484
+ results = @array.select { |tag_type| tag_type_names.include?(tag_type) }
485
+ tag_type_names.is_a?(Array) ? results : results.first
486
+ end
487
+
488
+ def respond_to?(*args)
489
+ super || @array.respond_to?(*args)
490
+ end
491
+
492
+ def to_ary
493
+ @array
494
+ end
495
+
496
+ def to_a
497
+ @array
498
+ end
499
+
500
+ def method_missing(name, *args)
501
+ result = if block_given?
502
+ @array.send(name, *args) { |*block_args| yield(*block_args) }
503
+ else
504
+ @array.send(name, *args)
505
+ end
506
+ result.is_a?(@array.class) ? self.class.new(result) : result
507
+ end
508
+ end
509
+
472
510
  end
473
511
  end
474
512
  end
@@ -14,7 +14,7 @@ module Tagtical::Taggable
14
14
  empty_tag_types = tag_types.select do |tag_type|
15
15
  Array.wrap(empty_tags_type_names).map(&:to_s).include?(tag_type)
16
16
  end
17
- empty_tag_type_classes = empty_tag_types.map { |t| t.klass.to_s }
17
+ empty_tag_type_classes = empty_tag_types.map { |t| t.klass.to_s }.to_a
18
18
  if empty_tag_type_classes.present?
19
19
  tagging_table = Tagtical::Tagging.arel_table
20
20
  tag_table = Tagtical::Tag.arel_table
@@ -359,6 +359,25 @@ module Tagtical::Taggable
359
359
  true
360
360
  end
361
361
 
362
+ # Returns flat list of tag classes and tag values.
363
+ #
364
+ # taggable.update_attributes!(gender_list: "boy, girl", skill_list: "ruby")
365
+ # taggable.short_tags # => %w(gender_boy gender_girl skill_ruby tag_boy tag_girl tag_ruby)
366
+ # taggable.short_tags(only: :gender) # => %w(gender_boy gender_girl)
367
+ # taggable.short_tags(exclude: :gender) # => %w(skill_ruby tag_boy tag_girl tag_ruby)
368
+ def short_tags(options = {})
369
+ tag_types = if options[:only].present?
370
+ self.class.tag_types.select { |tt| Array.wrap(options[:only]).map(&:to_s).include?(tt) }
371
+ else
372
+ self.class.tag_types - Array.wrap(options[:exclude]).compact.map(&:to_s)
373
+ end
374
+ tag_types.map do |tag_type|
375
+ send(tag_type.has_many_name).map do |tag|
376
+ "#{tag_type}_#{tag.value}"
377
+ end
378
+ end.flatten.to_a
379
+ end
380
+
362
381
 
363
382
  private
364
383
 
@@ -0,0 +1,51 @@
1
+ module Tagtical
2
+ module Taggable
3
+ module TagGroup
4
+
5
+ def has_many_through_tags(association_id, type = :subset, options = {})
6
+ case type
7
+ when :subset then has_many_through_tags_subset(association_id, options)
8
+ when :superset then has_many_through_tags_superset(association_id, options)
9
+ else raise "Wrong association type, should be :subset or :superset"
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def has_many_through_tags_superset(association_id, options)
16
+ define_method(association_id) do
17
+ result = instance_variable_get("@#{association_id}") || begin
18
+ klass = (options[:class_name] || association_id.to_s.singularize.camelize).constantize
19
+ klass.
20
+ joins(
21
+ "LEFT JOIN #{Tagtical::Tagging.table_name} AS t1 " +
22
+ "ON t1.taggable_id = #{klass.table_name}.id AND t1.taggable_type = '#{klass}'"
23
+ ).
24
+ joins(
25
+ "LEFT JOIN #{Tagtical::Tagging.table_name} AS t2 " +
26
+ "ON t2.tag_id = t1.tag_id AND t2.taggable_type = '#{self.class}' AND t2.taggable_id = #{id}"
27
+ ).
28
+ group("#{klass.table_name}.id").
29
+ having("COUNT(t2.tag_id) = COUNT(#{klass.table_name}.id)")
30
+ end
31
+ instance_variable_set("@#{association_id}", result)
32
+ result
33
+ end
34
+ end
35
+
36
+ def has_many_through_tags_subset(association_id, options)
37
+ define_method(association_id) do
38
+ result = instance_variable_get("@#{association_id}") || begin
39
+ klass = (options[:class_name] || association_id.to_s.singularize.camelize).constantize
40
+ klass.
41
+ joins(:taggings).
42
+ where("#{Tagtical::Tagging.table_name}.`tag_id` IN (?)", taggings.map(&:tag_id)).
43
+ group("#{klass.table_name}.id").having(["count(#{klass.table_name}.id) = ?", taggings.count])
44
+ end
45
+ instance_variable_set("@#{association_id}", result)
46
+ result
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -22,13 +22,14 @@ module Tagtical
22
22
  def acts_as_taggable(*tag_types)
23
23
  tag_types.flatten!
24
24
  tag_types << Tagtical::Tag::Type::BASE # always include the base type.
25
- tag_types = Tagtical::Tag::Type.register(tag_types.uniq.compact, self)
25
+ tag_types = Tagtical::Tag::Type::Collection.new(Tagtical::Tag::Type.register(tag_types.uniq.compact, self))
26
26
 
27
27
  if taggable?
28
28
  self.tag_types = (self.tag_types + tag_types).uniq
29
29
  else
30
- class_attribute(:tag_types)
30
+ class_attribute :tag_types, :custom_tag_types
31
31
  self.tag_types = tag_types
32
+ self.custom_tag_types = tag_types - [Tagtical::Tag::Type::BASE]
32
33
 
33
34
  has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "Tagtical::Tagging"
34
35
  has_many :tags, :through => :taggings, :class_name => "Tagtical::Tag"
@@ -45,9 +46,13 @@ module Tagtical
45
46
  include Tagtical::Taggable::Cache
46
47
  include Tagtical::Taggable::Ownership
47
48
  include Tagtical::Taggable::Related
49
+ extend Tagtical::Taggable::TagGroup
48
50
 
49
51
  end
50
52
 
53
+ # Purpose of this is to keep additional tagtical class level methods grouped
54
+ # together with the acts_as_taggable call.
55
+ yield if block_given?
51
56
  end
52
57
  end
53
58
  end
data/lib/tagtical.rb CHANGED
@@ -33,6 +33,7 @@ require "tagtical/taggable/collection"
33
33
  require "tagtical/taggable/cache"
34
34
  require "tagtical/taggable/ownership"
35
35
  require "tagtical/taggable/related"
36
+ require "tagtical/taggable/tag_group"
36
37
 
37
38
  require "tagtical/acts_as_tagger"
38
39
  require "tagtical/tag"
data/spec/models.rb CHANGED
@@ -35,6 +35,7 @@ end
35
35
  class TaggableModel < ActiveRecord::Base
36
36
  acts_as_taggable(:languages, :skills, {:crafts => Tag::FooCraft}, :needs, :offerings, {:styles => "BarCraft"})
37
37
  has_many :untaggable_models
38
+ has_many_through_tags :custom_groups, :superset
38
39
  end
39
40
 
40
41
  class CachedModel < ActiveRecord::Base
@@ -59,3 +60,9 @@ end
59
60
  class UntaggableModel < ActiveRecord::Base
60
61
  belongs_to :taggable_model
61
62
  end
63
+
64
+ class CustomGroup < ActiveRecord::Base
65
+ acts_as_taggable(:skills, :languages) do
66
+ has_many_through_tags :taggable_models
67
+ end
68
+ end
data/spec/schema.rb CHANGED
@@ -43,4 +43,8 @@ ActiveRecord::Schema.define :version => 0 do
43
43
  t.column :name, :string
44
44
  t.column :type, :string
45
45
  end
46
+
47
+ create_table :custom_groups, :force => true do |t|
48
+ t.column :name, :string
49
+ end
46
50
  end
@@ -0,0 +1,29 @@
1
+ require File.expand_path('../../spec_helper', __FILE__)
2
+
3
+ describe Tagtical::Taggable::TagGroup do
4
+
5
+ before { clean_database! }
6
+
7
+ it "should retrieve related tagged objects" do
8
+ group = CustomGroup.create!(language_list: "ruby", skill_list: "programmer")
9
+ objects = [
10
+ {:name => "One", language_list: "ruby, c", skill_list: "programmer"},
11
+ {:name => "Two", language_list: "c", skill_list: "programmer"},
12
+ {:name => "Three", language_list: "ruby", skill_list: "manager"},
13
+ {:name => "Four", language_list: "ruby", skill_list: "programmer"}
14
+ ].map { |attrs| TaggableModel.create!(attrs) }
15
+ group.taggable_models.should == [objects[0], objects[3]]
16
+ end
17
+
18
+ it "should retrieve associated tag groups" do
19
+ groups = [
20
+ {language_list: "ruby, c", skill_list: "programmer", name: "Ruby C Programmer"},
21
+ {language_list: "c", skill_list: "programmer", name: "C Programmer"},
22
+ {language_list: "ruby", skill_list: "manager", name: "Ruby Manager"},
23
+ {language_list: "ruby", skill_list: "programmer", name: "Ruby Programmer"}
24
+ ].map { |attrs| CustomGroup.create!(attrs) }
25
+ model = TaggableModel.create!(language_list: "ruby, c", skill_list: "programmer")
26
+ model.custom_groups.should == [groups[0], groups[1], groups[3]]
27
+ end
28
+
29
+ end
@@ -16,10 +16,40 @@ describe Tagtical::Taggable do
16
16
  end
17
17
 
18
18
  it "should have tag types" do
19
- TaggableModel.tag_types.should include("tag", "language", "skill", "craft", "need", "offering")
19
+ TaggableModel.tag_types.should include(Tagtical::Tag::Type::BASE, "language", "skill", "craft", "need", "offering")
20
20
  @taggable.tag_types.should == TaggableModel.tag_types
21
21
  end
22
22
 
23
+ describe ".custom_tag_types" do
24
+ subject { TaggableModel.custom_tag_types }
25
+ it { should include("language", "skill", "craft", "need", "offering") }
26
+ it { should_not include(Tagtical::Tag::Type::BASE) }
27
+ it { should == @taggable.custom_tag_types }
28
+ end
29
+
30
+ describe ".tag_types" do
31
+ subject { TaggableModel.tag_types }
32
+ it { should be_a(Tagtical::Tag::Type::Collection) }
33
+ its(:first) { should be_a(Tagtical::Tag::Type) }
34
+ end
35
+
36
+ describe "tag types collection" do
37
+
38
+ it "should return its class instance after executing Array's methods" do
39
+ TaggableModel.tag_types.select { |t| t == "tag" }.should be_a(Tagtical::Tag::Type::Collection)
40
+ end
41
+
42
+ context "get tag types" do
43
+ specify "by array" do
44
+ TaggableModel.tag_types.get(["language", "skill"]).should =~ ["skill", "language"]
45
+ end
46
+
47
+ specify "by item" do
48
+ TaggableModel.tag_types.get("language").should == "language"
49
+ end
50
+ end
51
+ end
52
+
23
53
  it "should have tag_counts_on" do
24
54
  TaggableModel.tag_counts_on(:tags).all.should be_empty
25
55
 
@@ -30,6 +60,15 @@ describe Tagtical::Taggable do
30
60
  @taggable.tag_counts_on(:tags).length.should == 2
31
61
  end
32
62
 
63
+ describe ".short_tags" do
64
+ subject { @taggable }
65
+ before { subject.update_attributes!(skill_list: %w(ruby rails), tag_list: %w(red)) }
66
+
67
+ its(:short_tags) { should == %w(skill_ruby skill_rails tag_red tag_ruby tag_rails) }
68
+ specify { subject.short_tags(only: :skill).should == %w(skill_ruby skill_rails) }
69
+ specify { subject.short_tags(exclude: :skill).should == %w(tag_red tag_ruby tag_rails) }
70
+ end
71
+
33
72
  it "should be able to create tags" do
34
73
  @taggable.skill_list = "ruby, rails, css"
35
74
  @taggable.tag_list_on(:skills).should be_an_instance_of(Tagtical::TagList)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tagtical
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.9
4
+ version: 1.6.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -14,7 +14,7 @@ default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails
17
- requirement: &2165943100 !ruby/object:Gem::Requirement
17
+ requirement: &2157842540 !ruby/object:Gem::Requirement
18
18
  none: false
19
19
  requirements:
20
20
  - - ~>
@@ -22,10 +22,10 @@ dependencies:
22
22
  version: '3.0'
23
23
  type: :runtime
24
24
  prerelease: false
25
- version_requirements: *2165943100
25
+ version_requirements: *2157842540
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: sqlite3
28
- requirement: &2165942720 !ruby/object:Gem::Requirement
28
+ requirement: &2157878760 !ruby/object:Gem::Requirement
29
29
  none: false
30
30
  requirements:
31
31
  - - ! '>='
@@ -33,10 +33,10 @@ dependencies:
33
33
  version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
- version_requirements: *2165942720
36
+ version_requirements: *2157878760
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: mysql
39
- requirement: &2165942260 !ruby/object:Gem::Requirement
39
+ requirement: &2157878300 !ruby/object:Gem::Requirement
40
40
  none: false
41
41
  requirements:
42
42
  - - ! '>='
@@ -44,10 +44,10 @@ dependencies:
44
44
  version: '0'
45
45
  type: :development
46
46
  prerelease: false
47
- version_requirements: *2165942260
47
+ version_requirements: *2157878300
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: rspec
50
- requirement: &2165941840 !ruby/object:Gem::Requirement
50
+ requirement: &2157877880 !ruby/object:Gem::Requirement
51
51
  none: false
52
52
  requirements:
53
53
  - - ! '>='
@@ -55,7 +55,7 @@ dependencies:
55
55
  version: '0'
56
56
  type: :development
57
57
  prerelease: false
58
- version_requirements: *2165941840
58
+ version_requirements: *2157877880
59
59
  description: Tagtical allows you do create subclasses for Tag and add additional functionality
60
60
  in an STI fashion. For example. You could do Tag::Color.find_by_name('blue').to_rgb.
61
61
  It also supports storing weights or relevance on the taggings.
@@ -80,6 +80,7 @@ files:
80
80
  - lib/tagtical/taggable/core.rb
81
81
  - lib/tagtical/taggable/ownership.rb
82
82
  - lib/tagtical/taggable/related.rb
83
+ - lib/tagtical/taggable/tag_group.rb
83
84
  - lib/tagtical/taggable.rb
84
85
  - lib/tagtical/tagging.rb
85
86
  - lib/tagtical/tags_helper.rb
@@ -97,6 +98,7 @@ files:
97
98
  - spec/schema.rb
98
99
  - spec/spec_helper.rb
99
100
  - spec/tagtical/acts_as_tagger_spec.rb
101
+ - spec/tagtical/tag_group_spec.rb
100
102
  - spec/tagtical/tag_list_spec.rb
101
103
  - spec/tagtical/tag_spec.rb
102
104
  - spec/tagtical/taggable_spec.rb
@@ -139,6 +141,7 @@ test_files:
139
141
  - spec/schema.rb
140
142
  - spec/spec_helper.rb
141
143
  - spec/tagtical/acts_as_tagger_spec.rb
144
+ - spec/tagtical/tag_group_spec.rb
142
145
  - spec/tagtical/tag_list_spec.rb
143
146
  - spec/tagtical/tag_spec.rb
144
147
  - spec/tagtical/taggable_spec.rb