no_fly_list 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/generators/no_fly_list/install_generator.rb +8 -8
- data/lib/generators/no_fly_list/models_generator.rb +7 -7
- data/lib/generators/no_fly_list/tagging_generator.rb +8 -8
- data/lib/generators/no_fly_list/templates/tag_transformer.rb +1 -1
- data/lib/generators/no_fly_list/transformer_generator.rb +6 -6
- data/lib/no_fly_list/railtie.rb +7 -7
- data/lib/no_fly_list/railties/tasks.rake +6 -6
- data/lib/no_fly_list/taggable_record/config.rb +4 -3
- data/lib/no_fly_list/taggable_record/configuration.rb +22 -19
- data/lib/no_fly_list/taggable_record/query/mysql_strategy.rb +6 -6
- data/lib/no_fly_list/taggable_record/query/postgresql_strategy.rb +6 -6
- data/lib/no_fly_list/taggable_record/query/sqlite_strategy.rb +10 -10
- data/lib/no_fly_list/taggable_record/tag_setup.rb +5 -3
- data/lib/no_fly_list/taggable_record.rb +17 -0
- data/lib/no_fly_list/tagging_proxy.rb +45 -16
- data/lib/no_fly_list/test_helper.rb +2 -2
- data/lib/no_fly_list/version.rb +1 -1
- data/lib/no_fly_list.rb +8 -8
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8e5fd47ff5074ba83503a9c1633bada42f7348aafb9a914d2c5f51a70820427d
|
4
|
+
data.tar.gz: a337a25cdce43d598fd501266ba0d99665c3adb183f994121fd201082e4e874b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a0063a48bd720527540437b156136c704bd9deea0c4985fb34a9c4a7595c3c932d3c9b13ab5b8032daf277b5e547b358b5343410bdb6bedae1e466bc081edecd
|
7
|
+
data.tar.gz: 2f430e49bc7a6cc8ed09b1aa5686121c8d90d2262f7a6cce48de48e208804ab77899227e2b48ace8a6b017a9147fd368136ce42b99eeaab9668361e1bf10acfc
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
3
|
+
require "rails/generators"
|
4
|
+
require "rails/generators/active_record"
|
5
|
+
require "rails/generators/named_base"
|
6
6
|
|
7
7
|
# Usage:
|
8
8
|
# bin/rails generate no_fly_list:application_tag
|
@@ -11,15 +11,15 @@ module NoFlyList
|
|
11
11
|
module Generators
|
12
12
|
class InstallGenerator < Rails::Generators::Base
|
13
13
|
include Rails::Generators::Migration
|
14
|
-
source_root File.expand_path(
|
14
|
+
source_root File.expand_path("templates", __dir__)
|
15
15
|
|
16
|
-
argument :connection_name, type: :string, desc:
|
16
|
+
argument :connection_name, type: :string, desc: "The name of the database connection", default: "primary"
|
17
17
|
|
18
18
|
def copy_application_tag
|
19
19
|
ensure_connection_exists
|
20
|
-
template
|
21
|
-
template
|
22
|
-
migration_template
|
20
|
+
template "application_tag.rb.erb", File.join("app/models", "application_tag.rb")
|
21
|
+
template "application_tagging.rb.erb", File.join("app/models", "application_tagging.rb")
|
22
|
+
migration_template "create_application_tagging_table.rb.erb", "db/migrate/create_application_tagging_table.rb"
|
23
23
|
end
|
24
24
|
|
25
25
|
def self.next_migration_number(dirname)
|
@@ -1,20 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
3
|
+
require "forwardable"
|
4
|
+
require "rails/generators"
|
5
|
+
require "rails/generators/active_record"
|
6
|
+
require "rails/generators/named_base"
|
7
7
|
|
8
8
|
module NoFlyList
|
9
9
|
module Generators
|
10
10
|
class ModelsGenerator < Rails::Generators::NamedBase
|
11
|
-
source_root File.expand_path(
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
12
12
|
|
13
13
|
def create_model_files
|
14
14
|
return unless validate_model # Ensure it's an ActiveRecord model
|
15
15
|
|
16
|
-
template
|
17
|
-
template
|
16
|
+
template "tagging_model.rb.erb", File.join("app/models", class_path, "#{file_name.underscore}/tagging.rb")
|
17
|
+
template "tag_model.rb.erb", File.join("app/models", class_path, "#{file_name.underscore}_tag.rb")
|
18
18
|
end
|
19
19
|
|
20
20
|
private
|
@@ -1,17 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
3
|
+
require "forwardable"
|
4
|
+
require "rails/generators"
|
5
|
+
require "rails/generators/active_record"
|
6
|
+
require "rails/generators/named_base"
|
7
7
|
|
8
8
|
module NoFlyList
|
9
9
|
module Generators
|
10
10
|
class TaggingGenerator < Rails::Generators::NamedBase
|
11
11
|
include ActiveRecord::Generators::Migration
|
12
12
|
|
13
|
-
class_option :database, type: :string, default:
|
14
|
-
desc:
|
13
|
+
class_option :database, type: :string, default: "primary",
|
14
|
+
desc: "Use different database for migration"
|
15
15
|
|
16
16
|
def self.default_generator_root
|
17
17
|
File.dirname(__FILE__)
|
@@ -19,8 +19,8 @@ module NoFlyList
|
|
19
19
|
|
20
20
|
def create_migration_file
|
21
21
|
ensure_model_exists
|
22
|
-
migration_template
|
23
|
-
[db_migrate_path, "create_#{migration_name}.rb"].compact.join(
|
22
|
+
migration_template "create_tagging_table.rb.erb",
|
23
|
+
[ db_migrate_path, "create_#{migration_name}.rb" ].compact.join("/")
|
24
24
|
end
|
25
25
|
|
26
26
|
def self.next_migration_number(dirname)
|
@@ -1,18 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
3
|
+
require "forwardable"
|
4
|
+
require "rails/generators"
|
5
|
+
require "rails/generators/active_record"
|
6
|
+
require "rails/generators/named_base"
|
7
7
|
|
8
8
|
unless defined?(ApplicationTagTransformer)
|
9
9
|
module NoFlyList
|
10
10
|
module Generators
|
11
11
|
# bin/rails g no_fly_list:transformer
|
12
12
|
class TransformerGenerator < Rails::Generators::Base
|
13
|
-
source_root File.expand_path(
|
13
|
+
source_root File.expand_path("templates", __dir__)
|
14
14
|
def create_tag_transformer_file
|
15
|
-
template
|
15
|
+
template "tag_transformer.rb", File.join("app/transformers", "application_tag_transformer.rb")
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
data/lib/no_fly_list/railtie.rb
CHANGED
@@ -1,18 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "active_record/railtie"
|
4
|
+
require "rails"
|
5
5
|
|
6
6
|
module NoFlyList
|
7
7
|
class Railtie < Rails::Railtie # :nodoc:
|
8
8
|
config.no_fly_list = ActiveSupport::OrderedOptions.new
|
9
|
-
config.no_fly_list.tag_class_name =
|
10
|
-
config.no_fly_list.tag_table_name =
|
11
|
-
config.no_fly_list.tagging_class_name =
|
12
|
-
config.no_fly_list.tagging_table_name =
|
9
|
+
config.no_fly_list.tag_class_name = "ApplicationTag"
|
10
|
+
config.no_fly_list.tag_table_name = "application_tags"
|
11
|
+
config.no_fly_list.tagging_class_name = "ApplicationTagging"
|
12
|
+
config.no_fly_list.tagging_table_name = "application_taggings"
|
13
13
|
|
14
14
|
rake_tasks do
|
15
|
-
load
|
15
|
+
load "no_fly_list/railties/tasks.rake"
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
namespace :no_fly_list do
|
4
|
-
desc
|
4
|
+
desc "List all taggable records"
|
5
5
|
task taggable_records: :environment do
|
6
6
|
Rails.application.eager_load!
|
7
7
|
taggable_classes = ActiveRecord::Base.descendants.select do |klass|
|
8
|
-
klass.included_modules.any? { |mod| mod.in?([NoFlyList::TaggableRecord]) }
|
8
|
+
klass.included_modules.any? { |mod| mod.in?([ NoFlyList::TaggableRecord ]) }
|
9
9
|
end
|
10
10
|
|
11
11
|
puts "Found #{taggable_classes.size} taggable classes:\n\n"
|
@@ -15,11 +15,11 @@ namespace :no_fly_list do
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
-
desc
|
18
|
+
desc "List all tag records"
|
19
19
|
task tag_records: :environment do
|
20
20
|
Rails.application.eager_load!
|
21
21
|
tag_classes = ActiveRecord::Base.descendants.select do |klass|
|
22
|
-
klass.included_modules.any? { |mod| mod.in?([NoFlyList::ApplicationTag, NoFlyList::TagRecord]) }
|
22
|
+
klass.included_modules.any? { |mod| mod.in?([ NoFlyList::ApplicationTag, NoFlyList::TagRecord ]) }
|
23
23
|
end
|
24
24
|
|
25
25
|
puts "Found #{tag_classes.size} tag classes:\n\n"
|
@@ -29,11 +29,11 @@ namespace :no_fly_list do
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
desc
|
32
|
+
desc "Check taggable records and their associated tables"
|
33
33
|
task check_taggable_records: :environment do
|
34
34
|
Rails.application.eager_load!
|
35
35
|
taggable_classes = ActiveRecord::Base.descendants.select do |klass|
|
36
|
-
klass.included_modules.any? { |mod| mod.in?([NoFlyList::TaggableRecord]) }
|
36
|
+
klass.included_modules.any? { |mod| mod.in?([ NoFlyList::TaggableRecord ]) }
|
37
37
|
end
|
38
38
|
|
39
39
|
puts "Checking #{taggable_classes.size} taggable classes:\n\n"
|
@@ -24,7 +24,8 @@ module NoFlyList
|
|
24
24
|
restrict_to_existing: options.fetch(:restrict_to_existing, false),
|
25
25
|
limit: options.fetch(:limit, nil),
|
26
26
|
case_sensitive: options.fetch(:case_sensitive, true),
|
27
|
-
adapter: @adapter
|
27
|
+
adapter: @adapter,
|
28
|
+
counter_cache: options.fetch(:counter_cache, false)
|
28
29
|
}
|
29
30
|
end
|
30
31
|
|
@@ -32,9 +33,9 @@ module NoFlyList
|
|
32
33
|
|
33
34
|
def determine_adapter
|
34
35
|
case taggable_class.connection.adapter_name.downcase
|
35
|
-
when
|
36
|
+
when "postgresql"
|
36
37
|
:postgresql
|
37
|
-
when
|
38
|
+
when "mysql2"
|
38
39
|
:mysql
|
39
40
|
else
|
40
41
|
:sqlite
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
4
|
-
require_relative
|
5
|
-
require_relative
|
3
|
+
require_relative "mutation"
|
4
|
+
require_relative "query"
|
5
|
+
require_relative "tag_setup"
|
6
6
|
|
7
7
|
module NoFlyList
|
8
8
|
module TaggableRecord
|
@@ -90,11 +90,12 @@ module NoFlyList
|
|
90
90
|
# Add the basic associations
|
91
91
|
belongs_to :tag,
|
92
92
|
class_name: setup.tag_class_name,
|
93
|
-
foreign_key:
|
93
|
+
foreign_key: "tag_id"
|
94
94
|
|
95
95
|
belongs_to :taggable,
|
96
96
|
class_name: setup.taggable_klass.name,
|
97
|
-
foreign_key:
|
97
|
+
foreign_key: "taggable_id",
|
98
|
+
counter_cache: setup.counter_cache ? "#{setup.context}_count" : nil
|
98
99
|
|
99
100
|
include NoFlyList::TaggingRecord
|
100
101
|
end
|
@@ -118,12 +119,12 @@ module NoFlyList
|
|
118
119
|
# Set up the tag model associations
|
119
120
|
setup.tag_class_name.constantize.class_eval do
|
120
121
|
# Fix: Use 'tagging' for the join association when context is 'tag'
|
121
|
-
association_name = (singular_name ==
|
122
|
+
association_name = (singular_name == "tag" ? :taggings : :"#{singular_name}_taggings")
|
122
123
|
|
123
124
|
has_many association_name,
|
124
125
|
-> { where(context: singular_name) },
|
125
126
|
class_name: setup.tagging_class_name,
|
126
|
-
foreign_key:
|
127
|
+
foreign_key: "tag_id",
|
127
128
|
dependent: :destroy
|
128
129
|
|
129
130
|
# Fix: Use consistent naming for through association
|
@@ -137,7 +138,7 @@ module NoFlyList
|
|
137
138
|
setup.tagging_class_name.constantize.class_eval do
|
138
139
|
belongs_to :tag,
|
139
140
|
class_name: setup.tag_class_name,
|
140
|
-
foreign_key:
|
141
|
+
foreign_key: "tag_id"
|
141
142
|
|
142
143
|
belongs_to :taggable,
|
143
144
|
polymorphic: true
|
@@ -149,19 +150,21 @@ module NoFlyList
|
|
149
150
|
|
150
151
|
validates :tag_id, uniqueness: {
|
151
152
|
scope: %i[taggable_type taggable_id context],
|
152
|
-
message:
|
153
|
+
message: "has already been tagged on this record in this context"
|
153
154
|
}
|
154
155
|
end
|
155
156
|
end
|
156
157
|
|
157
158
|
# Sets up associations for local (non-global) tags
|
158
159
|
def setup_local_tag_associations(setup, singular_name)
|
160
|
+
plural_name = setup.context.to_s
|
159
161
|
# Set up tag class associations
|
160
162
|
setup.tag_class_name.constantize.class_eval do
|
161
163
|
has_many :"#{singular_name}_taggings",
|
162
164
|
-> { where(context: singular_name) },
|
163
165
|
class_name: setup.tagging_class_name,
|
164
|
-
foreign_key:
|
166
|
+
foreign_key: "tag_id",
|
167
|
+
counter_cache: setup.counter_cache ? "#{plural_name}_count" : nil,
|
165
168
|
dependent: :destroy
|
166
169
|
|
167
170
|
has_many :"#{singular_name}_taggables",
|
@@ -173,17 +176,17 @@ module NoFlyList
|
|
173
176
|
setup.tagging_class_name.constantize.class_eval do
|
174
177
|
belongs_to :tag,
|
175
178
|
class_name: setup.tag_class_name,
|
176
|
-
foreign_key:
|
179
|
+
foreign_key: "tag_id"
|
177
180
|
|
178
181
|
# For local tags, we use a simple belongs_to without polymorphic
|
179
182
|
belongs_to :taggable,
|
180
183
|
class_name: setup.taggable_klass.name,
|
181
|
-
foreign_key:
|
184
|
+
foreign_key: "taggable_id"
|
182
185
|
|
183
186
|
validates :tag, :taggable, :context, presence: true
|
184
187
|
validates :tag_id, uniqueness: {
|
185
188
|
scope: %i[taggable_id context],
|
186
|
-
message:
|
189
|
+
message: "has already been tagged on this record in this context"
|
187
190
|
}
|
188
191
|
end
|
189
192
|
end
|
@@ -196,7 +199,7 @@ module NoFlyList
|
|
196
199
|
has_many :"#{singular_name}_taggings",
|
197
200
|
-> { where(context: singular_name) },
|
198
201
|
class_name: setup.tagging_class_name,
|
199
|
-
foreign_key:
|
202
|
+
foreign_key: "taggable_id",
|
200
203
|
as: :taggable,
|
201
204
|
dependent: :destroy
|
202
205
|
|
@@ -217,7 +220,7 @@ module NoFlyList
|
|
217
220
|
has_many :"#{singular_name}_taggings",
|
218
221
|
-> { where(context: singular_name) },
|
219
222
|
class_name: setup.tagging_class_name,
|
220
|
-
foreign_key:
|
223
|
+
foreign_key: "taggable_id",
|
221
224
|
dependent: :destroy
|
222
225
|
|
223
226
|
has_many setup.context,
|
@@ -237,9 +240,9 @@ module NoFlyList
|
|
237
240
|
define_method :create_and_set_proxy do |instance_variable_name, setup|
|
238
241
|
tag_model = if setup.polymorphic
|
239
242
|
setup.tag_class_name.constantize
|
240
|
-
|
243
|
+
else
|
241
244
|
self.class.const_get("#{self.class.name}Tag")
|
242
|
-
|
245
|
+
end
|
243
246
|
|
244
247
|
proxy = TaggingProxy.new(
|
245
248
|
self,
|
@@ -298,9 +301,9 @@ module NoFlyList
|
|
298
301
|
end
|
299
302
|
|
300
303
|
def define_constant_in_namespace(const_name)
|
301
|
-
parts = const_name.split(
|
304
|
+
parts = const_name.split("::")
|
302
305
|
const_name = parts.pop
|
303
|
-
namespace = parts.join(
|
306
|
+
namespace = parts.join("::").safe_constantize || Object
|
304
307
|
return if namespace.const_defined?(const_name, false)
|
305
308
|
|
306
309
|
namespace.const_set(const_name, yield)
|
@@ -40,8 +40,8 @@ module NoFlyList
|
|
40
40
|
return none if tags.empty?
|
41
41
|
|
42
42
|
count_function = Arel::Nodes::NamedFunction.new(
|
43
|
-
|
44
|
-
[Arel::Nodes::NamedFunction.new(
|
43
|
+
"COUNT",
|
44
|
+
[ Arel::Nodes::NamedFunction.new("DISTINCT", [ tag_table[:name] ]) ]
|
45
45
|
)
|
46
46
|
|
47
47
|
query = Arel::SelectManager.new(self)
|
@@ -78,11 +78,11 @@ module NoFlyList
|
|
78
78
|
.where(context: singular_name)
|
79
79
|
.where(taggable_type: name)
|
80
80
|
.select(:taggable_id)
|
81
|
-
|
81
|
+
else
|
82
82
|
setup.tagging_class_name.constantize
|
83
83
|
.where(context: singular_name)
|
84
84
|
.select(:taggable_id)
|
85
|
-
|
85
|
+
end
|
86
86
|
|
87
87
|
where("#{table_name}.#{primary_key} NOT IN (?)", subquery)
|
88
88
|
}
|
@@ -95,8 +95,8 @@ module NoFlyList
|
|
95
95
|
send("without_#{context}")
|
96
96
|
else
|
97
97
|
Arel::Nodes::NamedFunction.new(
|
98
|
-
|
99
|
-
[Arel::Nodes::NamedFunction.new(
|
98
|
+
"COUNT",
|
99
|
+
[ Arel::Nodes::NamedFunction.new("DISTINCT", [ tag_table[:id] ]) ]
|
100
100
|
)
|
101
101
|
|
102
102
|
# Build the query for records having exactly the tags
|
@@ -40,8 +40,8 @@ module NoFlyList
|
|
40
40
|
return none if tags.empty?
|
41
41
|
|
42
42
|
count_function = Arel::Nodes::NamedFunction.new(
|
43
|
-
|
44
|
-
[Arel::Nodes::NamedFunction.new(
|
43
|
+
"COUNT",
|
44
|
+
[ Arel::Nodes::NamedFunction.new("DISTINCT", [ tag_table[:name] ]) ]
|
45
45
|
)
|
46
46
|
|
47
47
|
query = Arel::SelectManager.new(self)
|
@@ -78,11 +78,11 @@ module NoFlyList
|
|
78
78
|
.where(context: singular_name)
|
79
79
|
.where(taggable_type: name)
|
80
80
|
.select(:taggable_id)
|
81
|
-
|
81
|
+
else
|
82
82
|
setup.tagging_class_name.constantize
|
83
83
|
.where(context: singular_name)
|
84
84
|
.select(:taggable_id)
|
85
|
-
|
85
|
+
end
|
86
86
|
|
87
87
|
where.not(primary_key => subquery)
|
88
88
|
}
|
@@ -95,8 +95,8 @@ module NoFlyList
|
|
95
95
|
send("without_#{context}")
|
96
96
|
else
|
97
97
|
Arel::Nodes::NamedFunction.new(
|
98
|
-
|
99
|
-
[Arel::Nodes::NamedFunction.new(
|
98
|
+
"COUNT",
|
99
|
+
[ Arel::Nodes::NamedFunction.new("DISTINCT", [ tag_table[:id] ]) ]
|
100
100
|
)
|
101
101
|
|
102
102
|
# Build the query for records having exactly the tags
|
@@ -40,8 +40,8 @@ module NoFlyList
|
|
40
40
|
return none if tags.empty?
|
41
41
|
|
42
42
|
count_function = Arel::Nodes::NamedFunction.new(
|
43
|
-
|
44
|
-
[Arel::Nodes::NamedFunction.new(
|
43
|
+
"COUNT",
|
44
|
+
[ Arel::Nodes::NamedFunction.new("DISTINCT", [ tag_table[:name] ]) ]
|
45
45
|
)
|
46
46
|
|
47
47
|
query = Arel::SelectManager.new(self)
|
@@ -70,7 +70,7 @@ module NoFlyList
|
|
70
70
|
.pluck("#{table_name}.id")
|
71
71
|
|
72
72
|
# Handle empty tagged_ids explicitly for SQLite compatibility
|
73
|
-
where("#{table_name}.id NOT IN (?)", tagged_ids.present? ? tagged_ids : [-1])
|
73
|
+
where("#{table_name}.id NOT IN (?)", tagged_ids.present? ? tagged_ids : [ -1 ])
|
74
74
|
}
|
75
75
|
|
76
76
|
# Find records without any tags
|
@@ -79,12 +79,12 @@ module NoFlyList
|
|
79
79
|
setup.tagging_class_name.constantize
|
80
80
|
.where(context: singular_name, taggable_type: name)
|
81
81
|
.select(:taggable_id)
|
82
|
-
|
82
|
+
else
|
83
83
|
setup.tagging_class_name.constantize
|
84
84
|
.where(context: singular_name)
|
85
85
|
.select(:taggable_id)
|
86
|
-
|
87
|
-
where(
|
86
|
+
end
|
87
|
+
where("id NOT IN (?)", subquery)
|
88
88
|
}
|
89
89
|
|
90
90
|
# Find records with exactly these tags
|
@@ -95,8 +95,8 @@ module NoFlyList
|
|
95
95
|
send("without_#{context}")
|
96
96
|
else
|
97
97
|
Arel::Nodes::NamedFunction.new(
|
98
|
-
|
99
|
-
[Arel::Nodes::NamedFunction.new(
|
98
|
+
"COUNT",
|
99
|
+
[ Arel::Nodes::NamedFunction.new("DISTINCT", [ tag_table[:id] ]) ]
|
100
100
|
)
|
101
101
|
|
102
102
|
# Build the query for records having exactly the tags
|
@@ -130,8 +130,8 @@ module NoFlyList
|
|
130
130
|
send("without_#{context}")
|
131
131
|
else
|
132
132
|
Arel::Nodes::NamedFunction.new(
|
133
|
-
|
134
|
-
[Arel::Nodes::NamedFunction.new(
|
133
|
+
"COUNT",
|
134
|
+
[ Arel::Nodes::NamedFunction.new("DISTINCT", [ tag_table[:id] ]) ]
|
135
135
|
)
|
136
136
|
|
137
137
|
# Build the query for records having exactly the tags
|
@@ -4,7 +4,7 @@ module NoFlyList
|
|
4
4
|
module TaggableRecord
|
5
5
|
class TagSetup
|
6
6
|
attr_reader :taggable_klass, :context, :transformer, :polymorphic,
|
7
|
-
:restrict_to_existing, :limit,
|
7
|
+
:restrict_to_existing, :limit, :counter_cache,
|
8
8
|
:tag_class_name, :tagging_class_name, :adapter
|
9
9
|
|
10
10
|
def initialize(taggable_klass, context, options = {})
|
@@ -13,6 +13,8 @@ module NoFlyList
|
|
13
13
|
@transformer = options.fetch(:transformer, ApplicationTagTransformer)
|
14
14
|
@polymorphic = options.fetch(:polymorphic, false)
|
15
15
|
@restrict_to_existing = options.fetch(:restrict_to_existing, false)
|
16
|
+
@counter_cache = options.fetch(:counter_cache, false)
|
17
|
+
@counter_cache_column = "#{context}_count"
|
16
18
|
@limit = options.fetch(:limit, nil)
|
17
19
|
@tag_class_name = determine_tag_class_name(taggable_klass, options)
|
18
20
|
@tagging_class_name = determine_tagging_class_name(taggable_klass, options)
|
@@ -23,9 +25,9 @@ module NoFlyList
|
|
23
25
|
|
24
26
|
def determine_adapter
|
25
27
|
case ActiveRecord::Base.connection.adapter_name.downcase
|
26
|
-
when
|
28
|
+
when "postgresql"
|
27
29
|
:postgresql
|
28
|
-
when
|
30
|
+
when "mysql2"
|
29
31
|
:mysql
|
30
32
|
else
|
31
33
|
:sqlite
|
@@ -12,6 +12,10 @@ module NoFlyList
|
|
12
12
|
before_validation :validate_tag_proxies
|
13
13
|
end
|
14
14
|
|
15
|
+
def changed_for_autosave?
|
16
|
+
super || tag_proxies_changed?
|
17
|
+
end
|
18
|
+
|
15
19
|
private
|
16
20
|
|
17
21
|
def validate_tag_proxies
|
@@ -66,6 +70,19 @@ module NoFlyList
|
|
66
70
|
tag_contexts[context.to_sym]
|
67
71
|
end
|
68
72
|
|
73
|
+
def tag_proxies_changed?
|
74
|
+
return false if @saving_proxies || @validating_proxies
|
75
|
+
|
76
|
+
instance_variables.any? do |var|
|
77
|
+
next unless var.to_s.match?(/_list_proxy$/)
|
78
|
+
|
79
|
+
proxy = instance_variable_get(var)
|
80
|
+
next if proxy.nil?
|
81
|
+
|
82
|
+
proxy.changed?
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
69
86
|
class_methods do
|
70
87
|
def has_tags(*contexts, **options)
|
71
88
|
contexts.each do |context|
|
@@ -26,6 +26,10 @@ module NoFlyList
|
|
26
26
|
@pending_changes = []
|
27
27
|
end
|
28
28
|
|
29
|
+
def changed?
|
30
|
+
@pending_changes.present? && @pending_changes != current_list_from_database
|
31
|
+
end
|
32
|
+
|
29
33
|
def method_missing(method_name, *args)
|
30
34
|
if current_list.respond_to?(method_name)
|
31
35
|
current_list.send(method_name, *args)
|
@@ -47,7 +51,7 @@ module NoFlyList
|
|
47
51
|
end
|
48
52
|
|
49
53
|
def coerce(other)
|
50
|
-
[other, to_a]
|
54
|
+
[ other, to_a ]
|
51
55
|
end
|
52
56
|
|
53
57
|
def to_ary
|
@@ -65,13 +69,16 @@ module NoFlyList
|
|
65
69
|
model.class.transaction do
|
66
70
|
# Always save parent first if needed
|
67
71
|
if model.new_record? && !model.save
|
68
|
-
errors.add(:base,
|
72
|
+
errors.add(:base, "Failed to save parent record")
|
69
73
|
raise ActiveRecord::Rollback
|
70
74
|
end
|
71
75
|
|
72
76
|
# Clear existing tags
|
77
|
+
old_count = model.send(context_taggings).count
|
73
78
|
model.send(context_taggings).delete_all
|
74
79
|
|
80
|
+
# Update counter
|
81
|
+
model.update_column("#{@context}_count", 0) if setup[:counter_cache]
|
75
82
|
# Create new tags
|
76
83
|
@pending_changes.each do |tag_name|
|
77
84
|
tag = find_or_create_tag(tag_name)
|
@@ -91,6 +98,8 @@ module NoFlyList
|
|
91
98
|
model.send(context_taggings).create!(attributes)
|
92
99
|
end
|
93
100
|
end
|
101
|
+
# Update counter to match the actual count
|
102
|
+
model.update_column("#{@context}_count", @pending_changes.size) if setup[:counter_cache]
|
94
103
|
|
95
104
|
refresh_from_database
|
96
105
|
true
|
@@ -143,10 +152,10 @@ module NoFlyList
|
|
143
152
|
"#<#{self.class.name} tags=#{current_list.inspect} transformer_with=#{transformer_name} >"
|
144
153
|
end
|
145
154
|
|
146
|
-
def add(
|
155
|
+
def add(*tags)
|
147
156
|
return self if limit_reached?
|
148
157
|
|
149
|
-
new_tags = transformer.parse_tags(tag)
|
158
|
+
new_tags = tags.flatten.map { |tag| transformer.parse_tags(tag) }.flatten
|
150
159
|
return self if new_tags.empty?
|
151
160
|
|
152
161
|
@pending_changes = current_list + new_tags
|
@@ -154,13 +163,15 @@ module NoFlyList
|
|
154
163
|
self
|
155
164
|
end
|
156
165
|
|
157
|
-
def add!(
|
158
|
-
add(
|
166
|
+
def add!(*tags)
|
167
|
+
add(*tags)
|
159
168
|
save
|
160
169
|
end
|
161
170
|
|
162
171
|
def remove(tag)
|
163
|
-
|
172
|
+
old_list = current_list.dup
|
173
|
+
@pending_changes = current_list - [ tag.to_s.strip ]
|
174
|
+
mark_record_dirty if @pending_changes != old_list
|
164
175
|
self
|
165
176
|
end
|
166
177
|
|
@@ -170,13 +181,17 @@ module NoFlyList
|
|
170
181
|
end
|
171
182
|
|
172
183
|
def clear
|
184
|
+
old_list = current_list.dup
|
173
185
|
@pending_changes = []
|
186
|
+
mark_record_dirty if @pending_changes != old_list
|
187
|
+
model.write_attribute("#{@context}_count", 0) if setup[:counter_cache]
|
174
188
|
self
|
175
189
|
end
|
176
190
|
|
177
191
|
def clear!
|
178
192
|
@model.send(@context.to_s).destroy_all
|
179
193
|
@pending_changes = []
|
194
|
+
@model.update_column("#{@context}_count", 0) if setup[:counter_cache]
|
180
195
|
self
|
181
196
|
end
|
182
197
|
|
@@ -194,6 +209,19 @@ module NoFlyList
|
|
194
209
|
|
195
210
|
private
|
196
211
|
|
212
|
+
def current_list_from_database
|
213
|
+
if setup[:polymorphic]
|
214
|
+
tagging_table = setup[:tagging_class_name].tableize
|
215
|
+
@model.send(@context.to_s)
|
216
|
+
.joins("INNER JOIN #{tagging_table} ON #{tagging_table}.tag_id = tags.id")
|
217
|
+
.where("#{tagging_table}.taggable_type = ? AND #{tagging_table}.taggable_id = ?",
|
218
|
+
@model.class.name, @model.id)
|
219
|
+
.pluck(:name)
|
220
|
+
else
|
221
|
+
@model.send(@context.to_s).pluck(:name)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
197
225
|
def set_list(_context, value)
|
198
226
|
@pending_changes = transformer.parse_tags(value)
|
199
227
|
valid? # Just check validity without raising
|
@@ -221,7 +249,7 @@ module NoFlyList
|
|
221
249
|
|
222
250
|
# Transform tags to lowercase for comparison
|
223
251
|
normalized_changes = @pending_changes.map(&:downcase)
|
224
|
-
existing_tags = @tag_model.where(
|
252
|
+
existing_tags = @tag_model.where("LOWER(name) IN (?)", normalized_changes).pluck(:name)
|
225
253
|
missing_tags = @pending_changes - existing_tags
|
226
254
|
|
227
255
|
return unless missing_tags.any?
|
@@ -279,20 +307,21 @@ module NoFlyList
|
|
279
307
|
def current_list
|
280
308
|
if @pending_changes.any?
|
281
309
|
@pending_changes
|
282
|
-
elsif setup[:polymorphic]
|
283
|
-
tagging_table = setup[:tagging_class_name].tableize
|
284
|
-
@model.send(@context.to_s)
|
285
|
-
.joins("INNER JOIN #{tagging_table} ON #{tagging_table}.tag_id = tags.id")
|
286
|
-
.where("#{tagging_table}.taggable_type = ? AND #{tagging_table}.taggable_id = ?",
|
287
|
-
@model.class.name, @model.id)
|
288
|
-
.pluck(:name)
|
289
310
|
else
|
290
|
-
|
311
|
+
current_list_from_database
|
291
312
|
end
|
292
313
|
end
|
293
314
|
|
294
315
|
def limit_reached?
|
295
316
|
@limit && current_list.size >= @limit
|
296
317
|
end
|
318
|
+
|
319
|
+
def mark_record_dirty
|
320
|
+
return unless model.respond_to?(:changed_attributes)
|
321
|
+
|
322
|
+
# We use a virtual attribute name based on the context
|
323
|
+
# This ensures the record is marked as changed when tags are modified
|
324
|
+
model.send(:attribute_will_change!, "#{context}_list")
|
325
|
+
end
|
297
326
|
end
|
298
327
|
end
|
@@ -45,9 +45,9 @@ module NoFlyList
|
|
45
45
|
def assert_polymorphic_tag_classes_exist(tags_klass, tagging_klass)
|
46
46
|
# Verify they include the correct modules
|
47
47
|
assert tags_klass.include?(NoFlyList::ApplicationTag),
|
48
|
-
|
48
|
+
"Polymorphic Tag should include NoFlyList::ApplicationTag"
|
49
49
|
assert tagging_klass.include?(NoFlyList::ApplicationTagging),
|
50
|
-
|
50
|
+
"Polymorphic Tagging should include NoFlyList::ApplicationTagging"
|
51
51
|
end
|
52
52
|
|
53
53
|
def assert_local_tag_classes_exist(klass, context)
|
data/lib/no_fly_list/version.rb
CHANGED
data/lib/no_fly_list.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require_relative
|
8
|
-
require
|
3
|
+
require "active_record"
|
4
|
+
require "active_support"
|
5
|
+
require "active_support/rails"
|
6
|
+
require "active_support/core_ext/numeric/time"
|
7
|
+
require_relative "no_fly_list/version"
|
8
|
+
require "no_fly_list/railtie" if defined?(Rails)
|
9
9
|
|
10
10
|
module NoFlyList
|
11
11
|
extend ActiveSupport::Autoload
|
@@ -16,10 +16,10 @@ module NoFlyList
|
|
16
16
|
# Common tagging tables
|
17
17
|
autoload :TaggableRecord
|
18
18
|
|
19
|
-
autoload_under
|
19
|
+
autoload_under "taggable_record" do
|
20
20
|
autoload :Configuration
|
21
21
|
autoload :Config
|
22
|
-
autoload_under
|
22
|
+
autoload_under "taggable_record/query" do
|
23
23
|
autoload :SqliteStrategy
|
24
24
|
autoload :MysqlStrategy
|
25
25
|
autoload :PostgresqlStrategy
|