culturecode-acts_as_taggable 0.2.3 → 1.0.0

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