no_fly_list 0.1.0 → 0.2.1

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.
@@ -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
- context = setup.context
10
- taggable_klass = setup.taggable_klass
11
- singular_name = context.to_s.singularize
12
-
13
- taggable_klass.class_eval do
14
- # Find records with any of the specified tags
15
- scope "with_any_#{context}", lambda { |*tags|
16
- tags = tags.flatten.compact.uniq
17
- return none if tags.empty?
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
- if tags.empty?
66
- send("without_#{context}")
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
- # Exclude records that have any other tags
79
- having_exact_tags.where.not(
80
- id: joins(:"#{singular_name}_taggings")
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
- # Count tags for each record
90
- scope "#{context}_count", lambda {
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
- instance_variables.each do |var|
15
- next unless var.to_s.match?(/_list_proxy$/)
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
- proxy = instance_variable_get(var)
18
- return false unless proxy.save
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
- if valid?
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
- @model.send(@context.to_s) << tag if tag
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
- else
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
- existing_tags = @tag_model.where(name: @pending_changes).pluck(:name)
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