culturecode-acts_as_taggable 0.2.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 805daa96b56881db74f2db609fbdbe17c8b398b0
4
- data.tar.gz: 97f49a0d3a0e429ad1f28c774eb71a379d49fb44
2
+ SHA256:
3
+ metadata.gz: 192c7c327986f8aeb8d2bdeb9a735b72c4cd833e11450b75eb7126d88d2e55e6
4
+ data.tar.gz: 8532639665cbef5350856e35878983f6058a16fabf4e0eff0c01192d49251b64
5
5
  SHA512:
6
- metadata.gz: f4bb21307f9e5e2981b02f98296a8a451f0cbb93619f48f646adc0b09905b19bc9784da654ecbda7450ee494a3c7a4838690e33e8922815199207183d681519b
7
- data.tar.gz: 2723ee05f50b1a646b7b4ae0a4b505dee171ca0637cea8d3230d01e4b3ebe539705dfee0091e4c59527be01b1a7fa08c910e98062ab3858ef8c96db002de8c2d
6
+ metadata.gz: 19d1205d39b3ab97f410eea7ba0a15e03563c921b6ca7533633812b658bc5f3e9c4ddcae0e4ffc6b459c3482d5d2bd4a1362c94dd40c503f71c6989af262855d
7
+ data.tar.gz: '09782afefb72be61de7138490d3cb2f804c3969d3a3dedf2139a393a6c8649bf11a71488eba4e00ae34bb173bb585c297f45ed905ff58e097eb39382029ceef6'
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2015 Nicholas Jakobsen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'ActsAsTaggable'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+
21
+
22
+ Bundler::GemHelper.install_tasks
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'lib'
28
+ t.libs << 'test'
29
+ t.pattern = 'test/**/*_test.rb'
30
+ t.verbose = false
31
+ end
32
+
33
+
34
+ task default: :test
@@ -2,4 +2,7 @@ require 'acts_as_taggable/acts_as_taggable'
2
2
  require 'acts_as_taggable/tag'
3
3
  require 'acts_as_taggable/tagging'
4
4
 
5
- ActiveRecord::Base.extend ActsAsTaggable::ActMethod
5
+ module ActsAsTaggable
6
+ end
7
+
8
+ ActiveRecord::Base.extend ActsAsTaggable::ActMethod
@@ -1,47 +1,190 @@
1
1
  module ActsAsTaggable
2
2
  module ActMethod #:nodoc:
3
3
  def acts_as_taggable(options = {})
4
- has_many :taggings, :as => :taggable
5
- has_many :tags, :through => :taggings
4
+ class_attribute :acts_as_taggable_options
5
+ self.acts_as_taggable_options = options
6
+ self.acts_as_taggable_options.reverse_merge! :delimiter => ',', :downcase => true, :remove_tag_if_empty => true
7
+ self.acts_as_taggable_options.reverse_merge! :output_delimiter => acts_as_taggable_options[:delimiter]
8
+ self.acts_as_taggable_options[:types] = Array(self.acts_as_taggable_options[:types])
6
9
 
7
- extend ActsAsTaggable::ClassMethods
8
- include ActsAsTaggable::InstanceMethods
9
- end
10
+ has_many :taggings, -> { order("#{Tagging.table_name}.id") }, :as => :taggable, :after_remove => :delete_tag_if_necessary, :dependent => :destroy, :class_name => 'ActsAsTaggable::Tagging'
11
+ has_many :tags, :through => :taggings, :class_name => 'ActsAsTaggable::Tag', :after_add => :reset_scoped_associations
12
+
13
+ extend ClassMethods
14
+ include InstanceMethods
15
+
16
+ self.acts_as_taggable_options[:types].each do |tag_type|
17
+ has_many :"#{tag_type}_taggings", -> { joins(:tag).order("#{Tagging.table_name}.id").where(Tag.table_name => {:tag_type => tag_type}) }, :as => :taggable, :after_remove => :delete_tag_if_necessary, :class_name => 'ActsAsTaggable::Tagging'
18
+ has_many :"#{tag_type}_tags", -> { where(:tag_type => tag_type) }, :through => :taggings, :source => :tag, :class_name => 'ActsAsTaggable::Tag', :after_add => :reset_associations
19
+
20
+ metaclass = class << self; self; end
21
+ HelperMethods.scope_class_methods(metaclass, tag_type)
22
+ HelperMethods.scope_instance_methods(self, tag_type)
23
+ end
24
+ end
10
25
  end
