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.
Files changed (156) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +47 -0
  3. data/.gitignore +13 -0
  4. data/.rspec +3 -0
  5. data/.standard.yml +18 -0
  6. data/.standard_todo.yml +5 -0
  7. data/.travis.yml +36 -0
  8. data/Appraisals +11 -0
  9. data/CHANGELOG.md +0 -0
  10. data/CODE_OF_CONDUCT.md +74 -0
  11. data/CONTRIBUTING.md +57 -0
  12. data/Gemfile +16 -0
  13. data/LICENSE.md +20 -0
  14. data/LICENSE.txt +21 -0
  15. data/README.md +478 -0
  16. data/Rakefile +7 -0
  17. data/bin/console +14 -0
  18. data/bin/setup +8 -0
  19. data/db/migrate/1_create_make_taggable_tags.rb +10 -0
  20. data/db/migrate/2_create_make_taggable_taggings.rb +12 -0
  21. data/db/migrate/3_add_index_to_tags.rb +5 -0
  22. data/db/migrate/4_add_index_to_taggings.rb +12 -0
  23. data/gemfiles/rails_5.gemfile +9 -0
  24. data/gemfiles/rails_6.gemfile +9 -0
  25. data/gemfiles/rails_master.gemfile +9 -0
  26. data/lib/make_taggable.rb +134 -0
  27. data/lib/make_taggable/default_parser.rb +75 -0
  28. data/lib/make_taggable/engine.rb +4 -0
  29. data/lib/make_taggable/generic_parser.rb +19 -0
  30. data/lib/make_taggable/tag.rb +131 -0
  31. data/lib/make_taggable/tag_list.rb +102 -0
  32. data/lib/make_taggable/taggable.rb +100 -0
  33. data/lib/make_taggable/taggable/cache.rb +90 -0
  34. data/lib/make_taggable/taggable/collection.rb +183 -0
  35. data/lib/make_taggable/taggable/core.rb +323 -0
  36. data/lib/make_taggable/taggable/ownership.rb +137 -0
  37. data/lib/make_taggable/taggable/related.rb +71 -0
  38. data/lib/make_taggable/taggable/tag_list_type.rb +4 -0
  39. data/lib/make_taggable/taggable/tagged_with_query.rb +16 -0
  40. data/lib/make_taggable/taggable/tagged_with_query/all_tags_query.rb +111 -0
  41. data/lib/make_taggable/taggable/tagged_with_query/any_tags_query.rb +68 -0
  42. data/lib/make_taggable/taggable/tagged_with_query/exclude_tags_query.rb +81 -0
  43. data/lib/make_taggable/taggable/tagged_with_query/query_base.rb +61 -0
  44. data/lib/make_taggable/tagger.rb +89 -0
  45. data/lib/make_taggable/tagging.rb +32 -0
  46. data/lib/make_taggable/tags_helper.rb +15 -0
  47. data/lib/make_taggable/utils.rb +34 -0
  48. data/lib/make_taggable/version.rb +4 -0
  49. data/lib/tasks/tags_collate_utf8.rake +17 -0
  50. data/make_taggable.gemspec +26 -0
  51. data/spec/dummy/README.md +24 -0
  52. data/spec/dummy/Rakefile +6 -0
  53. data/spec/dummy/app/assets/config/manifest.js +2 -0
  54. data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
  55. data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
  56. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  57. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  58. data/spec/dummy/app/jobs/application_job.rb +7 -0
  59. data/spec/dummy/app/mailers/application_mailer.rb +4 -0
  60. data/spec/dummy/app/models/altered_inheriting_taggable_model.rb +5 -0
  61. data/spec/dummy/app/models/application_record.rb +3 -0
  62. data/spec/dummy/app/models/cached_model.rb +3 -0
  63. data/spec/dummy/app/models/cached_model_with_array.rb +11 -0
  64. data/spec/dummy/app/models/columns_override_model.rb +5 -0
  65. data/spec/dummy/app/models/company.rb +15 -0
  66. data/spec/dummy/app/models/concerns/.keep +0 -0
  67. data/spec/dummy/app/models/inheriting_taggable_model.rb +4 -0
  68. data/spec/dummy/app/models/market.rb +2 -0
  69. data/spec/dummy/app/models/non_standard_id_taggable_model.rb +8 -0
  70. data/spec/dummy/app/models/ordered_taggable_model.rb +4 -0
  71. data/spec/dummy/app/models/other_cached_model.rb +3 -0
  72. data/spec/dummy/app/models/other_taggable_model.rb +4 -0
  73. data/spec/dummy/app/models/student.rb +4 -0
  74. data/spec/dummy/app/models/taggable_model.rb +14 -0
  75. data/spec/dummy/app/models/untaggable_model.rb +3 -0
  76. data/spec/dummy/app/models/user.rb +3 -0
  77. data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
  78. data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
  79. data/spec/dummy/bin/rails +4 -0
  80. data/spec/dummy/bin/rake +4 -0
  81. data/spec/dummy/bin/setup +33 -0
  82. data/spec/dummy/config.ru +5 -0
  83. data/spec/dummy/config/application.rb +19 -0
  84. data/spec/dummy/config/boot.rb +5 -0
  85. data/spec/dummy/config/cable.yml +10 -0
  86. data/spec/dummy/config/credentials.yml.enc +1 -0
  87. data/spec/dummy/config/database.yml +25 -0
  88. data/spec/dummy/config/environment.rb +5 -0
  89. data/spec/dummy/config/environments/development.rb +52 -0
  90. data/spec/dummy/config/environments/production.rb +105 -0
  91. data/spec/dummy/config/environments/test.rb +49 -0
  92. data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
  93. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  94. data/spec/dummy/config/initializers/cors.rb +16 -0
  95. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  96. data/spec/dummy/config/initializers/inflections.rb +16 -0
  97. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  98. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  99. data/spec/dummy/config/locales/en.yml +33 -0
  100. data/spec/dummy/config/master.key +1 -0
  101. data/spec/dummy/config/puma.rb +38 -0
  102. data/spec/dummy/config/routes.rb +3 -0
  103. data/spec/dummy/config/spring.rb +6 -0
  104. data/spec/dummy/config/storage.yml +34 -0
  105. data/spec/dummy/db/migrate/20201119220853_create_taggable_models.rb +8 -0
  106. data/spec/dummy/db/migrate/20201119221037_create_columns_override_models.rb +9 -0
  107. data/spec/dummy/db/migrate/20201119221121_create_non_standard_id_taggable_models.rb +8 -0
  108. data/spec/dummy/db/migrate/20201119221228_create_untaggable_models.rb +8 -0
  109. data/spec/dummy/db/migrate/20201119221247_create_cached_models.rb +9 -0
  110. data/spec/dummy/db/migrate/20201119221314_create_other_cached_models.rb +11 -0
  111. data/spec/dummy/db/migrate/20201119221343_create_companies.rb +7 -0
  112. data/spec/dummy/db/migrate/20201119221416_create_users.rb +7 -0
  113. data/spec/dummy/db/migrate/20201119221434_create_other_taggable_models.rb +8 -0
  114. data/spec/dummy/db/migrate/20201119221507_create_ordered_taggable_models.rb +8 -0
  115. data/spec/dummy/db/migrate/20201119221530_create_cache_methods_injected_models.rb +7 -0
  116. data/spec/dummy/db/migrate/20201119221629_create_other_cached_with_array_models.rb +11 -0
  117. data/spec/dummy/db/migrate/20201119221746_create_taggable_model_with_jsons.rb +9 -0
  118. data/spec/dummy/db/migrate/20201119222429_create_make_taggable_tags.make_taggable_engine.rb +11 -0
  119. data/spec/dummy/db/migrate/20201119222430_create_make_taggable_taggings.make_taggable_engine.rb +13 -0
  120. data/spec/dummy/db/migrate/20201119222431_add_index_to_tags.make_taggable_engine.rb +6 -0
  121. data/spec/dummy/db/migrate/20201119222432_add_index_to_taggings.make_taggable_engine.rb +13 -0
  122. data/spec/dummy/db/schema.rb +117 -0
  123. data/spec/dummy/db/seeds.rb +7 -0
  124. data/spec/dummy/lib/tasks/.keep +0 -0
  125. data/spec/dummy/log/.keep +0 -0
  126. data/spec/dummy/public/robots.txt +1 -0
  127. data/spec/dummy/storage/.keep +0 -0
  128. data/spec/dummy/test/channels/application_cable/connection_test.rb +11 -0
  129. data/spec/dummy/test/controllers/.keep +0 -0
  130. data/spec/dummy/test/fixtures/.keep +0 -0
  131. data/spec/dummy/test/fixtures/files/.keep +0 -0
  132. data/spec/dummy/test/integration/.keep +0 -0
  133. data/spec/dummy/test/mailers/.keep +0 -0
  134. data/spec/dummy/test/models/.keep +0 -0
  135. data/spec/dummy/test/test_helper.rb +13 -0
  136. data/spec/dummy/vendor/.keep +0 -0
  137. data/spec/make_taggable/acts_as_tagger_spec.rb +112 -0
  138. data/spec/make_taggable/caching_spec.rb +123 -0
  139. data/spec/make_taggable/default_parser_spec.rb +45 -0
  140. data/spec/make_taggable/dirty_spec.rb +140 -0
  141. data/spec/make_taggable/generic_parser_spec.rb +13 -0
  142. data/spec/make_taggable/make_taggable_spec.rb +260 -0
  143. data/spec/make_taggable/related_spec.rb +93 -0
  144. data/spec/make_taggable/single_table_inheritance_spec.rb +220 -0
  145. data/spec/make_taggable/tag_list_spec.rb +169 -0
  146. data/spec/make_taggable/tag_spec.rb +297 -0
  147. data/spec/make_taggable/taggable_spec.rb +804 -0
  148. data/spec/make_taggable/tagger_spec.rb +149 -0
  149. data/spec/make_taggable/tagging_spec.rb +115 -0
  150. data/spec/make_taggable/tags_helper_spec.rb +43 -0
  151. data/spec/make_taggable/utils_spec.rb +22 -0
  152. data/spec/make_taggable_spec.rb +5 -0
  153. data/spec/spec_helper.rb +18 -0
  154. data/spec/support/array.rb +9 -0
  155. data/spec/support/helpers.rb +31 -0
  156. 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