make_taggable 0.6.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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +47 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.standard.yml +18 -0
- data/.standard_todo.yml +5 -0
- data/.travis.yml +36 -0
- data/Appraisals +11 -0
- data/CHANGELOG.md +0 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +57 -0
- data/Gemfile +16 -0
- data/LICENSE.md +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +478 -0
- data/Rakefile +7 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/db/migrate/1_create_make_taggable_tags.rb +10 -0
- data/db/migrate/2_create_make_taggable_taggings.rb +12 -0
- data/db/migrate/3_add_index_to_tags.rb +5 -0
- data/db/migrate/4_add_index_to_taggings.rb +12 -0
- data/gemfiles/rails_5.gemfile +9 -0
- data/gemfiles/rails_6.gemfile +9 -0
- data/gemfiles/rails_master.gemfile +9 -0
- data/lib/make_taggable.rb +134 -0
- data/lib/make_taggable/default_parser.rb +75 -0
- data/lib/make_taggable/engine.rb +4 -0
- data/lib/make_taggable/generic_parser.rb +19 -0
- data/lib/make_taggable/tag.rb +131 -0
- data/lib/make_taggable/tag_list.rb +102 -0
- data/lib/make_taggable/taggable.rb +100 -0
- data/lib/make_taggable/taggable/cache.rb +90 -0
- data/lib/make_taggable/taggable/collection.rb +183 -0
- data/lib/make_taggable/taggable/core.rb +323 -0
- data/lib/make_taggable/taggable/ownership.rb +137 -0
- data/lib/make_taggable/taggable/related.rb +71 -0
- data/lib/make_taggable/taggable/tag_list_type.rb +4 -0
- data/lib/make_taggable/taggable/tagged_with_query.rb +16 -0
- data/lib/make_taggable/taggable/tagged_with_query/all_tags_query.rb +111 -0
- data/lib/make_taggable/taggable/tagged_with_query/any_tags_query.rb +68 -0
- data/lib/make_taggable/taggable/tagged_with_query/exclude_tags_query.rb +81 -0
- data/lib/make_taggable/taggable/tagged_with_query/query_base.rb +61 -0
- data/lib/make_taggable/tagger.rb +89 -0
- data/lib/make_taggable/tagging.rb +32 -0
- data/lib/make_taggable/tags_helper.rb +15 -0
- data/lib/make_taggable/utils.rb +34 -0
- data/lib/make_taggable/version.rb +4 -0
- data/lib/tasks/tags_collate_utf8.rake +17 -0
- data/make_taggable.gemspec +26 -0
- data/spec/dummy/README.md +24 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +2 -0
- data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
- data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
- data/spec/dummy/app/controllers/application_controller.rb +2 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/jobs/application_job.rb +7 -0
- data/spec/dummy/app/mailers/application_mailer.rb +4 -0
- data/spec/dummy/app/models/altered_inheriting_taggable_model.rb +5 -0
- data/spec/dummy/app/models/application_record.rb +3 -0
- data/spec/dummy/app/models/cached_model.rb +3 -0
- data/spec/dummy/app/models/cached_model_with_array.rb +11 -0
- data/spec/dummy/app/models/columns_override_model.rb +5 -0
- data/spec/dummy/app/models/company.rb +15 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/models/inheriting_taggable_model.rb +4 -0
- data/spec/dummy/app/models/market.rb +2 -0
- data/spec/dummy/app/models/non_standard_id_taggable_model.rb +8 -0
- data/spec/dummy/app/models/ordered_taggable_model.rb +4 -0
- data/spec/dummy/app/models/other_cached_model.rb +3 -0
- data/spec/dummy/app/models/other_taggable_model.rb +4 -0
- data/spec/dummy/app/models/student.rb +4 -0
- data/spec/dummy/app/models/taggable_model.rb +14 -0
- data/spec/dummy/app/models/untaggable_model.rb +3 -0
- data/spec/dummy/app/models/user.rb +3 -0
- data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +33 -0
- data/spec/dummy/config.ru +5 -0
- data/spec/dummy/config/application.rb +19 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/cable.yml +10 -0
- data/spec/dummy/config/credentials.yml.enc +1 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +52 -0
- data/spec/dummy/config/environments/production.rb +105 -0
- data/spec/dummy/config/environments/test.rb +49 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cors.rb +16 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +33 -0
- data/spec/dummy/config/master.key +1 -0
- data/spec/dummy/config/puma.rb +38 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config/spring.rb +6 -0
- data/spec/dummy/config/storage.yml +34 -0
- data/spec/dummy/db/migrate/20201119220853_create_taggable_models.rb +8 -0
- data/spec/dummy/db/migrate/20201119221037_create_columns_override_models.rb +9 -0
- data/spec/dummy/db/migrate/20201119221121_create_non_standard_id_taggable_models.rb +8 -0
- data/spec/dummy/db/migrate/20201119221228_create_untaggable_models.rb +8 -0
- data/spec/dummy/db/migrate/20201119221247_create_cached_models.rb +9 -0
- data/spec/dummy/db/migrate/20201119221314_create_other_cached_models.rb +11 -0
- data/spec/dummy/db/migrate/20201119221343_create_companies.rb +7 -0
- data/spec/dummy/db/migrate/20201119221416_create_users.rb +7 -0
- data/spec/dummy/db/migrate/20201119221434_create_other_taggable_models.rb +8 -0
- data/spec/dummy/db/migrate/20201119221507_create_ordered_taggable_models.rb +8 -0
- data/spec/dummy/db/migrate/20201119221530_create_cache_methods_injected_models.rb +7 -0
- data/spec/dummy/db/migrate/20201119221629_create_other_cached_with_array_models.rb +11 -0
- data/spec/dummy/db/migrate/20201119221746_create_taggable_model_with_jsons.rb +9 -0
- data/spec/dummy/db/migrate/20201119222429_create_make_taggable_tags.make_taggable_engine.rb +11 -0
- data/spec/dummy/db/migrate/20201119222430_create_make_taggable_taggings.make_taggable_engine.rb +13 -0
- data/spec/dummy/db/migrate/20201119222431_add_index_to_tags.make_taggable_engine.rb +6 -0
- data/spec/dummy/db/migrate/20201119222432_add_index_to_taggings.make_taggable_engine.rb +13 -0
- data/spec/dummy/db/schema.rb +117 -0
- data/spec/dummy/db/seeds.rb +7 -0
- data/spec/dummy/lib/tasks/.keep +0 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/dummy/public/robots.txt +1 -0
- data/spec/dummy/storage/.keep +0 -0
- data/spec/dummy/test/channels/application_cable/connection_test.rb +11 -0
- data/spec/dummy/test/controllers/.keep +0 -0
- data/spec/dummy/test/fixtures/.keep +0 -0
- data/spec/dummy/test/fixtures/files/.keep +0 -0
- data/spec/dummy/test/integration/.keep +0 -0
- data/spec/dummy/test/mailers/.keep +0 -0
- data/spec/dummy/test/models/.keep +0 -0
- data/spec/dummy/test/test_helper.rb +13 -0
- data/spec/dummy/vendor/.keep +0 -0
- data/spec/make_taggable/acts_as_tagger_spec.rb +112 -0
- data/spec/make_taggable/caching_spec.rb +123 -0
- data/spec/make_taggable/default_parser_spec.rb +45 -0
- data/spec/make_taggable/dirty_spec.rb +140 -0
- data/spec/make_taggable/generic_parser_spec.rb +13 -0
- data/spec/make_taggable/make_taggable_spec.rb +260 -0
- data/spec/make_taggable/related_spec.rb +93 -0
- data/spec/make_taggable/single_table_inheritance_spec.rb +220 -0
- data/spec/make_taggable/tag_list_spec.rb +169 -0
- data/spec/make_taggable/tag_spec.rb +297 -0
- data/spec/make_taggable/taggable_spec.rb +804 -0
- data/spec/make_taggable/tagger_spec.rb +149 -0
- data/spec/make_taggable/tagging_spec.rb +115 -0
- data/spec/make_taggable/tags_helper_spec.rb +43 -0
- data/spec/make_taggable/utils_spec.rb +22 -0
- data/spec/make_taggable_spec.rb +5 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/support/array.rb +9 -0
- data/spec/support/helpers.rb +31 -0
- metadata +391 -0
@@ -0,0 +1,102 @@
|
|
1
|
+
require "active_support/core_ext/module/delegation"
|
2
|
+
|
3
|
+
module MakeTaggable
|
4
|
+
class TagList < Array
|
5
|
+
attr_accessor :owner
|
6
|
+
attr_accessor :parser
|
7
|
+
|
8
|
+
def initialize(*args)
|
9
|
+
@parser = MakeTaggable.default_parser
|
10
|
+
add(*args)
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# Add tags to the tag_list. Duplicate or blank tags will be ignored.
|
15
|
+
# Use the <tt>:parse</tt> option to add an unparsed tag string.
|
16
|
+
#
|
17
|
+
# Example:
|
18
|
+
# tag_list.add("Fun", "Happy")
|
19
|
+
# tag_list.add("Fun, Happy", :parse => true)
|
20
|
+
def add(*names)
|
21
|
+
extract_and_apply_options!(names)
|
22
|
+
concat(names)
|
23
|
+
clean!
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
# Append---Add the tag to the tag_list. This
|
28
|
+
# expression returns the tag_list itself, so several appends
|
29
|
+
# may be chained together.
|
30
|
+
def <<(obj)
|
31
|
+
add(obj)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Concatenation --- Returns a new tag list built by concatenating the
|
35
|
+
# two tag lists together to produce a third tag list.
|
36
|
+
def +(other)
|
37
|
+
TagList.new.add(self).add(other)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Appends the elements of +other_tag_list+ to +self+.
|
41
|
+
def concat(other_tag_list)
|
42
|
+
super(other_tag_list).send(:clean!)
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Remove specific tags from the tag_list.
|
48
|
+
# Use the <tt>:parse</tt> option to add an unparsed tag string.
|
49
|
+
#
|
50
|
+
# Example:
|
51
|
+
# tag_list.remove("Sad", "Lonely")
|
52
|
+
# tag_list.remove("Sad, Lonely", :parse => true)
|
53
|
+
def remove(*names)
|
54
|
+
extract_and_apply_options!(names)
|
55
|
+
delete_if { |name| names.include?(name) }
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Transform the tag_list into a tag string suitable for editing in a form.
|
61
|
+
# The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
|
62
|
+
#
|
63
|
+
# Example:
|
64
|
+
# tag_list = TagList.new("Round", "Square,Cube")
|
65
|
+
# tag_list.to_s # 'Round, "Square,Cube"'
|
66
|
+
def to_s
|
67
|
+
tags = frozen? ? dup : self
|
68
|
+
tags.send(:clean!)
|
69
|
+
|
70
|
+
tags.map { |name|
|
71
|
+
d = MakeTaggable.delimiter
|
72
|
+
d = Regexp.new d.join("|") if d.is_a? Array
|
73
|
+
name.index(d) ? "\"#{name}\"" : name
|
74
|
+
}.join(MakeTaggable.glue)
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Convert everything to string, remove whitespace, duplicates, and blanks.
|
80
|
+
def clean!
|
81
|
+
reject!(&:blank?)
|
82
|
+
map!(&:to_s)
|
83
|
+
map!(&:strip)
|
84
|
+
map! { |tag| tag.mb_chars.downcase.to_s } if MakeTaggable.force_lowercase
|
85
|
+
map!(&:parameterize) if MakeTaggable.force_parameterize
|
86
|
+
|
87
|
+
MakeTaggable.strict_case_match ? uniq! : uniq! { |tag| tag.downcase }
|
88
|
+
self
|
89
|
+
end
|
90
|
+
|
91
|
+
def extract_and_apply_options!(args)
|
92
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
93
|
+
options.assert_valid_keys :parse, :parser
|
94
|
+
|
95
|
+
parser = options[:parser] || @parser
|
96
|
+
|
97
|
+
args.map! { |a| parser.new(a).parse } if options[:parse] || options[:parser]
|
98
|
+
|
99
|
+
args.flatten!
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module MakeTaggable
|
2
|
+
module Taggable
|
3
|
+
def taggable?
|
4
|
+
false
|
5
|
+
end
|
6
|
+
|
7
|
+
##
|
8
|
+
# This is an alias for calling <tt>make_taggable :tags</tt>.
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
# class Book < ActiveRecord::Base
|
12
|
+
# acts_as_taggable
|
13
|
+
# end
|
14
|
+
def acts_as_taggable
|
15
|
+
make_taggable :tags
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# This is an alias for calling <tt>acts_as_ordered_taggable_on :tags</tt>.
|
20
|
+
#
|
21
|
+
# Example:
|
22
|
+
# class Book < ActiveRecord::Base
|
23
|
+
# acts_as_ordered_taggable
|
24
|
+
# end
|
25
|
+
def acts_as_ordered_taggable
|
26
|
+
acts_as_ordered_taggable_on :tags
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Make a model taggable on specified contexts.
|
31
|
+
#
|
32
|
+
# @param [Array] tag_types An array of taggable contexts
|
33
|
+
#
|
34
|
+
# Example:
|
35
|
+
# class User < ActiveRecord::Base
|
36
|
+
# make_taggable :languages, :skills
|
37
|
+
# end
|
38
|
+
def make_taggable(*tag_types)
|
39
|
+
taggable_on(false, tag_types)
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Make a model taggable on specified contexts
|
44
|
+
# and preserves the order in which tags are created
|
45
|
+
#
|
46
|
+
# @param [Array] tag_types An array of taggable contexts
|
47
|
+
#
|
48
|
+
# Example:
|
49
|
+
# class User < ActiveRecord::Base
|
50
|
+
# acts_as_ordered_taggable_on :languages, :skills
|
51
|
+
# end
|
52
|
+
def acts_as_ordered_taggable_on(*tag_types)
|
53
|
+
taggable_on(true, tag_types)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Make a model taggable on specified contexts
|
59
|
+
# and optionally preserves the order in which tags are created
|
60
|
+
#
|
61
|
+
# Separate methods used above for backwards compatibility
|
62
|
+
# so that the original make_taggable method is unaffected
|
63
|
+
# as it's not possible to add another argument to the method
|
64
|
+
# without the tag_types being enclosed in square brackets
|
65
|
+
#
|
66
|
+
# NB: method overridden in core module in order to create tag type
|
67
|
+
# associations and methods after this logic has executed
|
68
|
+
#
|
69
|
+
def taggable_on(preserve_tag_order, *tag_types)
|
70
|
+
tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
|
71
|
+
|
72
|
+
if taggable?
|
73
|
+
self.tag_types = (self.tag_types + tag_types).uniq
|
74
|
+
self.preserve_tag_order = preserve_tag_order
|
75
|
+
else
|
76
|
+
class_attribute :tag_types
|
77
|
+
self.tag_types = tag_types
|
78
|
+
class_attribute :preserve_tag_order
|
79
|
+
self.preserve_tag_order = preserve_tag_order
|
80
|
+
|
81
|
+
class_eval do
|
82
|
+
has_many :taggings, as: :taggable, dependent: :destroy, class_name: "::MakeTaggable::Tagging"
|
83
|
+
has_many :base_tags, through: :taggings, source: :tag, class_name: "::MakeTaggable::Tag"
|
84
|
+
|
85
|
+
def self.taggable?
|
86
|
+
true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# each of these add context-specific methods and must be
|
92
|
+
# called on each call of taggable_on
|
93
|
+
include Core
|
94
|
+
include Collection
|
95
|
+
include Cache
|
96
|
+
include Ownership
|
97
|
+
include Related
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module MakeTaggable::Taggable
|
2
|
+
module Cache
|
3
|
+
def self.included(base)
|
4
|
+
# When included, conditionally adds tag caching methods when the model
|
5
|
+
# has any "cached_#{tag_type}_list" column
|
6
|
+
base.extend Columns
|
7
|
+
end
|
8
|
+
|
9
|
+
module Columns
|
10
|
+
# ActiveRecord::Base.columns makes a database connection and caches the
|
11
|
+
# calculated columns hash for the record as @columns. Since we don't
|
12
|
+
# want to add caching methods until we confirm the presence of a
|
13
|
+
# caching column, and we don't want to force opening a database
|
14
|
+
# connection when the class is loaded, here we intercept and cache
|
15
|
+
# the call to :columns as @make_taggable_cache_columns
|
16
|
+
# to mimic the underlying behavior. While processing this first
|
17
|
+
# call to columns, we do the caching column check and dynamically add
|
18
|
+
# the class and instance methods
|
19
|
+
# FIXME: this method cannot compile in rubinius
|
20
|
+
def columns
|
21
|
+
@make_taggable_cache_columns ||= begin
|
22
|
+
db_columns = super
|
23
|
+
_add_tags_caching_methods if _has_tags_cache_columns?(db_columns)
|
24
|
+
db_columns
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def reset_column_information
|
29
|
+
super
|
30
|
+
@make_taggable_cache_columns = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# @private
|
36
|
+
def _has_tags_cache_columns?(db_columns)
|
37
|
+
db_column_names = db_columns.map(&:name)
|
38
|
+
tag_types.any? do |context|
|
39
|
+
db_column_names.include?("cached_#{context.to_s.singularize}_list")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @private
|
44
|
+
def _add_tags_caching_methods
|
45
|
+
send :include, MakeTaggable::Taggable::Cache::InstanceMethods
|
46
|
+
extend MakeTaggable::Taggable::Cache::ClassMethods
|
47
|
+
|
48
|
+
before_save :save_cached_tag_list
|
49
|
+
|
50
|
+
initialize_tags_cache
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
module ClassMethods
|
55
|
+
def initialize_tags_cache
|
56
|
+
tag_types.map(&:to_s).each do |tag_type|
|
57
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
58
|
+
def self.caching_#{tag_type.singularize}_list?
|
59
|
+
caching_tag_list_on?("#{tag_type}")
|
60
|
+
end
|
61
|
+
RUBY
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def make_taggable(*args)
|
66
|
+
super(*args)
|
67
|
+
initialize_tags_cache
|
68
|
+
end
|
69
|
+
|
70
|
+
def caching_tag_list_on?(context)
|
71
|
+
column_names.include?("cached_#{context.to_s.singularize}_list")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
module InstanceMethods
|
76
|
+
def save_cached_tag_list
|
77
|
+
tag_types.map(&:to_s).each do |tag_type|
|
78
|
+
if self.class.send("caching_#{tag_type.singularize}_list?")
|
79
|
+
if tag_list_cache_set_on(tag_type)
|
80
|
+
list = tag_list_cache_on(tag_type).to_a.flatten.compact.join("#{MakeTaggable.delimiter} ")
|
81
|
+
self["cached_#{tag_type.singularize}_list"] = list
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
module MakeTaggable::Taggable
|
2
|
+
module Collection
|
3
|
+
def self.included(base)
|
4
|
+
base.extend MakeTaggable::Taggable::Collection::ClassMethods
|
5
|
+
base.initialize_make_taggable_collection
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def initialize_make_taggable_collection
|
10
|
+
tag_types.map(&:to_s).each do |tag_type|
|
11
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
12
|
+
def self.#{tag_type.singularize}_counts(options={})
|
13
|
+
tag_counts_on('#{tag_type}', options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def #{tag_type.singularize}_counts(options = {})
|
17
|
+
tag_counts_on('#{tag_type}', options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def top_#{tag_type}(limit = 10)
|
21
|
+
tag_counts_on('#{tag_type}', order: 'count desc', limit: limit.to_i)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.top_#{tag_type}(limit = 10)
|
25
|
+
tag_counts_on('#{tag_type}', order: 'count desc', limit: limit.to_i)
|
26
|
+
end
|
27
|
+
RUBY
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def make_taggable(*args)
|
32
|
+
super(*args)
|
33
|
+
initialize_make_taggable_collection
|
34
|
+
end
|
35
|
+
|
36
|
+
def tag_counts_on(context, options = {})
|
37
|
+
all_tag_counts(options.merge({on: context.to_s}))
|
38
|
+
end
|
39
|
+
|
40
|
+
def tags_on(context, options = {})
|
41
|
+
all_tags(options.merge({on: context.to_s}))
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Calculate the tag names.
|
46
|
+
# To be used when you don't need tag counts and want to avoid the taggable joins.
|
47
|
+
#
|
48
|
+
# @param [Hash] options Options:
|
49
|
+
# * :start_at - Restrict the tags to those created after a certain time
|
50
|
+
# * :end_at - Restrict the tags to those created before a certain time
|
51
|
+
# * :conditions - A piece of SQL conditions to add to the query. Note we don't join the taggable objects for performance reasons.
|
52
|
+
# * :limit - The maximum number of tags to return
|
53
|
+
# * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
|
54
|
+
# * :on - Scope the find to only include a certain context
|
55
|
+
def all_tags(options = {})
|
56
|
+
options = options.dup
|
57
|
+
options.assert_valid_keys :start_at, :end_at, :conditions, :order, :limit, :on
|
58
|
+
|
59
|
+
## Generate conditions:
|
60
|
+
options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
|
61
|
+
|
62
|
+
## Generate scope:
|
63
|
+
tagging_scope = MakeTaggable::Tagging.select("#{MakeTaggable::Tagging.table_name}.tag_id")
|
64
|
+
tag_scope = MakeTaggable::Tag.select("#{MakeTaggable::Tag.table_name}.*").order(options[:order]).limit(options[:limit])
|
65
|
+
|
66
|
+
# Joins and conditions
|
67
|
+
tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) }
|
68
|
+
tag_scope = tag_scope.where(options[:conditions])
|
69
|
+
|
70
|
+
group_columns = "#{MakeTaggable::Tagging.table_name}.tag_id"
|
71
|
+
|
72
|
+
# Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
|
73
|
+
tagging_scope = generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key).group(group_columns)
|
74
|
+
|
75
|
+
tag_scope_joins(tag_scope, tagging_scope)
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Calculate the tag counts for all tags.
|
80
|
+
#
|
81
|
+
# @param [Hash] options Options:
|
82
|
+
# * :start_at - Restrict the tags to those created after a certain time
|
83
|
+
# * :end_at - Restrict the tags to those created before a certain time
|
84
|
+
# * :conditions - A piece of SQL conditions to add to the query
|
85
|
+
# * :limit - The maximum number of tags to return
|
86
|
+
# * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
|
87
|
+
# * :at_least - Exclude tags with a frequency less than the given value
|
88
|
+
# * :at_most - Exclude tags with a frequency greater than the given value
|
89
|
+
# * :on - Scope the find to only include a certain context
|
90
|
+
def all_tag_counts(options = {})
|
91
|
+
options = options.dup
|
92
|
+
options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
|
93
|
+
|
94
|
+
## Generate conditions:
|
95
|
+
options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
|
96
|
+
|
97
|
+
## Generate scope:
|
98
|
+
tagging_scope = MakeTaggable::Tagging.select("#{MakeTaggable::Tagging.table_name}.tag_id, COUNT(#{MakeTaggable::Tagging.table_name}.tag_id) AS tags_count")
|
99
|
+
tag_scope = MakeTaggable::Tag.select("#{MakeTaggable::Tag.table_name}.*, #{MakeTaggable::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
|
100
|
+
|
101
|
+
# Current model is STI descendant, so add type checking to the join condition
|
102
|
+
unless descends_from_active_record?
|
103
|
+
taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{MakeTaggable::Tagging.table_name}.taggable_id"
|
104
|
+
taggable_join << " AND #{table_name}.#{inheritance_column} = '#{name}'"
|
105
|
+
tagging_scope = tagging_scope.joins(taggable_join)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Conditions
|
109
|
+
tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) }
|
110
|
+
tag_scope = tag_scope.where(options[:conditions])
|
111
|
+
|
112
|
+
# GROUP BY and HAVING clauses:
|
113
|
+
having = ["COUNT(#{MakeTaggable::Tagging.table_name}.tag_id) > 0"]
|
114
|
+
having.push sanitize_sql(["COUNT(#{MakeTaggable::Tagging.table_name}.tag_id) >= ?", options.delete(:at_least)]) if options[:at_least]
|
115
|
+
having.push sanitize_sql(["COUNT(#{MakeTaggable::Tagging.table_name}.tag_id) <= ?", options.delete(:at_most)]) if options[:at_most]
|
116
|
+
having = having.compact.join(" AND ")
|
117
|
+
|
118
|
+
group_columns = "#{MakeTaggable::Tagging.table_name}.tag_id"
|
119
|
+
|
120
|
+
unless options[:id]
|
121
|
+
# Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
|
122
|
+
tagging_scope = generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key)
|
123
|
+
end
|
124
|
+
|
125
|
+
tagging_scope = tagging_scope.group(group_columns).having(having)
|
126
|
+
|
127
|
+
tag_scope_joins(tag_scope, tagging_scope)
|
128
|
+
end
|
129
|
+
|
130
|
+
def safe_to_sql(relation)
|
131
|
+
connection.respond_to?(:unprepared_statement) ? connection.unprepared_statement { relation.to_sql } : relation.to_sql
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key)
|
137
|
+
table_name_pkey = "#{table_name}.#{primary_key}"
|
138
|
+
if MakeTaggable::Utils.using_mysql?
|
139
|
+
# See https://github.com/mbleigh/acts-as-taggable-on/pull/457 for details
|
140
|
+
scoped_ids = pluck(table_name_pkey)
|
141
|
+
tagging_scope = tagging_scope.where("#{MakeTaggable::Tagging.table_name}.taggable_id IN (?)", scoped_ids)
|
142
|
+
else
|
143
|
+
tagging_scope = tagging_scope.where("#{MakeTaggable::Tagging.table_name}.taggable_id IN(#{safe_to_sql(except(:select).select(table_name_pkey))})")
|
144
|
+
end
|
145
|
+
|
146
|
+
tagging_scope
|
147
|
+
end
|
148
|
+
|
149
|
+
def tagging_conditions(options)
|
150
|
+
tagging_conditions = []
|
151
|
+
tagging_conditions.push sanitize_sql(["#{MakeTaggable::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
152
|
+
tagging_conditions.push sanitize_sql(["#{MakeTaggable::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
153
|
+
|
154
|
+
taggable_conditions = sanitize_sql(["#{MakeTaggable::Tagging.table_name}.taggable_type = ?", base_class.name])
|
155
|
+
taggable_conditions << sanitize_sql([" AND #{MakeTaggable::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
|
156
|
+
taggable_conditions << sanitize_sql([" AND #{MakeTaggable::Tagging.table_name}.taggable_id = ?", options[:id]]) if options[:id]
|
157
|
+
|
158
|
+
tagging_conditions.push taggable_conditions
|
159
|
+
|
160
|
+
tagging_conditions
|
161
|
+
end
|
162
|
+
|
163
|
+
def tag_scope_joins(tag_scope, tagging_scope)
|
164
|
+
tag_scope = tag_scope.joins("JOIN (#{safe_to_sql(tagging_scope)}) AS #{MakeTaggable::Tagging.table_name} ON #{MakeTaggable::Tagging.table_name}.tag_id = #{MakeTaggable::Tag.table_name}.id")
|
165
|
+
tag_scope.extending(CalculationMethods)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def tag_counts_on(context, options = {})
|
170
|
+
self.class.tag_counts_on(context, options.merge(id: id))
|
171
|
+
end
|
172
|
+
|
173
|
+
module CalculationMethods
|
174
|
+
# Rails 5 TODO: Remove options argument as soon we remove support to
|
175
|
+
# activerecord-deprecated_finders.
|
176
|
+
# See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/calculations.rb#L38
|
177
|
+
def count(column_name = :all, options = {})
|
178
|
+
# https://github.com/rails/rails/commit/da9b5d4a8435b744fcf278fffd6d7f1e36d4a4f2
|
179
|
+
super(column_name)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|