11
26
 
12
27
  module ClassMethods
13
- # TODO: tagged_with_all
14
-
15
- def tagged_with_any(*args)
16
- args.flatten! # Allow an array of tags to be passed in
17
28
 
18
- joins(:tags).where(:tags => {:name => args.collect {|tag_name| Tag.sanitize_name(tag_name) } }).uniq
29
+ def tagged_with_any(*tags)
30
+ tags = find_tags(tags)
31
+ return none if tags.empty?
32
+
33
+ table_alias = "alias_#{tags.hash.abs}"
34
+ scope = all.uniq.select "#{table_name}.*"
35
+ scope = scope.joins "JOIN #{Tagging.table_name} AS #{table_alias} ON #{table_alias}.taggable_id = #{table_name}.id"
36
+ scope = scope.where "#{table_alias}.tag_id" => tags
37
+
38
+ return scope
39
+ end
40
+
41
+ def tagged_with_all(*tags)
42
+ tags = find_tags(tags)
43
+ return none if tags.empty?
44
+
45
+ tags.inject(all.uniq) do |scope, tag|
46
+ scope = scope.joins "LEFT OUTER JOIN #{Tagging.table_name} AS alias_#{tag.id} ON alias_#{tag.id}.taggable_id = #{table_name}.id"
47
+ scope = scope.where "alias_#{tag.id}.tag_id" => tag
48
+ end
49
+ end
50
+
51
+ # Make it possible to ask for tags on a scoped Taggable relation. e.g. Users.online.applied_tags
52
+ def applied_tags
53
+ Tag.select("#{Tag.table_name}.*, COUNT(*) AS count").joins(:taggings).where(:taggable_type => self.name, Tagging.table_name => {:taggable_id => all}).group("#{Tag.table_name}.id")
54
+ end
55
+
56
+ def applied_tag_names
57
+ applied_tags.pluck(:name)
19
58
  end
20
59
 
21
- # Make it possible to ask for tags on a scoped Taggable relation. e.g. Users.online.tags
22
60
  def tags
23
- Tag.joins(:taggings).where(:taggings => {:taggable_type => self, :taggable_id => all}).group('tags.id').select("tags.*, COUNT(*) AS count")
61
+ Tag.where(:taggable_type => name)
62
+ end
63
+
64
+ def tag_names
65
+ tags.pluck(:name)
66
+ end
67
+
68
+ def create_tag(tag_name)
69
+ find_tags(tag_name).first || tags.create!(:name => tag_name)
70
+ end
71
+
72
+ # Given an unsanitized string or list of tags, Returns a list of tags
73
+ def find_tags(*input)
74
+ input = input.flatten
75
+ input = input.first if input.one?
76
+ case input
77
+ when Tag
78
+ HelperMethods.filter_tags_by_current_tag_scope([input])
79
+ when String
80
+ tags.where(:name => input.split(acts_as_taggable_options[:delimiter]).collect {|tag_name| tag_name.strip}).to_a
81
+ when Array
82
+ input.flat_map {|tag| find_tags(tag)}.select(&:present?).uniq
83
+ when ActiveRecord::Relation
84
+ input.uniq.to_a
85
+ else
86
+ []
87
+ end
24
88
  end
25
89
  end
26
90
 
27
91
  module InstanceMethods
28
- TAG_DELIMITER = ','
29
-
30
92
  def acts_like_taggable?
31
93
  true
32
94
  end
33
95
 
