no_fly_list 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/generators/no_fly_list/templates/create_application_tagging_table.rb.erb +15 -5
- data/lib/generators/no_fly_list/templates/create_tagging_table.rb.erb +4 -2
- data/lib/no_fly_list/application_tag.rb +0 -7
- data/lib/no_fly_list/application_tagging.rb +0 -6
- data/lib/no_fly_list/tag_record.rb +2 -2
- data/lib/no_fly_list/taggable_record/config.rb +60 -0
- data/lib/no_fly_list/taggable_record/configuration.rb +154 -27
- data/lib/no_fly_list/taggable_record/query/mysql_strategy.rb +128 -0
- data/lib/no_fly_list/taggable_record/query/postgresql_strategy.rb +128 -0
- data/lib/no_fly_list/taggable_record/query/sqlite_strategy.rb +163 -0
- data/lib/no_fly_list/taggable_record/query.rb +16 -84
- data/lib/no_fly_list/taggable_record/tag_setup.rb +52 -0
- data/lib/no_fly_list/taggable_record.rb +55 -4
- data/lib/no_fly_list/tagging_proxy.rb +91 -14
- data/lib/no_fly_list/tagging_record.rb +3 -3
- data/lib/no_fly_list/test_helper.rb +85 -5
- data/lib/no_fly_list/version.rb +1 -1
- data/lib/no_fly_list.rb +8 -1
- metadata +7 -16
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoFlyList
|
4
|
+
module TaggableRecord
|
5
|
+
module Query
|
6
|
+
module PostgresqlStrategy
|
7
|
+
extend BaseStrategy
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def define_query_methods(setup)
|
12
|
+
context = setup.context
|
13
|
+
taggable_klass = setup.taggable_klass
|
14
|
+
tagging_klass = setup.tagging_class_name.constantize
|
15
|
+
tagging_table = tagging_klass.arel_table
|
16
|
+
tag_klass = setup.tag_class_name.constantize
|
17
|
+
tag_table = tag_klass.arel_table
|
18
|
+
singular_name = context.to_s.singularize
|
19
|
+
|
20
|
+
taggable_klass.class_eval do
|
21
|
+
# Find records with any of the specified tags
|
22
|
+
scope "with_any_#{context}", lambda { |*tags|
|
23
|
+
tags = tags.flatten.compact.uniq
|
24
|
+
return none if tags.empty?
|
25
|
+
|
26
|
+
query = Arel::SelectManager.new(self)
|
27
|
+
.from(table_name)
|
28
|
+
.project(arel_table[primary_key])
|
29
|
+
.distinct
|
30
|
+
.join(tagging_table).on(tagging_table[:taggable_id].eq(arel_table[primary_key]))
|
31
|
+
.join(tag_table).on(tag_table[:id].eq(tagging_table[:tag_id]))
|
32
|
+
.where(tagging_table[:context].eq(singular_name))
|
33
|
+
.where(tag_table[:name].in(tags))
|
34
|
+
|
35
|
+
where(arel_table[primary_key].in(query))
|
36
|
+
}
|
37
|
+
|
38
|
+
scope "with_all_#{context}", lambda { |*tags|
|
39
|
+
tags = tags.flatten.compact.uniq
|
40
|
+
return none if tags.empty?
|
41
|
+
|
42
|
+
count_function = Arel::Nodes::NamedFunction.new(
|
43
|
+
'COUNT',
|
44
|
+
[Arel::Nodes::NamedFunction.new('DISTINCT', [tag_table[:name]])]
|
45
|
+
)
|
46
|
+
|
47
|
+
query = Arel::SelectManager.new(self)
|
48
|
+
.from(table_name)
|
49
|
+
.project(arel_table[primary_key])
|
50
|
+
.join(tagging_table).on(tagging_table[:taggable_id].eq(arel_table[primary_key]))
|
51
|
+
.join(tag_table).on(tag_table[:id].eq(tagging_table[:tag_id]))
|
52
|
+
.where(tagging_table[:context].eq(singular_name))
|
53
|
+
.where(tag_table[:name].in(tags))
|
54
|
+
.group(arel_table[primary_key])
|
55
|
+
.having(count_function.eq(tags.size))
|
56
|
+
|
57
|
+
where(arel_table[primary_key].in(query))
|
58
|
+
}
|
59
|
+
|
60
|
+
# Find records without any of the specified tags
|
61
|
+
scope "without_any_#{context}", lambda { |*tags|
|
62
|
+
tags = tags.flatten.compact.uniq
|
63
|
+
return all if tags.empty?
|
64
|
+
|
65
|
+
subquery = joins(tagging_table)
|
66
|
+
.where(tagging_table[:context].eq(singular_name))
|
67
|
+
.where(tagging_table.name => { context: singular_name })
|
68
|
+
.where(tag_table.name => { name: tags })
|
69
|
+
.select(primary_key)
|
70
|
+
|
71
|
+
where.not(primary_key => subquery)
|
72
|
+
}
|
73
|
+
|
74
|
+
# Find records without any tags
|
75
|
+
scope "without_#{context}", lambda {
|
76
|
+
subquery = if setup.polymorphic
|
77
|
+
setup.tagging_class_name.constantize
|
78
|
+
.where(context: singular_name)
|
79
|
+
.where(taggable_type: name)
|
80
|
+
.select(:taggable_id)
|
81
|
+
else
|
82
|
+
setup.tagging_class_name.constantize
|
83
|
+
.where(context: singular_name)
|
84
|
+
.select(:taggable_id)
|
85
|
+
end
|
86
|
+
|
87
|
+
where.not(primary_key => subquery)
|
88
|
+
}
|
89
|
+
|
90
|
+
# Find records with exactly these tags
|
91
|
+
scope "with_exact_#{context}", lambda { |*tags|
|
92
|
+
tags = tags.flatten.compact.uniq
|
93
|
+
|
94
|
+
if tags.empty?
|
95
|
+
send("without_#{context}")
|
96
|
+
else
|
97
|
+
Arel::Nodes::NamedFunction.new(
|
98
|
+
'COUNT',
|
99
|
+
[Arel::Nodes::NamedFunction.new('DISTINCT', [tag_table[:id]])]
|
100
|
+
)
|
101
|
+
|
102
|
+
# Build the query for records having exactly the tags
|
103
|
+
all_tags_query = select(arel_table[primary_key])
|
104
|
+
.joins("INNER JOIN #{tagging_table.name} ON #{tagging_table.name}.taggable_id = #{table_name}.#{primary_key}")
|
105
|
+
.joins("INNER JOIN #{tag_table.name} ON #{tag_table.name}.id = #{tagging_table.name}.tag_id")
|
106
|
+
.where("#{tagging_table.name}.context = ?", context.to_s.singularize)
|
107
|
+
.where("#{tag_table.name}.name IN (?)", tags)
|
108
|
+
.group(arel_table[primary_key])
|
109
|
+
.having("COUNT(DISTINCT #{tag_table.name}.id) = ?", tags.size)
|
110
|
+
|
111
|
+
# Build query for records with other tags
|
112
|
+
other_tags_query = select(arel_table[primary_key])
|
113
|
+
.joins("INNER JOIN #{tagging_table.name} ON #{tagging_table.name}.taggable_id = #{table_name}.#{primary_key}")
|
114
|
+
.joins("INNER JOIN #{tag_table.name} ON #{tag_table.name}.id = #{tagging_table.name}.tag_id")
|
115
|
+
.where("#{tagging_table.name}.context = ?", context.to_s.singularize)
|
116
|
+
.where("#{tag_table.name}.name NOT IN (?)", tags)
|
117
|
+
|
118
|
+
# Combine queries
|
119
|
+
where("#{table_name}.#{primary_key} IN (?)", all_tags_query)
|
120
|
+
.where("#{table_name}.#{primary_key} NOT IN (?)", other_tags_query)
|
121
|
+
end
|
122
|
+
}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoFlyList
|
4
|
+
module TaggableRecord
|
5
|
+
module Query
|
6
|
+
module SqliteStrategy
|
7
|
+
extend BaseStrategy
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def define_query_methods(setup)
|
12
|
+
context = setup.context
|
13
|
+
taggable_klass = setup.taggable_klass
|
14
|
+
tagging_klass = setup.tagging_class_name.constantize
|
15
|
+
tagging_table = tagging_klass.arel_table
|
16
|
+
tag_klass = setup.tag_class_name.constantize
|
17
|
+
tag_table = tag_klass.arel_table
|
18
|
+
singular_name = context.to_s.singularize
|
19
|
+
|
20
|
+
taggable_klass.class_eval do
|
21
|
+
# Find records with any of the specified tags
|
22
|
+
scope "with_any_#{context}", lambda { |*tags|
|
23
|
+
tags = tags.flatten.compact.uniq
|
24
|
+
return none if tags.empty?
|
25
|
+
|
26
|
+
query = Arel::SelectManager.new(self)
|
27
|
+
.from(table_name)
|
28
|
+
.project(arel_table[primary_key])
|
29
|
+
.distinct
|
30
|
+
.join(tagging_table).on(tagging_table[:taggable_id].eq(arel_table[primary_key]))
|
31
|
+
.join(tag_table).on(tag_table[:id].eq(tagging_table[:tag_id]))
|
32
|
+
.where(tagging_table[:context].eq(singular_name))
|
33
|
+
.where(tag_table[:name].in(tags))
|
34
|
+
|
35
|
+
where(arel_table[primary_key].in(query))
|
36
|
+
}
|
37
|
+
|
38
|
+
scope "with_all_#{context}", lambda { |*tags|
|
39
|
+
tags = tags.flatten.compact.uniq
|
40
|
+
return none if tags.empty?
|
41
|
+
|
42
|
+
count_function = Arel::Nodes::NamedFunction.new(
|
43
|
+
'COUNT',
|
44
|
+
[Arel::Nodes::NamedFunction.new('DISTINCT', [tag_table[:name]])]
|
45
|
+
)
|
46
|
+
|
47
|
+
query = Arel::SelectManager.new(self)
|
48
|
+
.from(table_name)
|
49
|
+
.project(arel_table[primary_key])
|
50
|
+
.join(tagging_table).on(tagging_table[:taggable_id].eq(arel_table[primary_key]))
|
51
|
+
.join(tag_table).on(tag_table[:id].eq(tagging_table[:tag_id]))
|
52
|
+
.where(tagging_table[:context].eq(singular_name))
|
53
|
+
.where(tag_table[:name].in(tags))
|
54
|
+
.group(arel_table[primary_key])
|
55
|
+
.having(count_function.eq(tags.size))
|
56
|
+
|
57
|
+
where(arel_table[primary_key].in(query))
|
58
|
+
}
|
59
|
+
|
60
|
+
# Find records without any of the specified tags
|
61
|
+
scope "without_any_#{context}", lambda { |*tags|
|
62
|
+
tags = tags.flatten.compact.uniq
|
63
|
+
return all if tags.empty?
|
64
|
+
|
65
|
+
# Build dynamic joins
|
66
|
+
tagged_ids = distinct
|
67
|
+
.joins("INNER JOIN #{tagging_table} ON #{tagging_table}.taggable_id = #{table_name}.id")
|
68
|
+
.joins("INNER JOIN #{context} ON #{context}.id = #{tagging_table}.tag_id")
|
69
|
+
.where("#{context}.name IN (?)", tags)
|
70
|
+
.pluck("#{table_name}.id")
|
71
|
+
|
72
|
+
# Handle empty tagged_ids explicitly for SQLite compatibility
|
73
|
+
where("#{table_name}.id NOT IN (?)", tagged_ids.present? ? tagged_ids : [-1])
|
74
|
+
}
|
75
|
+
|
76
|
+
# Find records without any tags
|
77
|
+
scope "without_#{context}", lambda {
|
78
|
+
subquery = if setup.polymorphic
|
79
|
+
setup.tagging_class_name.constantize
|
80
|
+
.where(context: singular_name, taggable_type: name)
|
81
|
+
.select(:taggable_id)
|
82
|
+
else
|
83
|
+
setup.tagging_class_name.constantize
|
84
|
+
.where(context: singular_name)
|
85
|
+
.select(:taggable_id)
|
86
|
+
end
|
87
|
+
where('id NOT IN (?)', subquery)
|
88
|
+
}
|
89
|
+
|
90
|
+
# Find records with exactly these tags
|
91
|
+
scope "with_exact_#{context}", lambda { |*tags|
|
92
|
+
tags = tags.flatten.compact.uniq
|
93
|
+
|
94
|
+
if tags.empty?
|
95
|
+
send("without_#{context}")
|
96
|
+
else
|
97
|
+
Arel::Nodes::NamedFunction.new(
|
98
|
+
'COUNT',
|
99
|
+
[Arel::Nodes::NamedFunction.new('DISTINCT', [tag_table[:id]])]
|
100
|
+
)
|
101
|
+
|
102
|
+
# Build the query for records having exactly the tags
|
103
|
+
all_tags_query = select(arel_table[primary_key])
|
104
|
+
.joins("INNER JOIN #{tagging_table.name} ON #{tagging_table.name}.taggable_id = #{table_name}.#{primary_key}")
|
105
|
+
.joins("INNER JOIN #{tag_table.name} ON #{tag_table.name}.id = #{tagging_table.name}.tag_id")
|
106
|
+
.where("#{tagging_table.name}.context = ?", singular_name)
|
107
|
+
.where("#{tag_table.name}.name IN (?)", tags)
|
108
|
+
.group(arel_table[primary_key])
|
109
|
+
.having("COUNT(DISTINCT #{tag_table.name}.id) = ?", tags.size)
|
110
|
+
|
111
|
+
# Build query for records with other tags
|
112
|
+
other_tags_query = select(arel_table[primary_key])
|
113
|
+
.joins("INNER JOIN #{tagging_table.name} ON #{tagging_table.name}.taggable_id = #{table_name}.#{primary_key}")
|
114
|
+
.joins("INNER JOIN #{tag_table.name} ON #{tag_table.name}.id = #{tagging_table.name}.tag_id")
|
115
|
+
.where("#{tagging_table.name}.context = ?", singular_name)
|
116
|
+
.where("#{tag_table.name}.name NOT IN (?)", tags)
|
117
|
+
|
118
|
+
# Combine queries using subqueries
|
119
|
+
where("#{table_name}.#{primary_key} IN (?)", all_tags_query)
|
120
|
+
.where("#{table_name}.#{primary_key} NOT IN (?)", other_tags_query)
|
121
|
+
end
|
122
|
+
}
|
123
|
+
|
124
|
+
# Add tag counts
|
125
|
+
# Find records with exactly these tags
|
126
|
+
scope "with_exact_#{context}", lambda { |*tags|
|
127
|
+
tags = tags.flatten.compact.uniq
|
128
|
+
|
129
|
+
if tags.empty?
|
130
|
+
send("without_#{context}")
|
131
|
+
else
|
132
|
+
Arel::Nodes::NamedFunction.new(
|
133
|
+
'COUNT',
|
134
|
+
[Arel::Nodes::NamedFunction.new('DISTINCT', [tag_table[:id]])]
|
135
|
+
)
|
136
|
+
|
137
|
+
# Build the query for records having exactly the tags
|
138
|
+
all_tags_query = select(arel_table[primary_key])
|
139
|
+
.joins("INNER JOIN #{tagging_table.name} ON #{tagging_table.name}.taggable_id = #{table_name}.#{primary_key}")
|
140
|
+
.joins("INNER JOIN #{tag_table.name} ON #{tag_table.name}.id = #{tagging_table.name}.tag_id")
|
141
|
+
.where("#{tagging_table.name}.context = ?", context.to_s.singularize)
|
142
|
+
.where("#{tag_table.name}.name IN (?)", tags)
|
143
|
+
.group(arel_table[primary_key])
|
144
|
+
.having("COUNT(DISTINCT #{tag_table.name}.id) = ?", tags.size)
|
145
|
+
|
146
|
+
# Build query for records with other tags
|
147
|
+
other_tags_query = select(arel_table[primary_key])
|
148
|
+
.joins("INNER JOIN #{tagging_table.name} ON #{tagging_table.name}.taggable_id = #{table_name}.#{primary_key}")
|
149
|
+
.joins("INNER JOIN #{tag_table.name} ON #{tag_table.name}.id = #{tagging_table.name}.tag_id")
|
150
|
+
.where("#{tagging_table.name}.context = ?", context.to_s.singularize)
|
151
|
+
.where("#{tag_table.name}.name NOT IN (?)", tags)
|
152
|
+
|
153
|
+
# Combine queries
|
154
|
+
where("#{table_name}.#{primary_key} IN (?)", all_tags_query)
|
155
|
+
.where("#{table_name}.#{primary_key} NOT IN (?)", other_tags_query)
|
156
|
+
end
|
157
|
+
}
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -6,93 +6,25 @@ module NoFlyList
|
|
6
6
|
module_function
|
7
7
|
|
8
8
|
def define_query_methods(setup)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
joins(:"#{singular_name}_taggings")
|
20
|
-
.joins(context)
|
21
|
-
.where("#{singular_name}_taggings": { context: singular_name })
|
22
|
-
.where(context => { name: tags })
|
23
|
-
.distinct
|
24
|
-
}
|
25
|
-
|
26
|
-
# Find records without any tags
|
27
|
-
scope "without_#{context}", lambda {
|
28
|
-
where.not(
|
29
|
-
id: setup.tagging_class_name.constantize.where(context: singular_name).select(:taggable_id)
|
30
|
-
)
|
31
|
-
}
|
32
|
-
|
33
|
-
# Find records with all specified tags
|
34
|
-
scope "with_all_#{context}", lambda { |*tags|
|
35
|
-
tags = tags.flatten.compact.uniq
|
36
|
-
return none if tags.empty?
|
37
|
-
|
38
|
-
tag_count = tags.size
|
39
|
-
joins(:"#{singular_name}_taggings")
|
40
|
-
.joins(context)
|
41
|
-
.where("#{singular_name}_taggings": { context: singular_name })
|
42
|
-
.where(context => { name: tags })
|
43
|
-
.group(:id)
|
44
|
-
.having("COUNT(DISTINCT #{context}.name) = ?", tag_count)
|
45
|
-
}
|
46
|
-
|
47
|
-
# Find records without specific tags
|
48
|
-
scope "without_any_#{context}", lambda { |*tags|
|
49
|
-
tags = tags.flatten.compact.uniq
|
50
|
-
return all if tags.empty?
|
51
|
-
|
52
|
-
where.not(
|
53
|
-
id: joins(:"#{singular_name}_taggings")
|
54
|
-
.joins(context)
|
55
|
-
.where("#{singular_name}_taggings": { context: singular_name })
|
56
|
-
.where(context => { name: tags })
|
57
|
-
.select(:id)
|
58
|
-
)
|
59
|
-
}
|
60
|
-
|
61
|
-
# Find records with exactly these tags
|
62
|
-
scope "with_exact_#{context}", lambda { |*tags|
|
63
|
-
tags = tags.flatten.compact.uniq
|
9
|
+
case setup.adapter
|
10
|
+
when :postgresql
|
11
|
+
PostgresqlStrategy.define_query_methods(setup)
|
12
|
+
when :mysql
|
13
|
+
MysqlStrategy.define_query_methods(setup)
|
14
|
+
else
|
15
|
+
SqliteStrategy.define_query_methods(setup)
|
16
|
+
end
|
17
|
+
end
|
64
18
|
|
65
|
-
|
66
|
-
|
67
|
-
else
|
68
|
-
# Get records with the exact count of specified tags
|
69
|
-
having_exact_tags =
|
70
|
-
joins(:"#{singular_name}_taggings")
|
71
|
-
.joins(context)
|
72
|
-
.where("#{singular_name}_taggings": { context: singular_name })
|
73
|
-
.where(context => { name: tags })
|
74
|
-
.group(:id)
|
75
|
-
.having("COUNT(DISTINCT #{context}.name) = ?", tags.size)
|
76
|
-
.select(:id)
|
19
|
+
module BaseStrategy
|
20
|
+
module_function
|
77
21
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
.joins(context)
|
82
|
-
.where("#{singular_name}_taggings": { context: singular_name })
|
83
|
-
.where.not(context => { name: tags })
|
84
|
-
.select(:id)
|
85
|
-
)
|
86
|
-
end
|
87
|
-
}
|
22
|
+
def case_insensitive_where(table, column, values)
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
88
25
|
|
89
|
-
|
90
|
-
|
91
|
-
left_joins(:"#{singular_name}_taggings")
|
92
|
-
.where("#{singular_name}_taggings": { context: singular_name })
|
93
|
-
.group(:id)
|
94
|
-
.select("#{table_name}.*, COUNT(DISTINCT #{singular_name}_taggings.id) as #{context}_count")
|
95
|
-
}
|
26
|
+
def define_query_methods(setup)
|
27
|
+
raise NotImplementedError
|
96
28
|
end
|
97
29
|
end
|
98
30
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoFlyList
|
4
|
+
module TaggableRecord
|
5
|
+
class TagSetup
|
6
|
+
attr_reader :taggable_klass, :context, :transformer, :polymorphic,
|
7
|
+
:restrict_to_existing, :limit,
|
8
|
+
:tag_class_name, :tagging_class_name, :adapter
|
9
|
+
|
10
|
+
def initialize(taggable_klass, context, options = {})
|
11
|
+
@taggable_klass = taggable_klass
|
12
|
+
@context = context
|
13
|
+
@transformer = options.fetch(:transformer, ApplicationTagTransformer)
|
14
|
+
@polymorphic = options.fetch(:polymorphic, false)
|
15
|
+
@restrict_to_existing = options.fetch(:restrict_to_existing, false)
|
16
|
+
@limit = options.fetch(:limit, nil)
|
17
|
+
@tag_class_name = determine_tag_class_name(taggable_klass, options)
|
18
|
+
@tagging_class_name = determine_tagging_class_name(taggable_klass, options)
|
19
|
+
@adapter = determine_adapter
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def determine_adapter
|
25
|
+
case ActiveRecord::Base.connection.adapter_name.downcase
|
26
|
+
when 'postgresql'
|
27
|
+
:postgresql
|
28
|
+
when 'mysql2'
|
29
|
+
:mysql
|
30
|
+
else
|
31
|
+
:sqlite
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def determine_tag_class_name(taggable_klass, options)
|
36
|
+
if options[:polymorphic]
|
37
|
+
Rails.application.config.no_fly_list.tag_class_name
|
38
|
+
else
|
39
|
+
options.fetch(:tag_class_name, "#{taggable_klass.name}Tag")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def determine_tagging_class_name(taggable_klass, options)
|
44
|
+
if options[:polymorphic]
|
45
|
+
Rails.application.config.no_fly_list.tagging_class_name
|
46
|
+
else
|
47
|
+
options.fetch(:tagging_class_name, "#{taggable_klass.name}::Tagging")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -5,22 +5,73 @@ module NoFlyList
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
included do
|
8
|
+
class_attribute :_no_fly_list, instance_writer: false
|
9
|
+
self._no_fly_list = Config.new self
|
10
|
+
|
8
11
|
before_save :save_tag_proxies
|
12
|
+
before_validation :validate_tag_proxies
|
9
13
|
end
|
10
14
|
|
11
15
|
private
|
12
16
|
|
17
|
+
def validate_tag_proxies
|
18
|
+
return true if @validating_proxies
|
19
|
+
|
20
|
+
@validating_proxies = true
|
21
|
+
begin
|
22
|
+
instance_variables.each do |var|
|
23
|
+
next unless var.to_s.match?(/_list_proxy$/)
|
24
|
+
|
25
|
+
proxy = instance_variable_get(var)
|
26
|
+
next if proxy.nil? || proxy.valid?
|
27
|
+
|
28
|
+
proxy.errors.each do |error|
|
29
|
+
errors.add(:base, error.message)
|
30
|
+
end
|
31
|
+
return false
|
32
|
+
end
|
33
|
+
true
|
34
|
+
ensure
|
35
|
+
@validating_proxies = false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
13
39
|
def save_tag_proxies
|
14
|
-
|
15
|
-
|
40
|
+
return true if @saving_proxies
|
41
|
+
|
42
|
+
@saving_proxies = true
|
43
|
+
begin
|
44
|
+
instance_variables.each do |var|
|
45
|
+
next unless var.to_s.match?(/_list_proxy$/)
|
16
46
|
|
17
|
-
|
18
|
-
|
47
|
+
proxy = instance_variable_get(var)
|
48
|
+
next if proxy.nil?
|
49
|
+
return false unless proxy.save
|
50
|
+
end
|
51
|
+
true
|
52
|
+
ensure
|
53
|
+
@saving_proxies = false
|
19
54
|
end
|
20
55
|
end
|
21
56
|
|
57
|
+
def no_fly_list_config
|
58
|
+
self.class._no_fly_list
|
59
|
+
end
|
60
|
+
|
61
|
+
def tag_contexts
|
62
|
+
no_fly_list_config.tag_contexts
|
63
|
+
end
|
64
|
+
|
65
|
+
def options_for_context(context)
|
66
|
+
tag_contexts[context.to_sym]
|
67
|
+
end
|
68
|
+
|
22
69
|
class_methods do
|
23
70
|
def has_tags(*contexts, **options)
|
71
|
+
contexts.each do |context|
|
72
|
+
_no_fly_list.add_context(context, options)
|
73
|
+
end
|
74
|
+
|
24
75
|
Configuration.setup_tagging(self, contexts, options)
|
25
76
|
end
|
26
77
|
end
|
@@ -57,19 +57,48 @@ module NoFlyList
|
|
57
57
|
# @return [Boolean] true if the proxy is valid
|
58
58
|
def save
|
59
59
|
return true unless @pending_changes.any?
|
60
|
+
return false unless valid?
|
61
|
+
|
62
|
+
# Prevent recursive validation
|
63
|
+
@saving = true
|
64
|
+
begin
|
65
|
+
model.class.transaction do
|
66
|
+
# Always save parent first if needed
|
67
|
+
if model.new_record? && !model.save
|
68
|
+
errors.add(:base, 'Failed to save parent record')
|
69
|
+
raise ActiveRecord::Rollback
|
70
|
+
end
|
71
|
+
|
72
|
+
# Clear existing tags
|
73
|
+
model.send(context_taggings).delete_all
|
60
74
|
|
61
|
-
|
62
|
-
@model.transaction do
|
63
|
-
@model.send(@context.to_s).destroy_all
|
75
|
+
# Create new tags
|
64
76
|
@pending_changes.each do |tag_name|
|
65
77
|
tag = find_or_create_tag(tag_name)
|
66
|
-
|
78
|
+
next unless tag
|
79
|
+
|
80
|
+
attributes = {
|
81
|
+
tag: tag,
|
82
|
+
context: @context.to_s.singularize
|
83
|
+
}
|
84
|
+
|
85
|
+
if setup[:polymorphic]
|
86
|
+
attributes[:taggable_type] = model.class.name
|
87
|
+
attributes[:taggable_id] = model.id
|
88
|
+
end
|
89
|
+
|
90
|
+
# Use create! to ensure we catch any errors
|
91
|
+
model.send(context_taggings).create!(attributes)
|
67
92
|
end
|
68
93
|
end
|
94
|
+
|
69
95
|
refresh_from_database
|
70
96
|
true
|
71
|
-
|
97
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
|
98
|
+
errors.add(:base, e.message)
|
72
99
|
false
|
100
|
+
ensure
|
101
|
+
@saving = false
|
73
102
|
end
|
74
103
|
end
|
75
104
|
|
@@ -175,14 +204,6 @@ module NoFlyList
|
|
175
204
|
current_list
|
176
205
|
end
|
177
206
|
|
178
|
-
def current_list
|
179
|
-
if @pending_changes.any?
|
180
|
-
@pending_changes
|
181
|
-
else
|
182
|
-
@model.send(@context.to_s).pluck(:name)
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
207
|
def refresh_from_database
|
187
208
|
@pending_changes = []
|
188
209
|
end
|
@@ -198,7 +219,9 @@ module NoFlyList
|
|
198
219
|
return unless @restrict_to_existing
|
199
220
|
return if @pending_changes.empty?
|
200
221
|
|
201
|
-
|
222
|
+
# Transform tags to lowercase for comparison
|
223
|
+
normalized_changes = @pending_changes.map(&:downcase)
|
224
|
+
existing_tags = @tag_model.where('LOWER(name) IN (?)', normalized_changes).pluck(:name)
|
202
225
|
missing_tags = @pending_changes - existing_tags
|
203
226
|
|
204
227
|
return unless missing_tags.any?
|
@@ -206,6 +229,17 @@ module NoFlyList
|
|
206
229
|
errors.add(:base, "The following tags do not exist: #{missing_tags.join(', ')}")
|
207
230
|
end
|
208
231
|
|
232
|
+
def context_taggings
|
233
|
+
@context_taggings ||= "#{@context.to_s.singularize}_taggings"
|
234
|
+
end
|
235
|
+
|
236
|
+
def setup
|
237
|
+
@setup ||= begin
|
238
|
+
context = @context.to_sym
|
239
|
+
@model.class._no_fly_list.tag_contexts[context]
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
209
243
|
def find_or_create_tag(tag_name)
|
210
244
|
if @restrict_to_existing
|
211
245
|
@tag_model.find_by(name: tag_name)
|
@@ -214,6 +248,49 @@ module NoFlyList
|
|
214
248
|
end
|
215
249
|
end
|
216
250
|
|
251
|
+
def save_changes
|
252
|
+
# Clear existing tags
|
253
|
+
model.send(context_taggings).delete_all
|
254
|
+
|
255
|
+
# Create new tags
|
256
|
+
@pending_changes.each do |tag_name|
|
257
|
+
tag = find_or_create_tag(tag_name)
|
258
|
+
next unless tag
|
259
|
+
|
260
|
+
attributes = {
|
261
|
+
tag: tag,
|
262
|
+
context: @context.to_s.singularize
|
263
|
+
}
|
264
|
+
|
265
|
+
# Add polymorphic attributes for polymorphic tags
|
266
|
+
if setup[:polymorphic]
|
267
|
+
attributes[:taggable_type] = model.class.name
|
268
|
+
attributes[:taggable_id] = model.id
|
269
|
+
end
|
270
|
+
|
271
|
+
# Use create! to ensure we catch any errors
|
272
|
+
model.send(context_taggings).create!(attributes)
|
273
|
+
end
|
274
|
+
|
275
|
+
refresh_from_database
|
276
|
+
true
|
277
|
+
end
|
278
|
+
|
279
|
+
def current_list
|
280
|
+
if @pending_changes.any?
|
281
|
+
@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
|
+
else
|
290
|
+
@model.send(@context.to_s).pluck(:name)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
217
294
|
def limit_reached?
|
218
295
|
@limit && current_list.size >= @limit
|
219
296
|
end
|