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,71 @@
1
+ module MakeTaggable::Taggable
2
+ module Related
3
+ def self.included(base)
4
+ base.extend MakeTaggable::Taggable::Related::ClassMethods
5
+ base.initialize_make_taggable_related
6
+ end
7
+
8
+ module ClassMethods
9
+ def initialize_make_taggable_related
10
+ tag_types.map(&:to_s).each do |tag_type|
11
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
12
+ def find_related_#{tag_type}(options = {})
13
+ related_tags_for('#{tag_type}', self.class, options)
14
+ end
15
+ alias_method :find_related_on_#{tag_type}, :find_related_#{tag_type}
16
+
17
+ def find_related_#{tag_type}_for(klass, options = {})
18
+ related_tags_for('#{tag_type}', klass, options)
19
+ end
20
+ RUBY
21
+ end
22
+ end
23
+
24
+ def make_taggable(*args)
25
+ super(*args)
26
+ initialize_make_taggable_related
27
+ end
28
+ end
29
+
30
+ def find_matching_contexts(search_context, result_context, options = {})
31
+ matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
32
+ end
33
+
34
+ def find_matching_contexts_for(klass, search_context, result_context, options = {})
35
+ matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
36
+ end
37
+
38
+ def matching_contexts_for(search_context, result_context, klass, options = {})
39
+ tags_to_find = tags_on(search_context).map { |t| t.name }
40
+ related_where(klass, ["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{MakeTaggable::Tagging.table_name}.taggable_id AND #{MakeTaggable::Tagging.table_name}.taggable_type = '#{klass.base_class}' AND #{MakeTaggable::Tagging.table_name}.tag_id = #{MakeTaggable::Tag.table_name}.#{MakeTaggable::Tag.primary_key} AND #{MakeTaggable::Tag.table_name}.name IN (?) AND #{MakeTaggable::Tagging.table_name}.context = ?", tags_to_find, result_context])
41
+ end
42
+
43
+ def related_tags_for(context, klass, options = {})
44
+ tags_to_ignore = Array.wrap(options[:ignore]).map(&:to_s) || []
45
+ tags_to_find = tags_on(context).map { |t| t.name }.reject { |t| tags_to_ignore.include? t }
46
+ related_where(klass, ["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{MakeTaggable::Tagging.table_name}.taggable_id AND #{MakeTaggable::Tagging.table_name}.taggable_type = '#{klass.base_class}' AND #{MakeTaggable::Tagging.table_name}.tag_id = #{MakeTaggable::Tag.table_name}.#{MakeTaggable::Tag.primary_key} AND #{MakeTaggable::Tag.table_name}.name IN (?) AND #{MakeTaggable::Tagging.table_name}.context = ?", tags_to_find, context])
47
+ end
48
+
49
+ private
50
+
51
+ def exclude_self(klass, id)
52
+ "#{klass.arel_table[klass.primary_key].not_eq(id).to_sql} AND" if [self.class.base_class, self.class].include? klass
53
+ end
54
+
55
+ def group_columns(klass)
56
+ if MakeTaggable::Utils.using_postgresql?
57
+ grouped_column_names_for(klass)
58
+ else
59
+ "#{klass.table_name}.#{klass.primary_key}"
60
+ end
61
+ end
62
+
63
+ def related_where(klass, conditions)
64
+ klass.select("#{klass.table_name}.*, COUNT(#{MakeTaggable::Tag.table_name}.#{MakeTaggable::Tag.primary_key}) AS count")
65
+ .from("#{klass.table_name}, #{MakeTaggable::Tag.table_name}, #{MakeTaggable::Tagging.table_name}")
66
+ .group(group_columns(klass))
67
+ .order("count DESC")
68
+ .where(conditions)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,4 @@
1
+ module MakeTaggable::Taggable
2
+ class TagListType < ActiveModel::Type::Value
3
+ end
4
+ end
@@ -0,0 +1,16 @@
1
+ require_relative "tagged_with_query/query_base"
2
+ require_relative "tagged_with_query/exclude_tags_query"
3
+ require_relative "tagged_with_query/any_tags_query"
4
+ require_relative "tagged_with_query/all_tags_query"
5
+
6
+ module MakeTaggable::Taggable::TaggedWithQuery
7
+ def self.build(taggable_model, tag_model, tagging_model, tag_list, options)
8
+ if options[:exclude].present?
9
+ ExcludeTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build
10
+ elsif options[:any].present?
11
+ AnyTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build
12
+ else
13
+ AllTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,111 @@
1
+ module MakeTaggable::Taggable::TaggedWithQuery
2
+ class AllTagsQuery < QueryBase
3
+ def build
4
+ taggable_model.joins(each_tag_in_list)
5
+ .group(by_taggable)
6
+ .having(tags_that_matches_count)
7
+ .order(order_conditions)
8
+ .readonly(false)
9
+ end
10
+
11
+ private
12
+
13
+ def each_tag_in_list
14
+ arel_join = taggable_arel_table
15
+
16
+ tag_list.each do |tag|
17
+ tagging_alias = tagging_arel_table.alias(tagging_alias(tag))
18
+ arel_join = arel_join
19
+ .join(tagging_alias)
20
+ .on(on_conditions(tag, tagging_alias))
21
+ end
22
+
23
+ if options[:match_all].present?
24
+ arel_join = arel_join
25
+ .join(tagging_arel_table, Arel::Nodes::OuterJoin)
26
+ .on(
27
+ match_all_on_conditions
28
+ )
29
+ end
30
+
31
+ arel_join.join_sources
32
+ end
33
+
34
+ def on_conditions(tag, tagging_alias)
35
+ on_condition = tagging_alias[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key])
36
+ .and(tagging_alias[:taggable_type].eq(taggable_model.base_class.name))
37
+ .and(
38
+ tagging_alias[:tag_id].in(
39
+ tag_arel_table.project(tag_arel_table[:id]).where(tag_match_type(tag))
40
+ )
41
+ )
42
+
43
+ if options[:start_at].present?
44
+ on_condition = on_condition.and(tagging_alias[:created_at].gteq(options[:start_at]))
45
+ end
46
+
47
+ if options[:end_at].present?
48
+ on_condition = on_condition.and(tagging_alias[:created_at].lteq(options[:end_at]))
49
+ end
50
+
51
+ if options[:on].present?
52
+ on_condition = on_condition.and(tagging_alias[:context].eq(options[:on]))
53
+ end
54
+
55
+ if (owner = options[:owned_by]).present?
56
+ on_condition = on_condition.and(tagging_alias[:tagger_id].eq(owner.id))
57
+ .and(tagging_alias[:tagger_type].eq(owner.class.base_class.to_s))
58
+ end
59
+
60
+ on_condition
61
+ end
62
+
63
+ def match_all_on_conditions
64
+ on_condition = tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key])
65
+ .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
66
+
67
+ if options[:start_at].present?
68
+ on_condition = on_condition.and(tagging_arel_table[:created_at].gteq(options[:start_at]))
69
+ end
70
+
71
+ if options[:end_at].present?
72
+ on_condition = on_condition.and(tagging_arel_table[:created_at].lteq(options[:end_at]))
73
+ end
74
+
75
+ if options[:on].present?
76
+ on_condition = on_condition.and(tagging_arel_table[:context].eq(options[:on]))
77
+ end
78
+
79
+ on_condition
80
+ end
81
+
82
+ def by_taggable
83
+ return [] unless options[:match_all].present?
84
+
85
+ taggable_arel_table[taggable_model.primary_key]
86
+ end
87
+
88
+ def tags_that_matches_count
89
+ return [] unless options[:match_all].present?
90
+
91
+ taggable_model.find_by_sql(tag_arel_table.project(Arel.star.count).where(tags_match_type).to_sql)
92
+
93
+ tagging_arel_table[:taggable_id].count.eq(
94
+ tag_arel_table.project(Arel.star.count).where(tags_match_type)
95
+ )
96
+ end
97
+
98
+ def order_conditions
99
+ order_by = []
100
+ order_by << tagging_arel_table.project(tagging_arel_table[Arel.star].count.as("taggings_count")).order("taggings_count DESC").to_sql if options[:order_by_matching_tag_count].present? && options[:match_all].blank?
101
+
102
+ order_by << options[:order] if options[:order].present?
103
+ order_by.join(", ")
104
+ end
105
+
106
+ def tagging_alias(tag)
107
+ alias_base_name = taggable_model.base_class.name.downcase
108
+ adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{MakeTaggable::Utils.sha_prefix(tag)}")
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,68 @@
1
+ module MakeTaggable::Taggable::TaggedWithQuery
2
+ class AnyTagsQuery < QueryBase
3
+ def build
4
+ taggable_model.select(all_fields)
5
+ .where(model_has_at_least_one_tag)
6
+ .order(Arel.sql(order_conditions))
7
+ .readonly(false)
8
+ end
9
+
10
+ private
11
+
12
+ def all_fields
13
+ taggable_arel_table[Arel.star]
14
+ end
15
+
16
+ def model_has_at_least_one_tag
17
+ tagging_arel_table.project(Arel.star).where(at_least_one_tag).exists
18
+ end
19
+
20
+ def at_least_one_tag
21
+ exists_contition = tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key])
22
+ .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
23
+ .and(
24
+ tagging_arel_table[:tag_id].in(
25
+ tag_arel_table.project(tag_arel_table[:id]).where(tags_match_type)
26
+ )
27
+ )
28
+
29
+ if options[:start_at].present?
30
+ exists_contition = exists_contition.and(tagging_arel_table[:created_at].gteq(options[:start_at]))
31
+ end
32
+
33
+ if options[:end_at].present?
34
+ exists_contition = exists_contition.and(tagging_arel_table[:created_at].lteq(options[:end_at]))
35
+ end
36
+
37
+ if options[:on].present?
38
+ exists_contition = exists_contition.and(tagging_arel_table[:context].eq(options[:on]))
39
+ end
40
+
41
+ if (owner = options[:owned_by]).present?
42
+ exists_contition = exists_contition.and(tagging_arel_table[:tagger_id].eq(owner.id))
43
+ .and(tagging_arel_table[:tagger_type].eq(owner.class.base_class.to_s))
44
+ end
45
+
46
+ exists_contition
47
+ end
48
+
49
+ def order_conditions
50
+ order_by = []
51
+ if options[:order_by_matching_tag_count].present?
52
+ order_by << "(SELECT count(*) FROM #{tagging_model.table_name} WHERE #{at_least_one_tag.to_sql}) desc"
53
+ end
54
+
55
+ order_by << options[:order] if options[:order].present?
56
+ order_by.join(", ")
57
+ end
58
+
59
+ def alias_name(tag_list)
60
+ alias_base_name = taggable_model.base_class.name.downcase
61
+ taggings_context = options[:on] ? "_#{options[:on]}" : ""
62
+
63
+ adjust_taggings_alias(
64
+ "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{MakeTaggable::Utils.sha_prefix(tag_list.join("_"))}"
65
+ )
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,81 @@
1
+ module MakeTaggable::Taggable::TaggedWithQuery
2
+ class ExcludeTagsQuery < QueryBase
3
+ def build
4
+ taggable_model.joins(owning_to_tagger)
5
+ .where(tags_not_in_list)
6
+ .having(tags_that_matches_count)
7
+ .readonly(false)
8
+ end
9
+
10
+ private
11
+
12
+ def tags_not_in_list
13
+ taggable_arel_table[:id].not_in(
14
+ tagging_arel_table
15
+ .project(tagging_arel_table[:taggable_id])
16
+ .join(tag_arel_table)
17
+ .on(
18
+ tagging_arel_table[:tag_id].eq(tag_arel_table[:id])
19
+ .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
20
+ .and(tags_match_type)
21
+ )
22
+ )
23
+
24
+ # FIXME: missing time scope, this is also missing in the original implementation
25
+ end
26
+
27
+ def owning_to_tagger
28
+ return [] unless options[:owned_by].present?
29
+
30
+ owner = options[:owned_by]
31
+
32
+ arel_join = taggable_arel_table
33
+ .join(tagging_arel_table)
34
+ .on(
35
+ tagging_arel_table[:tagger_id].eq(owner.id)
36
+ .and(tagging_arel_table[:tagger_type].eq(owner.class.base_class.to_s))
37
+ .and(tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key]))
38
+ .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
39
+ )
40
+
41
+ if options[:match_all].present?
42
+ arel_join = arel_join
43
+ .join(tagging_arel_table, Arel::Nodes::OuterJoin)
44
+ .on(
45
+ match_all_on_conditions
46
+ )
47
+ end
48
+
49
+ arel_join.join_sources
50
+ end
51
+
52
+ def match_all_on_conditions
53
+ on_condition = tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key])
54
+ .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
55
+
56
+ if options[:start_at].present?
57
+ on_condition = on_condition.and(tagging_arel_table[:created_at].gteq(options[:start_at]))
58
+ end
59
+
60
+ if options[:end_at].present?
61
+ on_condition = on_condition.and(tagging_arel_table[:created_at].lteq(options[:end_at]))
62
+ end
63
+
64
+ if options[:on].present?
65
+ on_condition = on_condition.and(tagging_arel_table[:context].eq(options[:on]))
66
+ end
67
+
68
+ on_condition
69
+ end
70
+
71
+ def tags_that_matches_count
72
+ return [] unless options[:match_all].present?
73
+
74
+ taggable_model.find_by_sql(tag_arel_table.project(Arel.star.count).where(tags_match_type).to_sql)
75
+
76
+ tagging_arel_table[:taggable_id].count.eq(
77
+ tag_arel_table.project(Arel.star.count).where(tags_match_type)
78
+ )
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,61 @@
1
+ module MakeTaggable::Taggable::TaggedWithQuery
2
+ class QueryBase
3
+ def initialize(taggable_model, tag_model, tagging_model, tag_list, options)
4
+ @taggable_model = taggable_model
5
+ @tag_model = tag_model
6
+ @tagging_model = tagging_model
7
+ @tag_list = tag_list
8
+ @options = options
9
+ end
10
+
11
+ private
12
+
13
+ attr_reader :taggable_model, :tag_model, :tagging_model, :tag_list, :options
14
+
15
+ def taggable_arel_table
16
+ @taggable_arel_table ||= taggable_model.arel_table
17
+ end
18
+
19
+ def tag_arel_table
20
+ @tag_arel_table ||= tag_model.arel_table
21
+ end
22
+
23
+ def tagging_arel_table
24
+ @tagging_arel_table ||= tagging_model.arel_table
25
+ end
26
+
27
+ def tag_match_type(tag)
28
+ matches_attribute = tag_arel_table[:name]
29
+ matches_attribute = matches_attribute.lower unless MakeTaggable.strict_case_match
30
+
31
+ if options[:wild].present?
32
+ matches_attribute.matches("%#{escaped_tag(tag)}%", "!")
33
+ else
34
+ matches_attribute.matches(escaped_tag(tag), "!")
35
+ end
36
+ end
37
+
38
+ def tags_match_type
39
+ matches_attribute = tag_arel_table[:name]
40
+ matches_attribute = matches_attribute.lower unless MakeTaggable.strict_case_match
41
+
42
+ if options[:wild].present?
43
+ matches_attribute.matches_any(tag_list.map { |tag| "%#{escaped_tag(tag)}%" }, "!")
44
+ else
45
+ matches_attribute.matches_any(tag_list.map { |tag| escaped_tag(tag).to_s }, "!")
46
+ end
47
+ end
48
+
49
+ def escaped_tag(tag)
50
+ tag = tag.downcase unless MakeTaggable.strict_case_match
51
+ MakeTaggable::Utils.escape_like(tag)
52
+ end
53
+
54
+ def adjust_taggings_alias(taggings_alias)
55
+ if taggings_alias.size > 75
56
+ taggings_alias = "taggings_alias_" + Digest::SHA1.hexdigest(taggings_alias)
57
+ end
58
+ taggings_alias
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,89 @@
1
+ module MakeTaggable
2
+ module Tagger
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ ##
9
+ # Make a model a tagger. This allows an instance of a model to claim ownership
10
+ # of tags.
11
+ #
12
+ # Example:
13
+ # class User < ActiveRecord::Base
14
+ # acts_as_tagger
15
+ # end
16
+ def acts_as_tagger(opts = {})
17
+ class_eval do
18
+ owned_taggings_scope = opts.delete(:scope)
19
+
20
+ has_many :owned_taggings, owned_taggings_scope,
21
+ opts.merge(
22
+ as: :tagger,
23
+ class_name: "::MakeTaggable::Tagging",
24
+ dependent: :destroy
25
+ )
26
+
27
+ has_many :owned_tags, -> { distinct },
28
+ class_name: "::MakeTaggable::Tag",
29
+ source: :tag,
30
+ through: :owned_taggings
31
+ end
32
+
33
+ include MakeTaggable::Tagger::InstanceMethods
34
+ extend MakeTaggable::Tagger::SingletonMethods
35
+ end
36
+
37
+ def tagger?
38
+ false
39
+ end
40
+
41
+ def is_tagger?
42
+ tagger?
43
+ end
44
+ end
45
+
46
+ module InstanceMethods
47
+ ##
48
+ # Tag a taggable model with tags that are owned by the tagger.
49
+ #
50
+ # @param taggable The object that will be tagged
51
+ # @param [Hash] options An hash with options. Available options are:
52
+ # * <tt>:with</tt> - The tags that you want to
53
+ # * <tt>:on</tt> - The context on which you want to tag
54
+ #
55
+ # Example:
56
+ # @user.tag(@photo, :with => "paris, normandy", :on => :locations)
57
+ def tag(taggable, opts = {})
58
+ opts.reverse_merge!(force: true)
59
+ skip_save = opts.delete(:skip_save)
60
+ return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
61
+
62
+ fail "You need to specify a tag context using :on" unless opts.key?(:on)
63
+ fail "You need to specify some tags using :with" unless opts.key?(:with)
64
+ fail "No context :#{opts[:on]} defined in #{taggable.class}" unless opts[:force] || taggable.tag_types.include?(opts[:on])
65
+
66
+ taggable.set_owner_tag_list_on(self, opts[:on].to_s, opts[:with])
67
+ taggable.save unless skip_save
68
+ end
69
+
70
+ def tagger?
71
+ self.class.is_tagger?
72
+ end
73
+
74
+ def is_tagger?
75
+ tagger?
76
+ end
77
+ end
78
+
79
+ module SingletonMethods
80
+ def tagger?
81
+ true
82
+ end
83
+
84
+ def is_tagger?
85
+ tagger?
86
+ end
87
+ end
88
+ end
89
+ end