34
- def tags_list=(tag_string)
35
- self.tags = tag_string.to_s.split(TAG_DELIMITER).collect{|tag_name| Tag.find_or_create_by!(:name => Tag.sanitize_name(tag_name)) }
96
+ def tag_with(*tag_names)
97
+ self.tag_names = tag_names.flatten
36
98
  end
37
-
38
- def tags_list
39
- tag_names.join(TAG_DELIMITER + ' ')
99
+
100
+ def untag_with(*tag_names)
101
+ self.tag_names = self.tag_names - self.class.find_tags(tag_names).collect(&:name)
102
+ end
103
+
104
+ def tag_string
105
+ tag_names.join(acts_as_taggable_options[:output_delimiter] + ' ')
40
106
  end
41
-
107
+
108
+ def tag_string=(tag_string)
109
+ self.tag_names = tag_string.to_s.split(acts_as_taggable_options[:delimiter])
110
+ end
111
+
42
112
  def tag_names
43
- tags.collect(&:name) # don't use pluck since we want to use the cached association
113
+ send(HelperMethods.scoped_association_name).collect(&:name) # don't use pluck since we want to use the cached association
114
+ end
115
+
116
+ def tag_names=(names)
117
+ send HelperMethods.scoped_association_assignment_name, names.select(&:present?).collect {|tag_name| self.class.create_tag(tag_name) }
118
+ end
119
+
120
+ private
121
+
122
+ def delete_tag_if_necessary(tagging)
123
+ self.class.tags.where(:id => tagging.tag_id).destroy_all if acts_as_taggable_options[:remove_tag_if_empty] && tagging.tag.taggings.count == 0
124
+ end
125
+
126
+ def reset_scoped_associations(record)
127
+ return unless record.tag_type
128
+ send("#{record.tag_type}_taggings").reset
129
+ send("#{record.tag_type}_tags").reset
130
+ end
131
+
132
+ def reset_associations(record)
133
+ send(:taggings).reset
134
+ send(:tags).reset
44
135
  end
45
136
  end
46
- end
47
137
 
