make_taggable 0.6.3

Sign up to get free protection for your applications and to get access to all the features.
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