138
+ module HelperMethods
139
+ def self.scope_class_methods(metaclass, tag_type)
140
+ scope_tag_method(metaclass, tag_type, :create_tag, "create_#{tag_type}_tag")
141
+ scope_tag_method(metaclass, tag_type, :find_tags, "find_#{tag_type}_tags")
142
+ scope_tag_method(metaclass, tag_type, :tagged_with_any, "tagged_with_any_#{tag_type}")
143
+ scope_tag_method(metaclass, tag_type, :tagged_with_all, "tagged_with_all_#{tag_type.to_s.pluralize}")
144
+ scope_tag_method(metaclass, tag_type, :tags, "#{tag_type}_tags")
145
+ scope_tag_method(metaclass, tag_type, :tag_names, "#{tag_type}_tag_names")
146
+ scope_tag_method(metaclass, tag_type, :applied_tags, "applied_#{tag_type}_tags")
147
+ scope_tag_method(metaclass, tag_type, :applied_tag_names, "applied_#{tag_type}_tag_names")
148
+ end
149
+
150
+ def self.scope_instance_methods(klass, tag_type)
151
+ scope_tag_method(klass, tag_type, :tag_names, "#{tag_type}_tag_names")
152
+ scope_tag_method(klass, tag_type, :tag_names=, "#{tag_type}_tag_names=")
153
+ scope_tag_method(klass, tag_type, :tag_string, "#{tag_type}_tag_string")
154
+ scope_tag_method(klass, tag_type, :tag_string=, "#{tag_type}_tag_string=")
155
+ scope_tag_method(klass, tag_type, :tag_with, "tag_with_#{tag_type}")
156
+ scope_tag_method(klass, tag_type, :untag_with, "untag_with_#{tag_type}")
157
+ end
158
+
159
+ def self.scope_tag_method(context, tag_type, method_name, scoped_method_name)
160
+ context.send :define_method, scoped_method_name do |*args|
161
+ Tag.where(:tag_type => tag_type).scoping do
162
+ send(method_name, *args)
163
+ end
164
+ end
165
+ end
166
+
167
+ # Filters an array of tags by the current tag scope
168
+ def self.filter_tags_by_current_tag_scope(tags)
169
+ return tags unless current_tag_scope
170
+ tags.select do |tag|
171
+ current_tag_scope.all? do |attribute, value|
172
+ tag[attribute].to_s == value.to_s
173
+ end
174
+ end
175
+ end
176
+
177
+ def self.scoped_association_name
178
+ current_tag_scope ? "#{current_tag_scope['tag_type']}_tags" : "tags"
179
+ end
180
+
181
+ def self.scoped_association_assignment_name
182
+ current_tag_scope ? "#{current_tag_scope['tag_type']}_tags=" : "tags="
183
+ end
184
+
185
+ # Returns the current tag scope, e.g. :tag_type => 'material'
186
+ def self.current_tag_scope
187
+ Tag.current_scope.where_values_hash if Tag.current_scope
188
+ end
189
+ end
190
+ end
@@ -1,21 +1,32 @@
1
- class Tag < ActiveRecord::Base
2
- has_many :taggings
3
- default_scope lambda { order(:name) }
4
-
5
- validates_presence_of :name
6
- before_save :sanitize_name
7
-
8
- def self.sanitize_name(name)
9
- name.to_s.strip.squeeze(' ').downcase
10
- end
11
-
12
- def to_s
13
- self.name
14
- end
1
+ module ActsAsTaggable
2
+ class Tag < ActiveRecord::Base
3
+ self.table_name = "acts_as_taggable_tags"
4
+
5
+ has_many :taggings, :dependent => :destroy
6
+ default_scope lambda { order(:name) }
7
+
8
+ validates_presence_of :name, :taggable_type
9
+ before_save :sanitize_name
10
+
11
+ def to_s
12
+ self.name
13
+ end
14
+
15
+ def taggable_class
16
+ self.taggable_type.constantize
17
+ end
18
+
19
+ # Returns a cache key that is unique based on the last time the tags were updated or applied
20
+ def taggings_cache_key
21
+ [name, taggings.maximum(:id), taggings.count]
22
+ end
23
+
24
+ private
15
25
 
16
- private
17
-
18
- def sanitize_name
19
- self.name = self.class.sanitize_name(self.name)
26
+ def sanitize_name
27
+ name = self.name.to_s.squish
28
+ name.downcase! if taggable_class.acts_as_taggable_options[:downcase]
29
+ self.name = name
30
+ end
20
31
  end
21
- end
32
+ end
@@ -1,4 +1,17 @@
1
- class Tagging < ActiveRecord::Base
2
- belongs_to :tag
3
- belongs_to :taggable, :polymorphic => true
4
- end
1
+ module ActsAsTaggable
2
+ class Tagging < ActiveRecord::Base
3
+ self.table_name = "acts_as_taggable_taggings"
4
+
5
+ belongs_to :tag
6
+ belongs_to :taggable, :polymorphic => true
7
+
8
+ validate :taggable_type_matches
9
+
10
+
11
+ private
12
+
13
+ def taggable_type_matches
14
+ errors.add(:taggable_type, "can't be tagged with a tag from another class") if tag.taggable_type != taggable_type
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module ActsAsTaggable
2
+ VERSION = "1.0.0"
3
+ end
@@ -1,23 +1,5 @@
1
1
  class ActsAsTaggableMigrationGenerator < Rails::Generators::Base
2
2
  def create_migration_file
3
- create_file "db/migrations/initializer.rb", <<-EOV
4
- class ActsAsTaggableTable < ActiveRecord::Migration
5
- def change
6
- create_table :tags do |t|
7
- t.string :name
8
- t.string :tag_type
9
- end
10
-
11
- create_table :taggings do |t|
12
- t.belongs_to :tag
13
- t.belongs_to :taggable, :polymorphic => true
14
- end
15
-
16
- add_index :tags, [:name, :tag_type], :unique => true
17
- add_index :taggings, [:taggable_type, :taggable_id]
18
- add_index :taggings, :tag_id
19
- end
20
- end
21
- EOV
3
+ create_file "db/migrations/initializer.rb", File.open('schema.rb').read
22
4
  end
23
- end
5
+ end
@@ -0,0 +1,18 @@
1
+ class ActsAsTaggableTable < ActiveRecord::Migration
2
+ def change
3
+ create_table :acts_as_taggable_tags do |t|
4
+ t.string :name
5
+ t.string :taggable_type
6
+ t.string :tag_type
7
+ end
8
+
9
+ create_table :acts_as_taggable_taggings do |t|
10
+ t.belongs_to :tag
11
+ t.belongs_to :taggable, :polymorphic => true
12
+ end
13
+
14
+ add_index :acts_as_taggable_tags, [:name, :taggable_type, :tag_type], :name => "ensure_uniqueness_of_acts_as_taggable_tags", :unique => true
15
+ add_index :acts_as_taggable_taggings, [:taggable_type, :taggable_id], :name => "index_acts_as_taggable_tagging_associations"
16
+ add_index :acts_as_taggable_taggings, :tag_id
17
+ end
18
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: culturecode-acts_as_taggable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicholas Jakobsen
@@ -9,36 +9,68 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-08-23 00:00:00.000000000 Z
12
+ date: 2020-05-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: rails
15
+ name: activerecord
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
- - - ~>
18
+ - - "~>"
19
19
  - !ruby/object:Gem::Version
20
- version: '4.0'
20
+ version: 4.2.0
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
- - - ~>
25
+ - - "~>"
26
26
  - !ruby/object:Gem::Version
27
- version: '4.0'
28
- description:
27
+ version: 4.2.0
28
+ - !ruby/object:Gem::Dependency
29
+ name: rspec-rails
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: 3.0.0
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: 3.0.0
42
+ - !ruby/object:Gem::Dependency
43
+ name: sqlite3
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ description: Simple record tagging
29
57
  email: contact@culturecode.ca
30
58
  executables: []
31
59
  extensions: []
32
60
  extra_rdoc_files: []
33
61
  files:
62
+ - MIT-LICENSE
63
+ - Rakefile
64
+ - lib/acts_as_taggable.rb
34
65
  - lib/acts_as_taggable/acts_as_taggable.rb
35
66
  - lib/acts_as_taggable/tag.rb
36
67
  - lib/acts_as_taggable/tagging.rb
37
- - lib/acts_as_taggable.rb
68
+ - lib/acts_as_taggable/version.rb
38
69
  - lib/generators/migration_generator.rb
39
- - README.rdoc
70
+ - lib/generators/schema.rb
40
71
  homepage: http://github.com/culturecode/acts_as_taggable
41
- licenses: []
72
+ licenses:
73
+ - MIT
42
74
  metadata: {}
43
75
  post_install_message:
44
76
  rdoc_options: []
@@ -46,17 +78,17 @@ require_paths:
46
78
  - lib
47
79
  required_ruby_version: !ruby/object:Gem::Requirement
48
80
  requirements:
49
- - - '>='
81
+ - - ">="
50
82
  - !ruby/object:Gem::Version
51
83
  version: '0'
52
84
  required_rubygems_version: !ruby/object:Gem::Requirement
53
85
  requirements:
54
- - - '>='
86
+ - - ">="
55
87
  - !ruby/object:Gem::Version
56
88
  version: '0'
57
89
  requirements: []
58
90
  rubyforge_project:
59
- rubygems_version: 2.0.5
91
+ rubygems_version: 2.7.9
60
92
  signing_key:
61
93
  specification_version: 4
62
94
  summary: Simple record tagging
data/README.rdoc DELETED
@@ -1,3 +0,0 @@
1
- == ActsAsTaggable
2
-
3
- Simple tagging.